Skip to content

Commit 988f9a1

Browse files
committed
Add support for Jackson 3 and deprecate Jackson 2-based implementations.
1 parent 6749e32 commit 988f9a1

28 files changed

+1247
-107
lines changed

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@
7070
<artifactId>jackson-databind</artifactId>
7171
<optional>true</optional>
7272
</dependency>
73+
<dependency>
74+
<groupId>tools.jackson.core</groupId>
75+
<artifactId>jackson-databind</artifactId>
76+
<optional>true</optional>
77+
</dependency>
7378
<dependency>
7479
<groupId>org.springframework</groupId>
7580
<artifactId>spring-web</artifactId>

src/main/antora/modules/ROOT/pages/repositories/core-extensions-web.adoc

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,9 +290,9 @@ By default, the assembler points to the controller method it was invoked in, but
290290
== Spring Data Jackson Modules
291291

292292
The core module, and some of the store specific ones, ship with a set of Jackson Modules for types, like `org.springframework.data.geo.Distance` and `org.springframework.data.geo.Point`, used by the Spring Data domain. +
293-
Those Modules are imported once xref:repositories/core-extensions.adoc#core.web[web support] is enabled and `com.fasterxml.jackson.databind.ObjectMapper` is available.
293+
Those modules are imported once xref:repositories/core-extensions.adoc#core.web[web support] is enabled and `tools.jackson.databind.ObjectMapper` is available.
294294

295-
During initialization `SpringDataJacksonModules`, like the `SpringDataJacksonConfiguration`, get picked up by the infrastructure, so that the declared ``com.fasterxml.jackson.databind.Module``s are made available to the Jackson `ObjectMapper`.
295+
During initialization `SpringDataJackson3Modules`, like the `SpringDataJackson3Configuration`, get picked up by the infrastructure, so that the declared ``tools.jackson.databind.JacksonModule``s are made available to the Jackson `ObjectMapper`.
296296

297297
Data binding mixins for the following domain types are registered by the common infrastructure.
298298

@@ -306,10 +306,15 @@ org.springframework.data.geo.Polygon
306306

307307
[NOTE]
308308
====
309-
The individual module may provide additional `SpringDataJacksonModules`. +
309+
The individual module may provide additional `SpringDataJackson3Modules`. +
310310
Please refer to the store specific section for more details.
311311
====
312312

313+
[NOTE]
314+
====
315+
Jackson 2 support is deprecated and will be removed in a future release.
316+
====
317+
313318
[[core.web.binding]]
314319
== Web Databinding Support
315320

@@ -341,7 +346,7 @@ Nested projections are supported as described in xref:repositories/projections.a
341346
If the method returns a complex, non-interface type, a Jackson `ObjectMapper` is used to map the final value.
342347

343348
For Spring MVC, the necessary converters are registered automatically as soon as `@EnableSpringDataWebSupport` is active and the required dependencies are available on the classpath.
344-
For usage with `RestTemplate`, register a `ProjectingJackson2HttpMessageConverter` (JSON) or `XmlBeamHttpMessageConverter` manually.
349+
For usage with `RestTemplate`, register a `ProjectingJacksonHttpMessageConverter` (JSON) or `XmlBeamHttpMessageConverter` manually.
345350

346351
For more information, see the https://github.com/spring-projects/spring-data-examples/tree/main/web/projection[web projection example] in the canonical https://github.com/spring-projects/spring-data-examples[Spring Data Examples repository].
347352

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2014-2025 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+
package org.springframework.data.geo;
17+
18+
import tools.jackson.core.Version;
19+
import tools.jackson.databind.annotation.JsonDeserialize;
20+
import tools.jackson.databind.module.SimpleModule;
21+
22+
import java.io.Serial;
23+
import java.util.List;
24+
25+
import com.fasterxml.jackson.annotation.JsonIgnore;
26+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
27+
import com.fasterxml.jackson.annotation.JsonProperty;
28+
29+
/**
30+
* Custom module to deserialize the geo-spatial value objects using Jackson 3.
31+
*
32+
* @author Oliver Gierke
33+
* @author Mark Paluch
34+
* @since 4.0
35+
*/
36+
@SuppressWarnings("unused")
37+
public class GeoJacksonModule extends SimpleModule {
38+
39+
private static final @Serial long serialVersionUID = 1L;
40+
41+
/**
42+
* Creates a new {@link GeoJacksonModule} registering mixins for common geo-spatial types.
43+
*/
44+
public GeoJacksonModule() {
45+
46+
super("Spring Data Geo Mixins", new Version(1, 0, 0, null, "org.springframework.data", "spring-data-commons-geo"));
47+
48+
setMixInAnnotation(Distance.class, DistanceMixin.class);
49+
setMixInAnnotation(Point.class, PointMixin.class);
50+
setMixInAnnotation(Box.class, BoxMixin.class);
51+
setMixInAnnotation(Circle.class, CircleMixin.class);
52+
setMixInAnnotation(Polygon.class, PolygonMixin.class);
53+
}
54+
55+
@JsonIgnoreProperties("unit")
56+
static abstract class DistanceMixin {
57+
58+
DistanceMixin(@JsonProperty("value") double value,
59+
@JsonProperty("metric") @JsonDeserialize(as = Metrics.class) Metric metic) {}
60+
61+
@JsonIgnore
62+
abstract double getNormalizedValue();
63+
}
64+
65+
static abstract class PointMixin {
66+
PointMixin(@JsonProperty("x") double x, @JsonProperty("y") double y) {}
67+
}
68+
69+
static abstract class CircleMixin {
70+
CircleMixin(@JsonProperty("center") Point center, @JsonProperty("radius") Distance radius) {}
71+
}
72+
73+
static abstract class BoxMixin {
74+
BoxMixin(@JsonProperty("first") Point first, @JsonProperty("second") Point point) {}
75+
}
76+
77+
static abstract class PolygonMixin {
78+
PolygonMixin(@JsonProperty("points") List<Point> points) {}
79+
}
80+
}

src/main/java/org/springframework/data/geo/GeoModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@
3030
*
3131
* @author Oliver Gierke
3232
* @since 1.8
33+
* @deprecated since 4.0, use {@link GeoJacksonModule} instead.
3334
*/
3435
@SuppressWarnings("unused")
36+
@Deprecated(since = "4.0", forRemoval = true)
3537
public class GeoModule extends SimpleModule {
3638

3739
private static final @Serial long serialVersionUID = 1L;

src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
* @author Oliver Gierke
2828
* @author Christoph Strobl
2929
* @since 1.6
30+
* @deprecated since 4.0, in favor of {@link JacksonRepositoryPopulatorFactoryBean}.
3031
*/
32+
@Deprecated(since = "4.0", forRemoval = true)
3133
public class Jackson2RepositoryPopulatorFactoryBean extends AbstractRepositoryPopulatorFactoryBean {
3234

3335
private @Nullable ObjectMapper mapper;

src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@
4040
* @author Mark Paluch
4141
* @author Johannes Englmeier
4242
* @since 1.6
43+
* @deprecated since 4.0, in favor of {@link JacksonResourceReader}.
4344
*/
45+
@Deprecated(since = "4.0", forRemoval = true)
4446
public class Jackson2ResourceReader implements ResourceReader {
4547

4648
private static final String DEFAULT_TYPE_KEY = "_class";
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2025 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+
package org.springframework.data.repository.init;
17+
18+
import tools.jackson.databind.ObjectMapper;
19+
20+
import org.jspecify.annotations.Nullable;
21+
22+
import org.springframework.beans.factory.FactoryBean;
23+
24+
/**
25+
* {@link FactoryBean} to set up a {@link ResourceReaderRepositoryPopulator} with a {@link JacksonResourceReader}.
26+
*
27+
* @author Mark Paluch
28+
* @author Oliver Gierke
29+
* @author Christoph Strobl
30+
* @since 4.0
31+
*/
32+
public class JacksonRepositoryPopulatorFactoryBean extends AbstractRepositoryPopulatorFactoryBean {
33+
34+
private @Nullable ObjectMapper mapper;
35+
36+
/**
37+
* Configures the {@link ObjectMapper} to be used.
38+
*
39+
* @param mapper can be {@literal null}.
40+
*/
41+
public void setMapper(@Nullable ObjectMapper mapper) {
42+
this.mapper = mapper;
43+
}
44+
45+
@Override
46+
protected ResourceReader getResourceReader() {
47+
return mapper == null ? new JacksonResourceReader() : new JacksonResourceReader(mapper);
48+
}
49+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2013-2025 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+
package org.springframework.data.repository.init;
17+
18+
import tools.jackson.databind.DeserializationFeature;
19+
import tools.jackson.databind.JsonNode;
20+
import tools.jackson.databind.ObjectMapper;
21+
import tools.jackson.databind.json.JsonMapper;
22+
23+
import java.io.IOException;
24+
import java.io.InputStream;
25+
import java.util.ArrayList;
26+
import java.util.Iterator;
27+
import java.util.List;
28+
29+
import org.jspecify.annotations.Nullable;
30+
31+
import org.springframework.core.io.Resource;
32+
import org.springframework.util.Assert;
33+
import org.springframework.util.ClassUtils;
34+
35+
/**
36+
* A {@link ResourceReader} using Jackson to read JSON into objects.
37+
*
38+
* @author Oliver Gierke
39+
* @author Christoph Strobl
40+
* @author Mark Paluch
41+
* @author Johannes Englmeier
42+
* @since 4.0
43+
*/
44+
public class JacksonResourceReader implements ResourceReader {
45+
46+
private static final String DEFAULT_TYPE_KEY = "_class";
47+
private static final ObjectMapper DEFAULT_MAPPER = JsonMapper.builder()
48+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build();
49+
50+
private final ObjectMapper mapper;
51+
private String typeKey = DEFAULT_TYPE_KEY;
52+
53+
/**
54+
* Creates a new {@link JacksonResourceReader}.
55+
*/
56+
public JacksonResourceReader() {
57+
this(DEFAULT_MAPPER);
58+
}
59+
60+
/**
61+
* Creates a new {@link JacksonResourceReader} using the given {@link ObjectMapper}.
62+
*
63+
* @param mapper
64+
*/
65+
public JacksonResourceReader(ObjectMapper mapper) {
66+
this.mapper = mapper;
67+
}
68+
69+
/**
70+
* Configures the JSON document's key to look up the type to instantiate the object. Defaults to
71+
* {@link JacksonResourceReader#DEFAULT_TYPE_KEY}.
72+
*
73+
* @param typeKey
74+
*/
75+
public void setTypeKey(@Nullable String typeKey) {
76+
this.typeKey = typeKey == null ? DEFAULT_TYPE_KEY : typeKey;
77+
}
78+
79+
@Override
80+
public Object readFrom(Resource resource, @Nullable ClassLoader classLoader) throws Exception {
81+
82+
Assert.notNull(resource, "Resource must not be null");
83+
84+
InputStream stream = resource.getInputStream();
85+
JsonNode node = mapper.readerFor(JsonNode.class).readTree(stream);
86+
87+
if (node.isArray()) {
88+
89+
Iterator<JsonNode> elements = node.iterator();
90+
List<Object> result = new ArrayList<>();
91+
92+
while (elements.hasNext()) {
93+
JsonNode element = elements.next();
94+
result.add(readSingle(element, classLoader));
95+
}
96+
97+
return result;
98+
}
99+
100+
return readSingle(node, classLoader);
101+
}
102+
103+
/**
104+
* Reads the given {@link JsonNode} into an instance of the type encoded in it using the configured type key.
105+
*
106+
* @param node must not be {@literal null}.
107+
* @param classLoader can be {@literal null}.
108+
* @return
109+
*/
110+
private Object readSingle(JsonNode node, @Nullable ClassLoader classLoader) throws IOException {
111+
112+
JsonNode typeNode = node.findValue(typeKey);
113+
114+
if (typeNode == null) {
115+
throw new IllegalArgumentException(String.format("Could not find type for type key '%s'", typeKey));
116+
}
117+
118+
String typeName = typeNode.asString();
119+
Class<?> type = ClassUtils.resolveClassName(typeName, classLoader);
120+
121+
return mapper.readerFor(type).readValue(node);
122+
}
123+
}

src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,27 @@ private static boolean hasJsonPathAnnotation(Class<?> type) {
131131
return false;
132132
}
133133

134-
private static class InputMessageProjecting implements MethodInterceptor {
135-
136-
private final DocumentContext context;
137-
138-
public InputMessageProjecting(DocumentContext context) {
139-
this.context = context;
140-
}
134+
private record InputMessageProjecting(DocumentContext context) implements MethodInterceptor {
141135

142136
@Override
143137
public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
144138

145139
Method method = invocation.getMethod();
140+
141+
switch (method.getName()) {
142+
case "equals" -> {
143+
// Only consider equal when proxies are identical.
144+
return (invocation.getThis() == invocation.getArguments()[0]);
145+
}
146+
case "hashCode" -> {
147+
// Use hashCode of EntityManager proxy.
148+
return context.hashCode();
149+
}
150+
case "toString" -> {
151+
return context.jsonString();
152+
}
153+
}
154+
146155
TypeInformation<?> returnType = TypeInformation.fromReturnTypeOf(method);
147156
ResolvableType type = ResolvableType.forMethodReturnType(method);
148157
boolean isCollectionResult = type.getRawClass() != null && Collection.class.isAssignableFrom(type.getRawClass());

src/main/java/org/springframework/data/web/ProjectingJackson2HttpMessageConverter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@
4848
* @author Christoph Strobl
4949
* @soundtrack Richard Spaven - Ice Is Nice (Spaven's 5ive)
5050
* @since 1.13
51+
* @deprecated since 4.0, in favor of {@link ProjectingJacksonHttpMessageConverter}.
5152
*/
53+
@Deprecated(since = "4.0", forRemoval = true)
5254
public class ProjectingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter
5355
implements BeanClassLoaderAware, BeanFactoryAware {
5456

0 commit comments

Comments
 (0)