Skip to content

Commit 9cf7af8

Browse files
authored
feat: Add Jackson3JsonTypeMapper and rename Jackson v2 adapter (#61)
Renames JacksonJsonTypeMapper to Jackson2JsonTypeMapper and introduces Jackson3JsonTypeMapper backed by tools.jackson.databind (Jackson 3.1.3). Both adapters ship side-by-side as optional dependencies; consumers pick whichever matches their classpath. Jackson 3 made I/O exceptions unchecked, so the v3 adapter no longer wraps them in UncheckedIOException.
1 parent 53b347b commit 9cf7af8

7 files changed

Lines changed: 147 additions & 11 deletions

File tree

README.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ It is designed to be simple to use while providing the essential features needed
2424
- An OpenAPI 3.1.x specification (`openapi.json` or `openapi.yaml`).
2525
- For `application/json` request/response bodies, either:
2626
- Gson on the classpath — auto-registered via the built-in `GsonJsonMapper` (integer-preserving, JSR-310 written as ISO-8601), or
27-
- Jackson via the built-in `JacksonJsonTypeMapper(ObjectMapper)` adapter (caller supplies a configured `ObjectMapper`), or
27+
- Jackson via the built-in adapters — `Jackson2JsonTypeMapper(ObjectMapper)` for Jackson 2.x (`com.fasterxml.jackson.*`) or `Jackson3JsonTypeMapper(ObjectMapper)` for Jackson 3.x (`tools.jackson.*`). Caller supplies a configured `ObjectMapper`; the two adapters use disjoint package roots and can coexist on the same classpath.
2828
- any other `TypeMapper` you register via `Builder.jsonMapper(mapper)` (shortcut for `bodyMapper("application/json", mapper)`).
2929
- Built-in mappers for `application/x-www-form-urlencoded` and `text/plain` need no configuration. Any other media type (`application/xml`, `application/cbor`, etc.) requires registering its own `TypeMapper`.
3030

@@ -123,20 +123,42 @@ The library ships an internal `GsonJsonMapper` that is auto-registered for `appl
123123
- For `request.asPojo(MyDto.class)`, delegates to Gson — the target type's fields determine the Java types (`int`, `long`, `Instant`, etc.).
124124
- Round-trips JSR-310 types (`Instant`, `OffsetDateTime`, `ZonedDateTime`, `LocalDateTime`, `LocalDate`, `LocalTime`) as their ISO-8601 string form.
125125

126-
For Jackson, the library ships a `JacksonJsonTypeMapper` adapter that wraps an `ObjectMapper` you configure (modules, naming strategy, JSR-310, date formats — all your call):
126+
For Jackson, the library ships two adapters that wrap an `ObjectMapper` you configure (modules, naming strategy, JSR-310, date formats — all your call). Pick the one that matches your Jackson major:
127+
128+
```java
129+
// Jackson 2.x (group: com.fasterxml.jackson.core)
130+
import com.fasterxml.jackson.databind.ObjectMapper;
131+
import com.fasterxml.jackson.databind.SerializationFeature;
132+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
127133

128-
``` java
129134
ObjectMapper objectMapper = new ObjectMapper()
130135
.registerModule(new JavaTimeModule())
131136
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
132137

133138
var server = OpenApiServer.builder()
134139
.spec(spec)
135-
.jsonMapper(new JacksonJsonTypeMapper(objectMapper))
140+
.jsonMapper(new Jackson2JsonTypeMapper(objectMapper))
141+
.handlers(handlers)
142+
.build();
143+
```
144+
145+
```java
146+
// Jackson 3.x (group: tools.jackson.core)
147+
import tools.jackson.databind.ObjectMapper;
148+
149+
ObjectMapper objectMapper = ObjectMapper.builder()
150+
// ... configure modules, features, etc.
151+
.build();
152+
153+
var server = OpenApiServer.builder()
154+
.spec(spec)
155+
.jsonMapper(new Jackson3JsonTypeMapper(objectMapper))
136156
.handlers(handlers)
137157
.build();
138158
```
139159

160+
Jackson 3 made all I/O exceptions unchecked (`tools.jackson.core.JacksonException extends RuntimeException`), so `Jackson3JsonTypeMapper` propagates read/write failures as-is. `Jackson2JsonTypeMapper` wraps Jackson 2's checked `IOException` in `UncheckedIOException`.
161+
140162
The same shape applies to any custom mapper — implement `TypeMapper` (and optionally `TypedTypeMapper` if you can deserialise directly into a target type, so handlers can call `request.asPojo(MyDto.class)`).
141163

142164
If neither Gson is on the classpath nor any `application/json` mapper is registered, `build()` throws `IllegalStateException`.

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@
5656
<version>2.6</version>
5757
<optional>true</optional>
5858
</dependency>
59+
<dependency>
60+
<groupId>tools.jackson.core</groupId>
61+
<artifactId>jackson-databind</artifactId>
62+
<version>3.1.3</version>
63+
<optional>true</optional>
64+
</dependency>
5965
<dependency>
6066
<groupId>org.slf4j</groupId>
6167
<artifactId>slf4j-api</artifactId>

src/main/java/com/retailsvc/http/JacksonJsonTypeMapper.java renamed to src/main/java/com/retailsvc/http/Jackson2JsonTypeMapper.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* <pre>{@code
1919
* OpenApiServer.builder()
2020
* .spec(spec)
21-
* .bodyMapper("application/json", new JacksonJsonTypeMapper(myObjectMapper))
21+
* .bodyMapper("application/json", new Jackson2JsonTypeMapper(myObjectMapper))
2222
* .handlers(handlers)
2323
* .build();
2424
* }</pre>
@@ -27,11 +27,11 @@
2727
* must declare {@code jackson-databind} themselves. Consumers that use Gson can rely on the
2828
* built-in {@code GsonJsonMapper} auto-fallback instead.
2929
*/
30-
public final class JacksonJsonTypeMapper implements TypedTypeMapper {
30+
public final class Jackson2JsonTypeMapper implements TypedTypeMapper {
3131

3232
private final ObjectMapper mapper;
3333

34-
public JacksonJsonTypeMapper(ObjectMapper mapper) {
34+
public Jackson2JsonTypeMapper(ObjectMapper mapper) {
3535
this.mapper = Objects.requireNonNull(mapper, "mapper must not be null");
3636
}
3737

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.retailsvc.http;
2+
3+
import java.util.Objects;
4+
import tools.jackson.databind.ObjectMapper;
5+
6+
/**
7+
* {@link TypeMapper} for {@code application/json} backed by Jackson 3. The caller supplies a
8+
* fully-configured {@link ObjectMapper}; this class never adds modules or changes settings — the
9+
* mapper you pass is the mapper you get.
10+
*
11+
* <p>Implements {@link TypedTypeMapper}, so handlers can ask for a typed view of the body via
12+
* {@link Request#asPojo(Class)}.
13+
*
14+
* <p>Use this adapter for Jackson 3.x (group {@code tools.jackson.core}). For Jackson 2.x (group
15+
* {@code com.fasterxml.jackson.core}), use {@link Jackson2JsonTypeMapper} instead — the two majors
16+
* use disjoint package roots and can coexist on the same classpath.
17+
*
18+
* <p>Jackson is an <em>optional</em> Maven dependency of this library; consumers that use Jackson
19+
* must declare {@code tools.jackson.core:jackson-databind} themselves. Consumers that use Gson can
20+
* rely on the built-in {@code GsonJsonMapper} auto-fallback instead.
21+
*
22+
* <p>Typical wiring:
23+
*
24+
* <pre>{@code
25+
* OpenApiServer.builder()
26+
* .spec(spec)
27+
* .bodyMapper("application/json", new Jackson3JsonTypeMapper(myObjectMapper))
28+
* .handlers(handlers)
29+
* .build();
30+
* }</pre>
31+
*
32+
* <p>Jackson 3 made all I/O exceptions unchecked ({@code tools.jackson.core.JacksonException
33+
* extends RuntimeException}), so this adapter no longer needs to wrap them — read/write failures
34+
* propagate as-is to the caller.
35+
*/
36+
public final class Jackson3JsonTypeMapper implements TypedTypeMapper {
37+
38+
private final ObjectMapper mapper;
39+
40+
public Jackson3JsonTypeMapper(ObjectMapper mapper) {
41+
this.mapper = Objects.requireNonNull(mapper, "mapper must not be null");
42+
}
43+
44+
@Override
45+
public Object readFrom(byte[] body, String contentTypeHeader) {
46+
return readAs(body, contentTypeHeader, Object.class);
47+
}
48+
49+
@Override
50+
public <T> T readAs(byte[] body, String contentTypeHeader, Class<T> type) {
51+
return mapper.readValue(body, type);
52+
}
53+
54+
@Override
55+
public byte[] writeTo(Object value) {
56+
return mapper.writeValueAsBytes(value);
57+
}
58+
}

src/test/java/com/retailsvc/http/JacksonJsonTypeMapperTest.java renamed to src/test/java/com/retailsvc/http/Jackson2JsonTypeMapperTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
import java.util.Map;
1111
import org.junit.jupiter.api.Test;
1212

13-
class JacksonJsonTypeMapperTest {
13+
class Jackson2JsonTypeMapperTest {
1414

15-
private final JacksonJsonTypeMapper mapper = new JacksonJsonTypeMapper(new ObjectMapper());
15+
private final Jackson2JsonTypeMapper mapper = new Jackson2JsonTypeMapper(new ObjectMapper());
1616

1717
@Test
1818
void readsJsonObjectAsMap() {
@@ -43,7 +43,7 @@ void wrapsReadFailureAsUncheckedIOException() {
4343

4444
@Test
4545
void rejectsNullObjectMapper() {
46-
assertThatThrownBy(() -> new JacksonJsonTypeMapper(null))
46+
assertThatThrownBy(() -> new Jackson2JsonTypeMapper(null))
4747
.isInstanceOf(NullPointerException.class)
4848
.hasMessageContaining("mapper");
4949
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.retailsvc.http;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
6+
import java.nio.charset.StandardCharsets;
7+
import java.util.List;
8+
import java.util.Map;
9+
import org.junit.jupiter.api.Test;
10+
import tools.jackson.core.JacksonException;
11+
import tools.jackson.databind.ObjectMapper;
12+
13+
class Jackson3JsonTypeMapperTest {
14+
15+
private final Jackson3JsonTypeMapper mapper = new Jackson3JsonTypeMapper(new ObjectMapper());
16+
17+
@Test
18+
void readsJsonObjectAsMap() {
19+
byte[] body = "{\"n\":42,\"s\":\"hi\",\"a\":[1,2]}".getBytes(StandardCharsets.UTF_8);
20+
21+
Object parsed = mapper.readFrom(body, "application/json");
22+
23+
assertThat(parsed).isInstanceOf(Map.class);
24+
@SuppressWarnings("unchecked")
25+
Map<String, Object> m = (Map<String, Object>) parsed;
26+
assertThat(m).containsEntry("n", 42).containsEntry("s", "hi").containsEntry("a", List.of(1, 2));
27+
}
28+
29+
@Test
30+
void writesMapAsJson() {
31+
byte[] out = mapper.writeTo(Map.of("k", "v"));
32+
33+
assertThat(new String(out, StandardCharsets.UTF_8)).isEqualTo("{\"k\":\"v\"}");
34+
}
35+
36+
@Test
37+
void readFailurePropagatesAsJacksonException() {
38+
byte[] malformed = "not json".getBytes(StandardCharsets.UTF_8);
39+
40+
assertThatThrownBy(() -> mapper.readFrom(malformed, "application/json"))
41+
.isInstanceOf(JacksonException.class);
42+
}
43+
44+
@Test
45+
void rejectsNullObjectMapper() {
46+
assertThatThrownBy(() -> new Jackson3JsonTypeMapper(null))
47+
.isInstanceOf(NullPointerException.class)
48+
.hasMessageContaining("mapper");
49+
}
50+
}

src/test/java/com/retailsvc/http/RequestTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ void readsBoundContext() throws Exception {
6060

6161
@Test
6262
void asPojoDeserialisesViaTypedMapper() {
63-
JacksonJsonTypeMapper mapper = new JacksonJsonTypeMapper(new ObjectMapper());
63+
Jackson2JsonTypeMapper mapper = new Jackson2JsonTypeMapper(new ObjectMapper());
6464
byte[] body = "{\"id\":\"x-1\",\"qty\":7}".getBytes(StandardCharsets.UTF_8);
6565
Request req =
6666
new Request(

0 commit comments

Comments
 (0)