Skip to content

Commit 1813bc7

Browse files
committed
feat: GsonJsonMapper implements TypedTypeMapper and round-trips JSR-310
1 parent d7c3694 commit 1813bc7

3 files changed

Lines changed: 70 additions & 20 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public class PostDataHandler implements RequestHandler {
4444
byte[] body = request.bytes();
4545
// Loose structural view (Map / List / boxed primitives), produced by the registered TypeMapper.
4646
Object parsed = request.parsed();
47-
// Or, when the JSON mapper is Jackson (a TypedTypeMapper), get a typed POJO directly.
47+
// Or get a typed POJO directly (works with the Gson and Jackson built-ins; both implement
48+
// TypedTypeMapper).
4849
MyDto dto = request.asPojo(MyDto.class);
4950
// Path parameters, query parameters, and headers are also available.
5051
String id = request.pathParam("id");
@@ -117,8 +118,9 @@ public class YourServerLauncher {
117118

118119
The library ships an internal `GsonJsonMapper` that is auto-registered for `application/json` when Gson is on the classpath and no user-supplied JSON mapper has been registered. It:
119120

120-
- Returns JSON integers as `Long` and fractional numbers as `Double`.
121-
- Writes JSR-310 types (`Instant`, `OffsetDateTime`, `ZonedDateTime`, `LocalDateTime`, `LocalDate`, `LocalTime`) as ISO-8601 strings.
121+
- Returns JSON integers as `Long` and fractional numbers as `Double` for the loose `request.parsed()` view.
122+
- For `request.asPojo(MyDto.class)`, delegates to Gson — the target type's fields determine the Java types (`int`, `long`, `Instant`, etc.).
123+
- Round-trips JSR-310 types (`Instant`, `OffsetDateTime`, `ZonedDateTime`, `LocalDateTime`, `LocalDate`, `LocalTime`) as their ISO-8601 string form.
122124

123125
For Jackson, the library ships a `JacksonJsonTypeMapper` adapter that wraps an `ObjectMapper` you configure (modules, naming strategy, JSR-310, date formats — all your call):
124126

src/main/java/com/retailsvc/http/internal/gson/GsonJsonMapper.java

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
import com.google.gson.JsonPrimitive;
1111
import com.google.gson.TypeAdapter;
1212
import com.google.gson.stream.JsonReader;
13+
import com.google.gson.stream.JsonToken;
1314
import com.google.gson.stream.JsonWriter;
14-
import com.retailsvc.http.TypeMapper;
15+
import com.retailsvc.http.TypedTypeMapper;
1516
import java.io.IOException;
1617
import java.nio.charset.StandardCharsets;
1718
import java.time.Instant;
@@ -27,28 +28,36 @@
2728
import java.util.function.Function;
2829

2930
/**
30-
* Built-in {@link TypeMapper} for {@code application/json} backed by Gson. Auto-registered by
31+
* Built-in {@link TypedTypeMapper} for {@code application/json} backed by Gson. Auto-registered by
3132
* {@link com.retailsvc.http.OpenApiServer.Builder} when Gson is on the classpath and no
3233
* user-supplied JSON mapper has been registered.
3334
*
34-
* <p>JSON numbers without a decimal point or exponent are returned as {@code Long}; fractional
35-
* numbers are returned as {@code Double}. JSR-310 types ({@code Instant}, {@code OffsetDateTime},
36-
* {@code ZonedDateTime}, {@code LocalDateTime}, {@code LocalDate}, {@code LocalTime}) are written
37-
* as their ISO-8601 string form.
35+
* <p>The loose {@link #readFrom(byte[], String)} path returns JSON numbers without a decimal point
36+
* or exponent as {@code Long} and fractional numbers as {@code Double}. The typed {@link
37+
* #readAs(byte[], String, Class)} path delegates to Gson, so target-type fields determine the
38+
* resulting Java types (an {@code int} field gets an {@code int}, an {@code Instant} field gets an
39+
* {@code Instant}, etc.).
40+
*
41+
* <p>JSR-310 types ({@code Instant}, {@code OffsetDateTime}, {@code ZonedDateTime}, {@code
42+
* LocalDateTime}, {@code LocalDate}, {@code LocalTime}) are round-tripped as their ISO-8601 string
43+
* form.
3844
*/
39-
public final class GsonJsonMapper implements TypeMapper {
45+
public final class GsonJsonMapper implements TypedTypeMapper {
4046

4147
private final Gson gson;
4248

4349
public GsonJsonMapper() {
4450
this.gson =
4551
new GsonBuilder()
46-
.registerTypeAdapter(Instant.class, isoStringWriter(Instant::toString))
47-
.registerTypeAdapter(OffsetDateTime.class, isoStringWriter(OffsetDateTime::toString))
48-
.registerTypeAdapter(ZonedDateTime.class, isoStringWriter(ZonedDateTime::toString))
49-
.registerTypeAdapter(LocalDateTime.class, isoStringWriter(LocalDateTime::toString))
50-
.registerTypeAdapter(LocalDate.class, isoStringWriter(LocalDate::toString))
51-
.registerTypeAdapter(LocalTime.class, isoStringWriter(LocalTime::toString))
52+
.registerTypeAdapter(Instant.class, iso(Instant::toString, Instant::parse))
53+
.registerTypeAdapter(
54+
OffsetDateTime.class, iso(OffsetDateTime::toString, OffsetDateTime::parse))
55+
.registerTypeAdapter(
56+
ZonedDateTime.class, iso(ZonedDateTime::toString, ZonedDateTime::parse))
57+
.registerTypeAdapter(
58+
LocalDateTime.class, iso(LocalDateTime::toString, LocalDateTime::parse))
59+
.registerTypeAdapter(LocalDate.class, iso(LocalDate::toString, LocalDate::parse))
60+
.registerTypeAdapter(LocalTime.class, iso(LocalTime::toString, LocalTime::parse))
5261
.create();
5362
}
5463

@@ -58,6 +67,11 @@ public Object readFrom(byte[] body, String contentTypeHeader) {
5867
return toJavaObject(element);
5968
}
6069

70+
@Override
71+
public <T> T readAs(byte[] body, String contentTypeHeader, Class<T> type) {
72+
return gson.fromJson(new String(body, StandardCharsets.UTF_8), type);
73+
}
74+
6175
@Override
6276
public byte[] writeTo(Object value) {
6377
return gson.toJson(value).getBytes(StandardCharsets.UTF_8);
@@ -120,7 +134,8 @@ private static Object toNumber(String raw) {
120134
return Double.parseDouble(raw);
121135
}
122136

123-
private static <T> TypeAdapter<T> isoStringWriter(Function<T, String> toIso) {
137+
/** Round-trips a JSR-310 type as an ISO-8601 string. */
138+
private static <T> TypeAdapter<T> iso(Function<T, String> toIso, Function<String, T> fromIso) {
124139
return new TypeAdapter<T>() {
125140
@Override
126141
public void write(JsonWriter out, T value) throws IOException {
@@ -132,9 +147,12 @@ public void write(JsonWriter out, T value) throws IOException {
132147
}
133148

134149
@Override
135-
public T read(JsonReader in) {
136-
throw new UnsupportedOperationException(
137-
"GsonJsonMapper does not parse JSR-310 types; values arrive as String");
150+
public T read(JsonReader in) throws IOException {
151+
if (in.peek() == JsonToken.NULL) {
152+
in.nextNull();
153+
return null;
154+
}
155+
return fromIso.apply(in.nextString());
138156
}
139157
};
140158
}

src/test/java/com/retailsvc/http/internal/gson/GsonJsonMapperTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,36 @@ void writesLocalTimeAsIso8601() {
9898
.isEqualTo("{\"t\":\"10:00\"}");
9999
}
100100

101+
@Test
102+
void readAsDeserialisesPojo() {
103+
Item item = mapper.readAs(bytes("{\"id\":\"x-1\",\"qty\":7}"), "application/json", Item.class);
104+
105+
assertThat(item.id).isEqualTo("x-1");
106+
assertThat(item.qty).isEqualTo(7);
107+
}
108+
109+
@Test
110+
void readAsRoundTripsJsr310Fields() {
111+
WithDates value =
112+
mapper.readAs(
113+
bytes("{\"ts\":\"2026-05-13T10:00:00Z\",\"day\":\"2026-05-13\"}"),
114+
"application/json",
115+
WithDates.class);
116+
117+
assertThat(value.ts).isEqualTo(Instant.parse("2026-05-13T10:00:00Z"));
118+
assertThat(value.day).isEqualTo(LocalDate.of(2026, 5, 13));
119+
}
120+
121+
static final class Item {
122+
String id;
123+
int qty;
124+
}
125+
126+
static final class WithDates {
127+
Instant ts;
128+
LocalDate day;
129+
}
130+
101131
private static byte[] bytes(String s) {
102132
return s.getBytes(StandardCharsets.UTF_8);
103133
}

0 commit comments

Comments
 (0)