Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit aed23a3

Browse files
committedJun 25, 2025·
Rename Jackson3 classes and handle exception
* Rename `Jackson3JsonObjectMapper` to `JacksonJsonObjectMapper` * Rename `Jackson3JsonMessageParser` to `JacksonJsonMessageParser` * Wrap `JacksonException` in `IOException` to maintain API contract * Replace manual module collection with `findAndAddModules()` Signed-off-by: Jooyoung Pyoung <pyoungjy@gmail.com>
1 parent 6926b29 commit aed23a3

File tree

4 files changed

+366
-77
lines changed

4 files changed

+366
-77
lines changed
 
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,26 @@
3838
*
3939
* @since 7.0
4040
*/
41-
public class Jackson3JsonMessageParser extends AbstractJacksonJsonMessageParser<JsonParser> {
41+
public class JacksonJsonMessageParser extends AbstractJacksonJsonMessageParser<JsonParser> {
42+
4243
private static final JsonFactory JSON_FACTORY = JsonFactory.builder().build();
4344

44-
public Jackson3JsonMessageParser() {
45-
this(new Jackson3JsonObjectMapper());
45+
public JacksonJsonMessageParser() {
46+
this(new JacksonJsonObjectMapper());
4647
}
4748

48-
public Jackson3JsonMessageParser(Jackson3JsonObjectMapper objectMapper) {
49+
public JacksonJsonMessageParser(JacksonJsonObjectMapper objectMapper) {
4950
super(objectMapper);
5051
}
5152

5253
@Override
53-
protected JsonParser createJsonParser(String jsonMessage) throws JacksonException {
54+
protected JsonParser createJsonParser(String jsonMessage) {
5455
return JSON_FACTORY.createParser(ObjectReadContext.empty(), jsonMessage);
5556
}
5657

5758
@Override
5859
protected Message<?> parseWithHeaders(JsonParser parser, String jsonMessage,
59-
@Nullable Map<String, Object> headersToAdd) throws JacksonException {
60+
@Nullable Map<String, Object> headersToAdd) {
6061

6162
String error = AbstractJsonInboundMessageMapper.MESSAGE_FORMAT_ERROR + jsonMessage;
6263
Assert.isTrue(JsonToken.START_OBJECT == parser.nextToken(), error);
Lines changed: 54 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,24 @@
1717
package org.springframework.integration.support.json;
1818

1919
import java.io.File;
20+
import java.io.IOException;
2021
import java.io.InputStream;
2122
import java.io.Reader;
2223
import java.io.Writer;
2324
import java.lang.reflect.Type;
2425
import java.net.URL;
25-
import java.util.ArrayList;
2626
import java.util.Collection;
27-
import java.util.List;
2827
import java.util.Map;
2928

3029
import tools.jackson.core.JacksonException;
3130
import tools.jackson.core.JsonParser;
32-
import tools.jackson.databind.JacksonModule;
3331
import tools.jackson.databind.JavaType;
3432
import tools.jackson.databind.JsonNode;
3533
import tools.jackson.databind.ObjectMapper;
3634
import tools.jackson.databind.json.JsonMapper;
3735

3836
import org.springframework.integration.mapping.support.JsonHeaders;
3937
import org.springframework.util.Assert;
40-
import org.springframework.util.ClassUtils;
4138

4239
/**
4340
* Jackson 3 JSON-processor (@link https://github.com/FasterXML)
@@ -57,25 +54,17 @@
5754
* @since 7.0
5855
*
5956
*/
60-
public class Jackson3JsonObjectMapper extends AbstractJacksonJsonObjectMapper<JsonNode, JsonParser, JavaType> {
61-
62-
private static final boolean JODA_MODULE_PRESENT =
63-
ClassUtils.isPresent("tools.jackson.datatype.joda.JodaModule", null);
64-
65-
private static final boolean KOTLIN_MODULE_PRESENT =
66-
ClassUtils.isPresent("kotlin.Unit", null) &&
67-
ClassUtils.isPresent("tools.jackson.module.kotlin.KotlinModule", null);
57+
public class JacksonJsonObjectMapper extends AbstractJacksonJsonObjectMapper<JsonNode, JsonParser, JavaType> {
6858

6959
private final ObjectMapper objectMapper;
7060

71-
public Jackson3JsonObjectMapper() {
72-
List<JacksonModule> jacksonModules = collectWellKnownModulesIfAvailable();
61+
public JacksonJsonObjectMapper() {
7362
this.objectMapper = JsonMapper.builder()
74-
.addModules(jacksonModules)
63+
.findAndAddModules(JacksonJsonObjectMapper.class.getClassLoader())
7564
.build();
7665
}
7766

78-
public Jackson3JsonObjectMapper(ObjectMapper objectMapper) {
67+
public JacksonJsonObjectMapper(ObjectMapper objectMapper) {
7968
Assert.notNull(objectMapper, "objectMapper must not be null");
8069
this.objectMapper = objectMapper;
8170
}
@@ -85,17 +74,27 @@ public ObjectMapper getObjectMapper() {
8574
}
8675

8776
@Override
88-
public String toJson(Object value) throws JacksonException {
89-
return this.objectMapper.writeValueAsString(value);
77+
public String toJson(Object value) throws IOException {
78+
try {
79+
return this.objectMapper.writeValueAsString(value);
80+
}
81+
catch (JacksonException e) {
82+
throw new IOException(e);
83+
}
9084
}
9185

9286
@Override
93-
public void toJson(Object value, Writer writer) throws JacksonException {
94-
this.objectMapper.writeValue(writer, value);
87+
public void toJson(Object value, Writer writer) throws IOException {
88+
try {
89+
this.objectMapper.writeValue(writer, value);
90+
}
91+
catch (JacksonException e) {
92+
throw new IOException(e);
93+
}
9594
}
9695

9796
@Override
98-
public JsonNode toJsonNode(Object json) throws JacksonException {
97+
public JsonNode toJsonNode(Object json) throws IOException {
9998
try {
10099
if (json instanceof String) {
101100
return this.objectMapper.readTree((String) json);
@@ -118,7 +117,7 @@ else if (json instanceof Reader) {
118117
}
119118
catch (JacksonException e) {
120119
if (!(json instanceof String) && !(json instanceof byte[])) {
121-
throw e;
120+
throw new IOException(e);
122121
}
123122
// Otherwise the input might not be valid JSON, fallback to TextNode with ObjectMapper.valueToTree()
124123
}
@@ -127,34 +126,44 @@ else if (json instanceof Reader) {
127126
}
128127

129128
@Override
130-
protected <T> T fromJson(Object json, JavaType type) throws RuntimeException {
131-
if (json instanceof String) {
132-
return this.objectMapper.readValue((String) json, type);
133-
}
134-
else if (json instanceof byte[]) {
135-
return this.objectMapper.readValue((byte[]) json, type);
136-
}
137-
else if (json instanceof File) {
138-
return this.objectMapper.readValue((File) json, type);
139-
}
140-
else if (json instanceof URL) {
141-
return this.objectMapper.readValue((URL) json, type);
142-
}
143-
else if (json instanceof InputStream) {
144-
return this.objectMapper.readValue((InputStream) json, type);
145-
}
146-
else if (json instanceof Reader) {
147-
return this.objectMapper.readValue((Reader) json, type);
129+
protected <T> T fromJson(Object json, JavaType type) throws IOException {
130+
try {
131+
if (json instanceof String) {
132+
return this.objectMapper.readValue((String) json, type);
133+
}
134+
else if (json instanceof byte[]) {
135+
return this.objectMapper.readValue((byte[]) json, type);
136+
}
137+
else if (json instanceof File) {
138+
return this.objectMapper.readValue((File) json, type);
139+
}
140+
else if (json instanceof URL) {
141+
return this.objectMapper.readValue((URL) json, type);
142+
}
143+
else if (json instanceof InputStream) {
144+
return this.objectMapper.readValue((InputStream) json, type);
145+
}
146+
else if (json instanceof Reader) {
147+
return this.objectMapper.readValue((Reader) json, type);
148+
}
149+
else {
150+
throw new IllegalArgumentException("'json' argument must be an instance of: " + SUPPORTED_JSON_TYPES
151+
+ " , but gotten: " + json.getClass());
152+
}
148153
}
149-
else {
150-
throw new IllegalArgumentException("'json' argument must be an instance of: " + SUPPORTED_JSON_TYPES
151-
+ " , but gotten: " + json.getClass());
154+
catch (JacksonException e) {
155+
throw new IOException(e);
152156
}
153157
}
154158

155159
@Override
156-
public <T> T fromJson(JsonParser parser, Type valueType) throws JacksonException {
157-
return this.objectMapper.readValue(parser, constructType(valueType));
160+
public <T> T fromJson(JsonParser parser, Type valueType) throws IOException {
161+
try {
162+
return this.objectMapper.readValue(parser, constructType(valueType));
163+
}
164+
catch (JacksonException e) {
165+
throw new IOException(e);
166+
}
158167
}
159168

160169
@Override
@@ -182,29 +191,4 @@ protected JavaType constructType(Type type) {
182191
return this.objectMapper.constructType(type);
183192
}
184193

185-
private List<JacksonModule> collectWellKnownModulesIfAvailable() {
186-
List<JacksonModule> modules = new ArrayList<>();
187-
if (JODA_MODULE_PRESENT) {
188-
modules.add(JodaModuleProvider.MODULE);
189-
}
190-
if (KOTLIN_MODULE_PRESENT) {
191-
modules.add(KotlinModuleProvider.MODULE);
192-
}
193-
return modules;
194-
}
195-
196-
private static final class JodaModuleProvider {
197-
198-
static final tools.jackson.databind.JacksonModule MODULE =
199-
new tools.jackson.datatype.joda.JodaModule();
200-
201-
}
202-
203-
private static final class KotlinModuleProvider {
204-
205-
static final tools.jackson.databind.JacksonModule MODULE =
206-
new tools.jackson.module.kotlin.KotlinModule.Builder().build();
207-
208-
}
209-
210194
}

‎spring-integration-core/src/main/java/org/springframework/integration/support/json/JsonObjectMapperProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ private JsonObjectMapperProvider() {
4545
return new Jackson2JsonObjectMapper();
4646
}
4747
else if (JacksonPresent.isJackson3Present()) {
48-
return new Jackson3JsonObjectMapper();
48+
return new JacksonJsonObjectMapper();
4949
}
5050
else {
5151
throw new IllegalStateException("No jackson-databind.jar is present in the classpath.");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
/*
2+
* Copyright 2025-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.support.json;
18+
19+
import java.io.ByteArrayInputStream;
20+
import java.io.File;
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.io.Reader;
24+
import java.io.StringReader;
25+
import java.io.StringWriter;
26+
import java.net.URL;
27+
import java.nio.charset.StandardCharsets;
28+
import java.nio.file.Files;
29+
import java.nio.file.Path;
30+
import java.time.LocalDateTime;
31+
import java.time.ZoneId;
32+
import java.time.ZonedDateTime;
33+
import java.util.ArrayList;
34+
import java.util.Arrays;
35+
import java.util.Collection;
36+
import java.util.Date;
37+
import java.util.List;
38+
import java.util.Map;
39+
import java.util.Optional;
40+
import java.util.Set;
41+
import java.util.stream.Collectors;
42+
43+
import org.joda.time.DateTime;
44+
import org.joda.time.DateTimeZone;
45+
import org.junit.jupiter.api.BeforeEach;
46+
import org.junit.jupiter.api.Test;
47+
import org.junit.jupiter.params.ParameterizedTest;
48+
import org.junit.jupiter.params.provider.ValueSource;
49+
import tools.jackson.databind.JacksonModule;
50+
import tools.jackson.databind.JsonNode;
51+
import tools.jackson.databind.ObjectMapper;
52+
import tools.jackson.databind.json.JsonMapper;
53+
import tools.jackson.datatype.joda.JodaModule;
54+
import tools.jackson.module.kotlin.KotlinModule;
55+
56+
import org.springframework.util.ClassUtils;
57+
58+
import static org.assertj.core.api.Assertions.assertThat;
59+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
60+
61+
/**
62+
* @author Jooyoung Pyoung
63+
*
64+
* @since 7.0
65+
*/
66+
class JacksonJsonObjectMapperTest {
67+
68+
private static final boolean JODA_MODULE_PRESENT =
69+
ClassUtils.isPresent("tools.jackson.datatype.joda.JodaModule", null);
70+
71+
private static final boolean KOTLIN_MODULE_PRESENT =
72+
ClassUtils.isPresent("kotlin.Unit", null) &&
73+
ClassUtils.isPresent("tools.jackson.module.kotlin.KotlinModule", null);
74+
75+
private JacksonJsonObjectMapper mapper;
76+
77+
@BeforeEach
78+
void setUp() {
79+
mapper = new JacksonJsonObjectMapper();
80+
}
81+
82+
@Test
83+
void compareAutoDiscoveryVsManualModules() {
84+
ObjectMapper manualMapper = JsonMapper.builder()
85+
.addModules(collectWellKnownModulesIfAvailable())
86+
.build();
87+
88+
Set<String> collectedModuleNames = getModuleNames(collectWellKnownModulesIfAvailable());
89+
90+
Set<String> autoModuleNames = getModuleNames(mapper.getObjectMapper().getRegisteredModules());
91+
assertThat(autoModuleNames).isEqualTo(collectedModuleNames);
92+
93+
Set<String> manualModuleNames = getModuleNames(manualMapper.getRegisteredModules());
94+
assertThat(manualModuleNames).isEqualTo(collectedModuleNames);
95+
}
96+
97+
@Test
98+
void testToJsonNodeWithVariousInputTypes() throws IOException {
99+
String jsonString = "{\"name\":\"test\",\"value\":123}";
100+
JsonNode nodeFromString = mapper.toJsonNode(jsonString);
101+
assertThat(nodeFromString.get("name").asString()).isEqualTo("test");
102+
assertThat(nodeFromString.get("value").asInt()).isEqualTo(123);
103+
104+
byte[] jsonBytes = jsonString.getBytes(StandardCharsets.UTF_8);
105+
JsonNode nodeFromBytes = mapper.toJsonNode(jsonBytes);
106+
assertThat(nodeFromBytes).isEqualTo(nodeFromString);
107+
108+
try (InputStream inputStream = new ByteArrayInputStream(jsonBytes)) {
109+
JsonNode nodeFromInputStream = mapper.toJsonNode(inputStream);
110+
assertThat(nodeFromInputStream).isEqualTo(nodeFromString);
111+
}
112+
113+
try (Reader reader = new StringReader(jsonString)) {
114+
JsonNode nodeFromReader = mapper.toJsonNode(reader);
115+
assertThat(nodeFromReader).isEqualTo(nodeFromString);
116+
}
117+
}
118+
119+
@Test
120+
void testToJsonNodeWithFile() throws IOException {
121+
Path tempFile = Files.createTempFile("test", ".json");
122+
String jsonContent = "{\"message\":\"hello from file\",\"number\":42}";
123+
Files.write(tempFile, jsonContent.getBytes(StandardCharsets.UTF_8));
124+
125+
try {
126+
File file = tempFile.toFile();
127+
JsonNode nodeFromFile = mapper.toJsonNode(file);
128+
assertThat(nodeFromFile.get("message").asString()).isEqualTo("hello from file");
129+
assertThat(nodeFromFile.get("number").asInt()).isEqualTo(42);
130+
131+
URL fileUrl = file.toURI().toURL();
132+
JsonNode nodeFromUrl = mapper.toJsonNode(fileUrl);
133+
assertThat(nodeFromUrl).isEqualTo(nodeFromFile);
134+
}
135+
finally {
136+
Files.deleteIfExists(tempFile);
137+
}
138+
}
139+
140+
@Test
141+
void testToJsonWithWriter() throws IOException {
142+
TestData data = new TestData("John", Optional.of("john@test.com"), Optional.empty());
143+
144+
try (StringWriter writer = new StringWriter()) {
145+
mapper.toJson(data, writer);
146+
String json = writer.toString();
147+
assertThat(json).isEqualTo("{\"name\":\"John\",\"email\":\"john@test.com\",\"age\":null}");
148+
}
149+
}
150+
151+
@Test
152+
void testFromJsonWithUnsupportedType() {
153+
Object unsupportedInput = new Date();
154+
155+
assertThatThrownBy(() -> mapper.fromJson(unsupportedInput, String.class))
156+
.isInstanceOf(IllegalArgumentException.class);
157+
}
158+
159+
@ParameterizedTest
160+
@ValueSource(strings = {"42", "true", "\"hello\"", "3.14", "null"})
161+
void testPrimitiveTypes(String jsonValue) throws IOException {
162+
JsonNode node = mapper.toJsonNode(jsonValue);
163+
assertThat(node).isNotNull();
164+
165+
String serialized = mapper.toJson(node);
166+
JsonNode roundTrip = mapper.toJsonNode(serialized);
167+
assertThat(roundTrip).isEqualTo(node);
168+
}
169+
170+
@Test
171+
void testCollectionTypes() throws IOException {
172+
List<String> stringList = Arrays.asList("a", "b", "c");
173+
String json = mapper.toJson(stringList);
174+
assertThat(json).isEqualTo("[\"a\",\"b\",\"c\"]");
175+
176+
@SuppressWarnings("unchecked")
177+
List<String> deserialized = mapper.fromJson(json, List.class);
178+
assertThat(deserialized).isEqualTo(stringList);
179+
180+
Set<Integer> intSet = Set.of(1, 2, 3);
181+
String setJson = mapper.toJson(intSet);
182+
assertThat(setJson).isNotNull();
183+
}
184+
185+
@Test
186+
void testMapTypes() throws IOException {
187+
Map<String, Object> map = Map.of(
188+
"string", "value",
189+
"number", 42,
190+
"boolean", true,
191+
"nested", Map.of("inner", "value")
192+
);
193+
194+
String json = mapper.toJson(map);
195+
assertThat(json).isNotNull();
196+
197+
@SuppressWarnings("unchecked")
198+
Map<String, Object> deserialized = mapper.fromJson(json, Map.class);
199+
assertThat(deserialized.get("string")).isEqualTo("value");
200+
assertThat(deserialized.get("number")).isEqualTo(42);
201+
assertThat(deserialized.get("boolean")).isEqualTo(true);
202+
}
203+
204+
@Test
205+
public void testOptional() throws IOException {
206+
TestData data = new TestData("John", Optional.of("john@test.com"), Optional.empty());
207+
208+
String json = mapper.toJson(data);
209+
assertThat(json).isEqualTo("{\"name\":\"John\",\"email\":\"john@test.com\",\"age\":null}");
210+
211+
TestData deserialized = mapper.fromJson(json, TestData.class);
212+
assertThat(deserialized).isEqualTo(data);
213+
}
214+
215+
@Test
216+
public void testJavaTime() throws Exception {
217+
LocalDateTime localDateTime = LocalDateTime.of(2000, 1, 1, 0, 0);
218+
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, ZoneId.of("UTC"));
219+
TimeData data = new TimeData(localDateTime, zonedDateTime);
220+
221+
String json = mapper.toJson(data);
222+
assertThat("{\"localDate\":\"2000-01-01T00:00:00\",\"zoneDate\":\"2000-01-01T00:00:00Z\"}").isEqualTo(json);
223+
224+
// 물어보기
225+
TimeData deserialized = mapper.fromJson(json, TimeData.class);
226+
assertThat(deserialized.localDate()).isEqualTo(data.localDate());
227+
assertThat(deserialized.zoneDate().toInstant()).isEqualTo(data.zoneDate().toInstant());
228+
}
229+
230+
@Test
231+
public void testJodaWithJodaModule() throws Exception {
232+
ObjectMapper objectMapper = mapper.getObjectMapper();
233+
Set<String> registeredModules = getModuleNames(objectMapper.getRegisteredModules());
234+
assertThat(registeredModules.contains(JodaModuleProvider.MODULE.getModuleName())).isTrue();
235+
236+
org.joda.time.DateTime jodaDateTime = new DateTime(2000, 1, 1, 0, 0, DateTimeZone.UTC);
237+
JodaData data = new JodaData("John", jodaDateTime);
238+
239+
String json = mapper.toJson(data);
240+
assertThat("{\"name\":\"John\",\"jodaDate\":\"2000-01-01T00:00:00.000Z\"}").isEqualTo(json);
241+
242+
JodaData deserialized = mapper.fromJson(json, JodaData.class);
243+
assertThat(deserialized.name()).isEqualTo(data.name());
244+
assertThat(deserialized.jodaDate()).isEqualTo(data.jodaDate());
245+
}
246+
247+
@Test
248+
public void testJodaWithoutJodaModule() {
249+
ObjectMapper customMapper = JsonMapper.builder().build();
250+
JacksonJsonObjectMapper mapper = new JacksonJsonObjectMapper(customMapper);
251+
252+
Set<String> registeredModules = getModuleNames(mapper.getObjectMapper().getRegisteredModules());
253+
assertThat(registeredModules.contains(JodaModuleProvider.MODULE.getModuleName())).isFalse();
254+
255+
org.joda.time.DateTime jodaDateTime = new DateTime(2000, 1, 1, 0, 0, DateTimeZone.UTC);
256+
JodaData data = new JodaData("John", jodaDateTime);
257+
258+
assertThatThrownBy(() -> mapper.toJson(data))
259+
.isInstanceOf(IOException.class);
260+
261+
String json = "{\"name\":\"John\",\"jodaDate\":\"2000-01-01T00:00:00.000Z\"}";
262+
assertThatThrownBy(() -> mapper.fromJson(json, JodaData.class))
263+
.isInstanceOf(IOException.class);
264+
}
265+
266+
private Set<String> getModuleNames(Collection<JacksonModule> modules) {
267+
return modules.stream()
268+
.map(JacksonModule::getModuleName)
269+
.collect(Collectors.toUnmodifiableSet());
270+
}
271+
272+
private static final class JodaModuleProvider {
273+
274+
private static final JacksonModule MODULE = new JodaModule();
275+
276+
}
277+
278+
private static final class KotlinModuleProvider {
279+
280+
private static final JacksonModule MODULE = new KotlinModule.Builder().build();
281+
282+
}
283+
284+
private List<JacksonModule> collectWellKnownModulesIfAvailable() {
285+
List<JacksonModule> modules = new ArrayList<>();
286+
if (JODA_MODULE_PRESENT) {
287+
modules.add(JodaModuleProvider.MODULE);
288+
}
289+
if (KOTLIN_MODULE_PRESENT) {
290+
modules.add(KotlinModuleProvider.MODULE);
291+
}
292+
return modules;
293+
}
294+
295+
private record TestData(String name, Optional<String> email, Optional<Integer> age) {
296+
}
297+
298+
private record TimeData(LocalDateTime localDate, ZonedDateTime zoneDate) {
299+
}
300+
301+
private record JodaData(String name, org.joda.time.DateTime jodaDate) {
302+
}
303+
304+
}

0 commit comments

Comments
 (0)
Please sign in to comment.