Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/asciidoc/modules/modules.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Modules are distributed as separate dependencies. Below is the catalog of offici
* link:{uiVersion}/modules/gson[Gson]: Gson module for Jooby.
* link:{uiVersion}/modules/jackson2[Jackson2]: Jackson2 module for Jooby.
* link:{uiVersion}/modules/jackson3[Jackson3]: Jackson3 module for Jooby.
* link:{uiVersion}/modules/yasson[JSON-B]: JSON-B module for Jooby.
* link:{uiVersion}/modules/yasson[Yasson]: JSON-B module for Jooby.

==== OpenAPI
* link:{uiVersion}/modules/openapi[OpenAPI]: OpenAPI supports.
Expand Down
31 changes: 31 additions & 0 deletions jooby/src/main/java/io/jooby/json/JsonCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.json;

/**
* A unified contract for complete JSON processing, combining both serialization and deserialization
* capabilities.
*
* <p>This interface acts as a convenient composite of {@link JsonEncoder} and {@link JsonDecoder}.
* Implementations of this interface (such as Jooby's Jackson, Gson, or Moshi integration modules)
* provide full-stack JSON support. This allows a Jooby application to seamlessly parse incoming
* JSON request bodies into Java objects, and render outgoing Java objects as JSON responses.
*
* <p>By providing a single interface that encompasses both directions of data binding, JSON
* libraries can be easily registered into the Jooby environment to handle all JSON-related content
* negotiation.
*
* <p><strong>Important Note:</strong> Jooby core itself <em>does not</em> implement these
* interfaces. These contracts act as a bridge and are designed to be implemented exclusively by
* dedicated JSON modules (such as {@code jooby-jackson}, {@code jooby-gson}, or {@code
* jooby-avaje-json}), etc.
*
* @see JsonEncoder
* @see JsonDecoder
* @since 4.5.0
* @author edgar
*/
public interface JsonCodec extends JsonEncoder, JsonDecoder {}
74 changes: 74 additions & 0 deletions jooby/src/main/java/io/jooby/json/JsonDecoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.json;

import java.lang.reflect.Type;

import io.jooby.Reified;

/**
* Contract for decoding (deserializing) JSON strings into Java objects.
*
* <p>This functional interface provides the core deserialization strategy for Jooby. It is designed
* to be implemented by specific JSON library integrations (such as Jackson, Gson, Moshi, etc.) to
* adapt their internal parsing mechanics to Jooby's standard architecture.
*
* <p><strong>Important Note:</strong> Jooby core itself <em>does not</em> implement these
* interfaces. These contracts act as a bridge and are designed to be implemented exclusively by
* dedicated JSON modules (such as {@code jooby-jackson}, {@code jooby-gson}, or {@code
* jooby-avaje-json}), etc.
*
* @since 4.5.0
* @author edgar
*/
@FunctionalInterface
public interface JsonDecoder {

/**
* Decodes a JSON string into the specified Java {@link java.lang.reflect.Type}.
*
* <p>This is the primary decoding method that all underlying JSON libraries must implement. It
* accepts a raw reflection {@code Type}, making it capable of handling both simple classes and
* complex parameterized/generic types (e.g., {@code List<MyObject>}).
*
* @param json The JSON payload as a string.
* @param type The target Java reflection type to deserialize into.
* @return The deserialized Java object instance.
* @param <T> The expected generic type of the returned object.
*/
<T> T decode(String json, Type type);

/**
* Decodes a JSON string into the specified Java {@link Class}.
*
* <p>This is a convenience method for deserializing simple, non-generic types. It delegates
* directly to {@link #decode(String, Type)}.
*
* @param json The JSON payload as a string.
* @param type The target Java class to deserialize into.
* @return The deserialized Java object instance.
* @param <T> The expected generic type of the returned object.
*/
default <T> T decode(String json, Class<T> type) {
return decode(json, (java.lang.reflect.Type) type);
}

/**
* Decodes a JSON string into the specified Jooby {@link Reified} type.
*
* <p>This is a convenience method for deserializing complex, generic types while avoiding type
* erasure. It extracts the underlying reflection type from the {@code Reified} token and
* delegates to {@link #decode(String, Type)}.
*
* @param json The JSON payload as a string.
* @param type The Reified type token capturing the target type.
* @return The deserialized Java object instance.
* @param <T> The expected generic type of the returned object.
*/
default <T> T decode(String json, Reified<T> type) {
return decode(json, type.getType());
}
}
34 changes: 34 additions & 0 deletions jooby/src/main/java/io/jooby/json/JsonEncoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.json;

/**
* Contract for encoding (serializing) Java objects into JSON strings.
*
* <p><strong>Important Note:</strong> Jooby core itself <em>does not</em> implement these
* interfaces. These contracts act as a bridge and are designed to be implemented exclusively by
* dedicated JSON modules (such as {@code jooby-jackson}, {@code jooby-gson}, or {@code
* jooby-avaje-json}), etc.
*
* @since 4.5.0
* @author edgar
*/
@FunctionalInterface
public interface JsonEncoder {

/**
* Encodes a Java object into its JSON string representation.
*
* <p>This method takes an arbitrary Java object and converts it into a valid JSON payload.
* Implementations are responsible for handling the specific serialization rules, configurations,
* and exception management of their underlying JSON library.
*
* @param value The Java object to serialize. This can be a simple data type, a collection, or a
* complex custom bean.
* @return The JSON string representation of the provided object.
*/
String encode(Object value);
}
30 changes: 30 additions & 0 deletions jooby/src/main/java/io/jooby/json/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Provides the core JSON processing contracts and abstractions for the Jooby framework.
*
* <p>This package defines the foundational interfaces (such as {@link io.jooby.json.JsonEncoder},
* {@link io.jooby.json.JsonDecoder}, and {@link io.jooby.json.JsonCodec}) that allow Jooby to
* integrate seamlessly with various external JSON libraries (like Jackson, Gson, or Moshi). By
* implementing these contracts, those libraries can participate in Jooby's content negotiation,
* enabling automatic serialization of HTTP responses and deserialization of HTTP request bodies.
*
* <h2>Null-Safety Guarantee</h2>
*
* <p>This package is explicitly marked with {@link org.jspecify.annotations.NullMarked}. This
* establishes a strict nullability contract where all types (parameters, return types, and fields)
* within this package are considered <strong>non-null by default</strong>, unless explicitly
* annotated otherwise (e.g., using {@code @Nullable}).
*
* <p>Adopting JSpecify semantics ensures excellent interoperability with null-safe languages like
* Kotlin and provides robust guarantees for modern static code analysis tools.
*
* <p><strong>Important Note:</strong> Jooby core itself <em>does not</em> implement these
* interfaces. These contracts act as a bridge and are designed to be implemented exclusively by
* dedicated JSON modules (such as {@code jooby-jackson}, {@code jooby-gson}, or {@code
* jooby-avaje-json}), etc.
*
* @see io.jooby.json.JsonCodec
* @author edgar
* @since 4.5.0
*/
@org.jspecify.annotations.NullMarked
package io.jooby.json;
1 change: 1 addition & 0 deletions jooby/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
exports io.jooby;
exports io.jooby.annotation;
exports io.jooby.exception;
exports io.jooby.json;
exports io.jooby.handler;
exports io.jooby.validation;
exports io.jooby.problem;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.avaje.jsonb;

import java.lang.reflect.Type;

import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import io.jooby.json.JsonCodec;

class AvajeJsonCodec implements JsonCodec {
private final Jsonb jsonb;

public AvajeJsonCodec(Jsonb jsonb) {
this.jsonb = jsonb;
}

@SuppressWarnings("unchecked")
@Override
public <T> T decode(String json, Type type) {
var jsonType = (JsonType<T>) jsonb.type(type);
return jsonType.fromJson(json);
}

@Override
public String encode(Object value) {
return jsonb.toJson(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
import io.avaje.jsonb.Jsonb;
import io.jooby.*;
import io.jooby.internal.avaje.jsonb.*;
import io.jooby.json.JsonCodec;
import io.jooby.json.JsonDecoder;
import io.jooby.json.JsonEncoder;
import io.jooby.output.Output;

/**
Expand Down Expand Up @@ -85,6 +88,11 @@ public void install(Jooby application) throws Exception {

var services = application.getServices();
services.put(Jsonb.class, jsonb);
// JsonCodec
var jsonCodec = new AvajeJsonCodec(jsonb);
services.putIfAbsent(JsonCodec.class, jsonCodec);
services.putIfAbsent(JsonEncoder.class, jsonCodec);
services.putIfAbsent(JsonDecoder.class, jsonCodec);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.avaje.jsonb;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import io.avaje.jsonb.Jsonb;
import io.jooby.Reified;

class AvajeJsonCodecTest {

private AvajeJsonCodec codec;

@BeforeEach
void setUp() {
Jsonb jsonb = Jsonb.builder().build();
codec = new AvajeJsonCodec(jsonb);
}

@Test
void shouldEncodeMapToJson() {
// Using a LinkedHashMap guarantees the order of the keys in the output JSON
Map<String, Integer> map = new java.util.LinkedHashMap<>();
map.put("Alice", 30);
map.put("Bob", 25);

String json = codec.encode(map);

assertEquals("{\"Alice\":30,\"Bob\":25}", json);
}

@Test
void shouldDecodeJsonToGenericMapUsingReified() {
String json = "{\"Alice\":30,\"Bob\":25}";

// Using the anonymous subclass trick to capture Map<String, Integer> without type erasure
Map<String, Integer> map = codec.decode(json, Reified.map(String.class, Integer.class));

assertNotNull(map);
assertEquals(2, map.size());

assertEquals(30, map.get("Alice"));
assertEquals(25, map.get("Bob"));
}

@Test
void shouldDecodeJsonToGenericListMapUsingReified() {
String json = "[{\"Alice\":30,\"Bob\":25}]";

// Using the anonymous subclass trick to capture Map<String, Integer> without type erasure
List<Map<String, Integer>> list =
codec.decode(json, Reified.list(Reified.map(String.class, Integer.class)));

assertNotNull(list);
assertEquals(1, list.size());

var map = list.getFirst();

assertEquals(30, map.get("Alice"));
assertEquals(25, map.get("Bob"));
}
}
29 changes: 29 additions & 0 deletions modules/jooby-gson/src/main/java/io/jooby/gson/GsonJsonCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.gson;

import java.lang.reflect.Type;

import com.google.gson.Gson;
import io.jooby.json.JsonCodec;

public class GsonJsonCodec implements JsonCodec {
private final Gson gson;

public GsonJsonCodec(Gson gson) {
this.gson = gson;
}

@Override
public <T> T decode(String json, Type type) {
return gson.fromJson(json, type);
}

@Override
public String encode(Object value) {
return gson.toJson(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import io.jooby.MediaType;
import io.jooby.MessageDecoder;
import io.jooby.MessageEncoder;
import io.jooby.json.JsonCodec;
import io.jooby.json.JsonDecoder;
import io.jooby.json.JsonEncoder;
import io.jooby.output.Output;

/**
Expand Down Expand Up @@ -90,6 +93,11 @@ public void install(Jooby application) {

var services = application.getServices();
services.put(Gson.class, gson);
// JsonCodec
var jsonCodec = new GsonJsonCodec(gson);
services.putIfAbsent(JsonCodec.class, jsonCodec);
services.putIfAbsent(JsonEncoder.class, jsonCodec);
services.putIfAbsent(JsonDecoder.class, jsonCodec);
}

@Override
Expand Down
Loading
Loading