Skip to content

Commit 29e97b8

Browse files
authored
GH-10058: Add Jackson 3 ObjectMapper and MessageParser
* GH-10058: Add Jackson 3 ObjectMapper and MessageParser Related to: #10058 * Add Jackson3JsonObjectMapper, Jackson3JsonMessageParser to prepare for Jackson 2 to 3 migration. * 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()` * Add try-catch for valueToTree() call * Remove conditional module checks in tests * Update jsonAvailable() to check Jackson 3 Signed-off-by: Jooyoung Pyoung <[email protected]>
1 parent a1f4df8 commit 29e97b8

File tree

5 files changed

+593
-1
lines changed

5 files changed

+593
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.util.LinkedHashMap;
20+
import java.util.Map;
21+
22+
import org.jspecify.annotations.Nullable;
23+
import tools.jackson.core.JacksonException;
24+
import tools.jackson.core.JsonParser;
25+
import tools.jackson.core.JsonToken;
26+
import tools.jackson.core.ObjectReadContext;
27+
import tools.jackson.core.json.JsonFactory;
28+
29+
import org.springframework.messaging.Message;
30+
import org.springframework.util.Assert;
31+
32+
/**
33+
* {@link JsonInboundMessageMapper.JsonMessageParser} implementation that parses JSON messages
34+
* and builds a {@link Message} with the specified payload type from provided {@link JsonInboundMessageMapper}.
35+
* Uses <a href="https://github.com/FasterXML">Jackson JSON Processor</a>.
36+
*
37+
* @author Jooyoung Pyoung
38+
*
39+
* @since 7.0
40+
*/
41+
public class JacksonJsonMessageParser extends AbstractJacksonJsonMessageParser<JsonParser> {
42+
43+
private static final JsonFactory JSON_FACTORY = JsonFactory.builder().build();
44+
45+
public JacksonJsonMessageParser() {
46+
this(new JacksonJsonObjectMapper());
47+
}
48+
49+
public JacksonJsonMessageParser(JacksonJsonObjectMapper objectMapper) {
50+
super(objectMapper);
51+
}
52+
53+
@Override
54+
protected JsonParser createJsonParser(String jsonMessage) {
55+
return JSON_FACTORY.createParser(ObjectReadContext.empty(), jsonMessage);
56+
}
57+
58+
@Override
59+
protected Message<?> parseWithHeaders(JsonParser parser, String jsonMessage,
60+
@Nullable Map<String, Object> headersToAdd) {
61+
62+
String error = AbstractJsonInboundMessageMapper.MESSAGE_FORMAT_ERROR + jsonMessage;
63+
Assert.isTrue(JsonToken.START_OBJECT == parser.nextToken(), error);
64+
Map<String, Object> headers = null;
65+
Object payload = null;
66+
while (JsonToken.END_OBJECT != parser.nextToken()) {
67+
Assert.isTrue(JsonToken.PROPERTY_NAME == parser.currentToken(), error);
68+
String currentName = parser.currentName();
69+
boolean isHeadersToken = "headers".equals(currentName);
70+
boolean isPayloadToken = "payload".equals(currentName);
71+
Assert.isTrue(isHeadersToken || isPayloadToken, error);
72+
if (isHeadersToken) {
73+
Assert.isTrue(parser.nextToken() == JsonToken.START_OBJECT, error);
74+
headers = readHeaders(parser, jsonMessage);
75+
}
76+
else {
77+
parser.nextToken();
78+
payload = readPayload(parser, jsonMessage);
79+
}
80+
}
81+
Assert.notNull(headers, error);
82+
83+
return getMessageBuilderFactory()
84+
.withPayload(payload)
85+
.copyHeaders(headers)
86+
.copyHeadersIfAbsent(headersToAdd)
87+
.build();
88+
}
89+
90+
private Map<String, Object> readHeaders(JsonParser parser, String jsonMessage) throws JacksonException {
91+
Map<String, Object> headers = new LinkedHashMap<>();
92+
while (JsonToken.END_OBJECT != parser.nextToken()) {
93+
String headerName = parser.currentName();
94+
parser.nextToken();
95+
Object headerValue = readHeader(parser, headerName, jsonMessage);
96+
headers.put(headerName, headerValue);
97+
}
98+
return headers;
99+
}
100+
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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.File;
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.io.Reader;
23+
import java.io.Writer;
24+
import java.lang.reflect.Type;
25+
import java.net.URL;
26+
import java.util.Collection;
27+
import java.util.Map;
28+
29+
import tools.jackson.core.JacksonException;
30+
import tools.jackson.core.JsonParser;
31+
import tools.jackson.databind.JavaType;
32+
import tools.jackson.databind.JsonNode;
33+
import tools.jackson.databind.ObjectMapper;
34+
import tools.jackson.databind.json.JsonMapper;
35+
36+
import org.springframework.integration.mapping.support.JsonHeaders;
37+
import org.springframework.util.Assert;
38+
39+
/**
40+
* Jackson 3 JSON-processor (@link https://github.com/FasterXML)
41+
* {@linkplain JsonObjectMapper} implementation.
42+
* Delegates {@link #toJson} and {@link #fromJson}
43+
* to the {@linkplain ObjectMapper}
44+
* <p>
45+
* It customizes Jackson's default properties with the following ones:
46+
* <ul>
47+
* <li>The well-known modules are registered through the classpath scan</li>
48+
* </ul>
49+
*
50+
* See {@code tools.jackson.databind.json.JsonMapper.builder} for more information.
51+
*
52+
* @author Jooyoung Pyoung
53+
*
54+
* @since 7.0
55+
*
56+
*/
57+
public class JacksonJsonObjectMapper extends AbstractJacksonJsonObjectMapper<JsonNode, JsonParser, JavaType> {
58+
59+
private final ObjectMapper objectMapper;
60+
61+
public JacksonJsonObjectMapper() {
62+
this.objectMapper = JsonMapper.builder()
63+
.findAndAddModules(JacksonJsonObjectMapper.class.getClassLoader())
64+
.build();
65+
}
66+
67+
public JacksonJsonObjectMapper(ObjectMapper objectMapper) {
68+
Assert.notNull(objectMapper, "objectMapper must not be null");
69+
this.objectMapper = objectMapper;
70+
}
71+
72+
public ObjectMapper getObjectMapper() {
73+
return this.objectMapper;
74+
}
75+
76+
@Override
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+
}
84+
}
85+
86+
@Override
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+
}
94+
}
95+
96+
@Override
97+
public JsonNode toJsonNode(Object json) throws IOException {
98+
try {
99+
if (json instanceof String) {
100+
return this.objectMapper.readTree((String) json);
101+
}
102+
else if (json instanceof byte[]) {
103+
return this.objectMapper.readTree((byte[]) json);
104+
}
105+
else if (json instanceof File) {
106+
return this.objectMapper.readTree((File) json);
107+
}
108+
else if (json instanceof URL) {
109+
return this.objectMapper.readTree((URL) json);
110+
}
111+
else if (json instanceof InputStream) {
112+
return this.objectMapper.readTree((InputStream) json);
113+
}
114+
else if (json instanceof Reader) {
115+
return this.objectMapper.readTree((Reader) json);
116+
}
117+
}
118+
catch (JacksonException e) {
119+
if (!(json instanceof String) && !(json instanceof byte[])) {
120+
throw new IOException(e);
121+
}
122+
// Otherwise the input might not be valid JSON, fallback to TextNode with ObjectMapper.valueToTree()
123+
}
124+
125+
try {
126+
return this.objectMapper.valueToTree(json);
127+
}
128+
catch (JacksonException e) {
129+
throw new IOException(e);
130+
}
131+
}
132+
133+
@Override
134+
protected <T> T fromJson(Object json, JavaType type) throws IOException {
135+
try {
136+
if (json instanceof String) {
137+
return this.objectMapper.readValue((String) json, type);
138+
}
139+
else if (json instanceof byte[]) {
140+
return this.objectMapper.readValue((byte[]) json, type);
141+
}
142+
else if (json instanceof File) {
143+
return this.objectMapper.readValue((File) json, type);
144+
}
145+
else if (json instanceof URL) {
146+
return this.objectMapper.readValue((URL) json, type);
147+
}
148+
else if (json instanceof InputStream) {
149+
return this.objectMapper.readValue((InputStream) json, type);
150+
}
151+
else if (json instanceof Reader) {
152+
return this.objectMapper.readValue((Reader) json, type);
153+
}
154+
else {
155+
throw new IllegalArgumentException("'json' argument must be an instance of: " + SUPPORTED_JSON_TYPES
156+
+ " , but gotten: " + json.getClass());
157+
}
158+
}
159+
catch (JacksonException e) {
160+
throw new IOException(e);
161+
}
162+
}
163+
164+
@Override
165+
public <T> T fromJson(JsonParser parser, Type valueType) throws IOException {
166+
try {
167+
return this.objectMapper.readValue(parser, constructType(valueType));
168+
}
169+
catch (JacksonException e) {
170+
throw new IOException(e);
171+
}
172+
}
173+
174+
@Override
175+
@SuppressWarnings({"unchecked"})
176+
protected JavaType extractJavaType(Map<String, Object> javaTypes) {
177+
JavaType classType = this.createJavaType(javaTypes, JsonHeaders.TYPE_ID);
178+
if (!classType.isContainerType() || classType.isArrayType()) {
179+
return classType;
180+
}
181+
182+
JavaType contentClassType = this.createJavaType(javaTypes, JsonHeaders.CONTENT_TYPE_ID);
183+
if (classType.getKeyType() == null) {
184+
return this.objectMapper.getTypeFactory()
185+
.constructCollectionType((Class<? extends Collection<?>>) classType.getRawClass(),
186+
contentClassType);
187+
}
188+
189+
JavaType keyClassType = createJavaType(javaTypes, JsonHeaders.KEY_TYPE_ID);
190+
return this.objectMapper.getTypeFactory()
191+
.constructMapType((Class<? extends Map<?, ?>>) classType.getRawClass(), keyClassType, contentClassType);
192+
}
193+
194+
@Override
195+
protected JavaType constructType(Type type) {
196+
return this.objectMapper.constructType(type);
197+
}
198+
199+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* The utility to check if Jackson JSON processor is present in the classpath.
2323
*
2424
* @author Artem Bilan
25+
* @author Jooyoung Pyoung
2526
*
2627
* @since 4.3.10
2728
*/
@@ -31,10 +32,18 @@ public final class JacksonPresent {
3132
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", null) &&
3233
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", null);
3334

35+
private static final boolean JACKSON_3_PRESENT =
36+
ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", null) &&
37+
ClassUtils.isPresent("tools.jackson.core.JsonGenerator", null);
38+
3439
public static boolean isJackson2Present() {
3540
return JACKSON_2_PRESENT;
3641
}
3742

43+
public static boolean isJackson3Present() {
44+
return JACKSON_3_PRESENT;
45+
}
46+
3847
private JacksonPresent() {
3948
}
4049

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
* @author Artem Bilan
2525
* @author Gary Russell
2626
* @author Vikas Prasad
27+
* @author Jooyoung Pyoung
2728
*
2829
* @since 3.0
2930
*
31+
* @see JacksonJsonObjectMapper
3032
* @see Jackson2JsonObjectMapper
3133
*/
3234
public final class JsonObjectMapperProvider {
@@ -43,6 +45,9 @@ private JsonObjectMapperProvider() {
4345
if (JacksonPresent.isJackson2Present()) {
4446
return new Jackson2JsonObjectMapper();
4547
}
48+
else if (JacksonPresent.isJackson3Present()) {
49+
return new JacksonJsonObjectMapper();
50+
}
4651
else {
4752
throw new IllegalStateException("No jackson-databind.jar is present in the classpath.");
4853
}
@@ -54,7 +59,7 @@ private JsonObjectMapperProvider() {
5459
* @since 4.2.7
5560
*/
5661
public static boolean jsonAvailable() {
57-
return JacksonPresent.isJackson2Present();
62+
return JacksonPresent.isJackson3Present() || JacksonPresent.isJackson2Present();
5863
}
5964

6065
}

0 commit comments

Comments
 (0)