Skip to content

Commit 8adb596

Browse files
authored
Merge pull request #3908 from jooby-project/3904
feature: Generic JSON Codec Abstraction (JsonCodec)
2 parents 37bfa4b + baeb3e9 commit 8adb596

24 files changed

Lines changed: 872 additions & 2 deletions

File tree

docs/asciidoc/modules/modules.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Modules are distributed as separate dependencies. Below is the catalog of offici
4949
* link:{uiVersion}/modules/gson[Gson]: Gson module for Jooby.
5050
* link:{uiVersion}/modules/jackson2[Jackson2]: Jackson2 module for Jooby.
5151
* link:{uiVersion}/modules/jackson3[Jackson3]: Jackson3 module for Jooby.
52-
* link:{uiVersion}/modules/yasson[JSON-B]: JSON-B module for Jooby.
52+
* link:{uiVersion}/modules/yasson[Yasson]: JSON-B module for Jooby.
5353

5454
==== OpenAPI
5555
* link:{uiVersion}/modules/openapi[OpenAPI]: OpenAPI supports.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.json;
7+
8+
/**
9+
* A unified contract for complete JSON processing, combining both serialization and deserialization
10+
* capabilities.
11+
*
12+
* <p>This interface acts as a convenient composite of {@link JsonEncoder} and {@link JsonDecoder}.
13+
* Implementations of this interface (such as Jooby's Jackson, Gson, or Moshi integration modules)
14+
* provide full-stack JSON support. This allows a Jooby application to seamlessly parse incoming
15+
* JSON request bodies into Java objects, and render outgoing Java objects as JSON responses.
16+
*
17+
* <p>By providing a single interface that encompasses both directions of data binding, JSON
18+
* libraries can be easily registered into the Jooby environment to handle all JSON-related content
19+
* negotiation.
20+
*
21+
* <p><strong>Important Note:</strong> Jooby core itself <em>does not</em> implement these
22+
* interfaces. These contracts act as a bridge and are designed to be implemented exclusively by
23+
* dedicated JSON modules (such as {@code jooby-jackson}, {@code jooby-gson}, or {@code
24+
* jooby-avaje-json}), etc.
25+
*
26+
* @see JsonEncoder
27+
* @see JsonDecoder
28+
* @since 4.5.0
29+
* @author edgar
30+
*/
31+
public interface JsonCodec extends JsonEncoder, JsonDecoder {}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.json;
7+
8+
import java.lang.reflect.Type;
9+
10+
import io.jooby.Reified;
11+
12+
/**
13+
* Contract for decoding (deserializing) JSON strings into Java objects.
14+
*
15+
* <p>This functional interface provides the core deserialization strategy for Jooby. It is designed
16+
* to be implemented by specific JSON library integrations (such as Jackson, Gson, Moshi, etc.) to
17+
* adapt their internal parsing mechanics to Jooby's standard architecture.
18+
*
19+
* <p><strong>Important Note:</strong> Jooby core itself <em>does not</em> implement these
20+
* interfaces. These contracts act as a bridge and are designed to be implemented exclusively by
21+
* dedicated JSON modules (such as {@code jooby-jackson}, {@code jooby-gson}, or {@code
22+
* jooby-avaje-json}), etc.
23+
*
24+
* @since 4.5.0
25+
* @author edgar
26+
*/
27+
@FunctionalInterface
28+
public interface JsonDecoder {
29+
30+
/**
31+
* Decodes a JSON string into the specified Java {@link java.lang.reflect.Type}.
32+
*
33+
* <p>This is the primary decoding method that all underlying JSON libraries must implement. It
34+
* accepts a raw reflection {@code Type}, making it capable of handling both simple classes and
35+
* complex parameterized/generic types (e.g., {@code List<MyObject>}).
36+
*
37+
* @param json The JSON payload as a string.
38+
* @param type The target Java reflection type to deserialize into.
39+
* @return The deserialized Java object instance.
40+
* @param <T> The expected generic type of the returned object.
41+
*/
42+
<T> T decode(String json, Type type);
43+
44+
/**
45+
* Decodes a JSON string into the specified Java {@link Class}.
46+
*
47+
* <p>This is a convenience method for deserializing simple, non-generic types. It delegates
48+
* directly to {@link #decode(String, Type)}.
49+
*
50+
* @param json The JSON payload as a string.
51+
* @param type The target Java class to deserialize into.
52+
* @return The deserialized Java object instance.
53+
* @param <T> The expected generic type of the returned object.
54+
*/
55+
default <T> T decode(String json, Class<T> type) {
56+
return decode(json, (java.lang.reflect.Type) type);
57+
}
58+
59+
/**
60+
* Decodes a JSON string into the specified Jooby {@link Reified} type.
61+
*
62+
* <p>This is a convenience method for deserializing complex, generic types while avoiding type
63+
* erasure. It extracts the underlying reflection type from the {@code Reified} token and
64+
* delegates to {@link #decode(String, Type)}.
65+
*
66+
* @param json The JSON payload as a string.
67+
* @param type The Reified type token capturing the target type.
68+
* @return The deserialized Java object instance.
69+
* @param <T> The expected generic type of the returned object.
70+
*/
71+
default <T> T decode(String json, Reified<T> type) {
72+
return decode(json, type.getType());
73+
}
74+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.json;
7+
8+
/**
9+
* Contract for encoding (serializing) Java objects into JSON strings.
10+
*
11+
* <p><strong>Important Note:</strong> Jooby core itself <em>does not</em> implement these
12+
* interfaces. These contracts act as a bridge and are designed to be implemented exclusively by
13+
* dedicated JSON modules (such as {@code jooby-jackson}, {@code jooby-gson}, or {@code
14+
* jooby-avaje-json}), etc.
15+
*
16+
* @since 4.5.0
17+
* @author edgar
18+
*/
19+
@FunctionalInterface
20+
public interface JsonEncoder {
21+
22+
/**
23+
* Encodes a Java object into its JSON string representation.
24+
*
25+
* <p>This method takes an arbitrary Java object and converts it into a valid JSON payload.
26+
* Implementations are responsible for handling the specific serialization rules, configurations,
27+
* and exception management of their underlying JSON library.
28+
*
29+
* @param value The Java object to serialize. This can be a simple data type, a collection, or a
30+
* complex custom bean.
31+
* @return The JSON string representation of the provided object.
32+
*/
33+
String encode(Object value);
34+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Provides the core JSON processing contracts and abstractions for the Jooby framework.
3+
*
4+
* <p>This package defines the foundational interfaces (such as {@link io.jooby.json.JsonEncoder},
5+
* {@link io.jooby.json.JsonDecoder}, and {@link io.jooby.json.JsonCodec}) that allow Jooby to
6+
* integrate seamlessly with various external JSON libraries (like Jackson, Gson, or Moshi). By
7+
* implementing these contracts, those libraries can participate in Jooby's content negotiation,
8+
* enabling automatic serialization of HTTP responses and deserialization of HTTP request bodies.
9+
*
10+
* <h2>Null-Safety Guarantee</h2>
11+
*
12+
* <p>This package is explicitly marked with {@link org.jspecify.annotations.NullMarked}. This
13+
* establishes a strict nullability contract where all types (parameters, return types, and fields)
14+
* within this package are considered <strong>non-null by default</strong>, unless explicitly
15+
* annotated otherwise (e.g., using {@code @Nullable}).
16+
*
17+
* <p>Adopting JSpecify semantics ensures excellent interoperability with null-safe languages like
18+
* Kotlin and provides robust guarantees for modern static code analysis tools.
19+
*
20+
* <p><strong>Important Note:</strong> Jooby core itself <em>does not</em> implement these
21+
* interfaces. These contracts act as a bridge and are designed to be implemented exclusively by
22+
* dedicated JSON modules (such as {@code jooby-jackson}, {@code jooby-gson}, or {@code
23+
* jooby-avaje-json}), etc.
24+
*
25+
* @see io.jooby.json.JsonCodec
26+
* @author edgar
27+
* @since 4.5.0
28+
*/
29+
@org.jspecify.annotations.NullMarked
30+
package io.jooby.json;

jooby/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
exports io.jooby;
1010
exports io.jooby.annotation;
1111
exports io.jooby.exception;
12+
exports io.jooby.json;
1213
exports io.jooby.handler;
1314
exports io.jooby.validation;
1415
exports io.jooby.problem;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.avaje.jsonb;
7+
8+
import java.lang.reflect.Type;
9+
10+
import io.avaje.jsonb.JsonType;
11+
import io.avaje.jsonb.Jsonb;
12+
import io.jooby.json.JsonCodec;
13+
14+
class AvajeJsonCodec implements JsonCodec {
15+
private final Jsonb jsonb;
16+
17+
public AvajeJsonCodec(Jsonb jsonb) {
18+
this.jsonb = jsonb;
19+
}
20+
21+
@SuppressWarnings("unchecked")
22+
@Override
23+
public <T> T decode(String json, Type type) {
24+
var jsonType = (JsonType<T>) jsonb.type(type);
25+
return jsonType.fromJson(json);
26+
}
27+
28+
@Override
29+
public String encode(Object value) {
30+
return jsonb.toJson(value);
31+
}
32+
}

modules/jooby-avaje-jsonb/src/main/java/io/jooby/avaje/jsonb/AvajeJsonbModule.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
import io.avaje.jsonb.Jsonb;
1515
import io.jooby.*;
1616
import io.jooby.internal.avaje.jsonb.*;
17+
import io.jooby.json.JsonCodec;
18+
import io.jooby.json.JsonDecoder;
19+
import io.jooby.json.JsonEncoder;
1720
import io.jooby.output.Output;
1821

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

8689
var services = application.getServices();
8790
services.put(Jsonb.class, jsonb);
91+
// JsonCodec
92+
var jsonCodec = new AvajeJsonCodec(jsonb);
93+
services.putIfAbsent(JsonCodec.class, jsonCodec);
94+
services.putIfAbsent(JsonEncoder.class, jsonCodec);
95+
services.putIfAbsent(JsonDecoder.class, jsonCodec);
8896
}
8997

9098
@Override
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.avaje.jsonb;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertNotNull;
10+
11+
import java.util.List;
12+
import java.util.Map;
13+
14+
import org.junit.jupiter.api.BeforeEach;
15+
import org.junit.jupiter.api.Test;
16+
17+
import io.avaje.jsonb.Jsonb;
18+
import io.jooby.Reified;
19+
20+
class AvajeJsonCodecTest {
21+
22+
private AvajeJsonCodec codec;
23+
24+
@BeforeEach
25+
void setUp() {
26+
Jsonb jsonb = Jsonb.builder().build();
27+
codec = new AvajeJsonCodec(jsonb);
28+
}
29+
30+
@Test
31+
void shouldEncodeMapToJson() {
32+
// Using a LinkedHashMap guarantees the order of the keys in the output JSON
33+
Map<String, Integer> map = new java.util.LinkedHashMap<>();
34+
map.put("Alice", 30);
35+
map.put("Bob", 25);
36+
37+
String json = codec.encode(map);
38+
39+
assertEquals("{\"Alice\":30,\"Bob\":25}", json);
40+
}
41+
42+
@Test
43+
void shouldDecodeJsonToGenericMapUsingReified() {
44+
String json = "{\"Alice\":30,\"Bob\":25}";
45+
46+
// Using the anonymous subclass trick to capture Map<String, Integer> without type erasure
47+
Map<String, Integer> map = codec.decode(json, Reified.map(String.class, Integer.class));
48+
49+
assertNotNull(map);
50+
assertEquals(2, map.size());
51+
52+
assertEquals(30, map.get("Alice"));
53+
assertEquals(25, map.get("Bob"));
54+
}
55+
56+
@Test
57+
void shouldDecodeJsonToGenericListMapUsingReified() {
58+
String json = "[{\"Alice\":30,\"Bob\":25}]";
59+
60+
// Using the anonymous subclass trick to capture Map<String, Integer> without type erasure
61+
List<Map<String, Integer>> list =
62+
codec.decode(json, Reified.list(Reified.map(String.class, Integer.class)));
63+
64+
assertNotNull(list);
65+
assertEquals(1, list.size());
66+
67+
var map = list.getFirst();
68+
69+
assertEquals(30, map.get("Alice"));
70+
assertEquals(25, map.get("Bob"));
71+
}
72+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.gson;
7+
8+
import java.lang.reflect.Type;
9+
10+
import com.google.gson.Gson;
11+
import io.jooby.json.JsonCodec;
12+
13+
public class GsonJsonCodec implements JsonCodec {
14+
private final Gson gson;
15+
16+
public GsonJsonCodec(Gson gson) {
17+
this.gson = gson;
18+
}
19+
20+
@Override
21+
public <T> T decode(String json, Type type) {
22+
return gson.fromJson(json, type);
23+
}
24+
25+
@Override
26+
public String encode(Object value) {
27+
return gson.toJson(value);
28+
}
29+
}

0 commit comments

Comments
 (0)