Skip to content

Commit 6926b29

Browse files
committed
GH-10058: Add Jackson 3 ObjectMapper and MessageParser
Related to: #10058 * Add Jackson3JsonObjectMapper, Jackson3JsonMessageParser to prepare for Jackson 2 to 3 migration. Signed-off-by: Jooyoung Pyoung <[email protected]>
1 parent c4c9627 commit 6926b29

File tree

4 files changed

+323
-0
lines changed

4 files changed

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

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* @author Artem Bilan
2525
* @author Gary Russell
2626
* @author Vikas Prasad
27+
* @author Jooyoung Pyoung
2728
*
2829
* @since 3.0
2930
*
@@ -43,6 +44,9 @@ private JsonObjectMapperProvider() {
4344
if (JacksonPresent.isJackson2Present()) {
4445
return new Jackson2JsonObjectMapper();
4546
}
47+
else if (JacksonPresent.isJackson3Present()) {
48+
return new Jackson3JsonObjectMapper();
49+
}
4650
else {
4751
throw new IllegalStateException("No jackson-databind.jar is present in the classpath.");
4852
}

0 commit comments

Comments
 (0)