diff --git a/pom.xml b/pom.xml
index a6dc167a03..d7ee1acbfc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-commons
- 4.0.0-SNAPSHOT
+ 4.0.0-GH-3292-SNAPSHOT
Spring Data Core
Core Spring concepts underpinning every Spring Data module.
@@ -70,6 +70,11 @@
jackson-databind
true
+
+ tools.jackson.core
+ jackson-databind
+ true
+
org.springframework
spring-web
diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-extensions-web.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-extensions-web.adoc
index 1ced7779f5..7be6cf1c04 100644
--- a/src/main/antora/modules/ROOT/pages/repositories/core-extensions-web.adoc
+++ b/src/main/antora/modules/ROOT/pages/repositories/core-extensions-web.adoc
@@ -290,9 +290,9 @@ By default, the assembler points to the controller method it was invoked in, but
== Spring Data Jackson Modules
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. +
-Those Modules are imported once xref:repositories/core-extensions.adoc#core.web[web support] is enabled and `com.fasterxml.jackson.databind.ObjectMapper` is available.
+Those modules are imported once xref:repositories/core-extensions.adoc#core.web[web support] is enabled and `tools.jackson.databind.ObjectMapper` is available.
-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`.
+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`.
Data binding mixins for the following domain types are registered by the common infrastructure.
@@ -306,10 +306,15 @@ org.springframework.data.geo.Polygon
[NOTE]
====
-The individual module may provide additional `SpringDataJacksonModules`. +
+The individual module may provide additional `SpringDataJackson3Modules`. +
Please refer to the store specific section for more details.
====
+[NOTE]
+====
+Jackson 2 support is deprecated and will be removed in a future release.
+====
+
[[core.web.binding]]
== Web Databinding Support
@@ -341,7 +346,7 @@ Nested projections are supported as described in xref:repositories/projections.a
If the method returns a complex, non-interface type, a Jackson `ObjectMapper` is used to map the final value.
For Spring MVC, the necessary converters are registered automatically as soon as `@EnableSpringDataWebSupport` is active and the required dependencies are available on the classpath.
-For usage with `RestTemplate`, register a `ProjectingJackson2HttpMessageConverter` (JSON) or `XmlBeamHttpMessageConverter` manually.
+For usage with `RestTemplate`, register a `ProjectingJacksonHttpMessageConverter` (JSON) or `XmlBeamHttpMessageConverter` manually.
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].
diff --git a/src/main/java/org/springframework/data/geo/GeoJacksonModule.java b/src/main/java/org/springframework/data/geo/GeoJacksonModule.java
new file mode 100644
index 0000000000..f13703109f
--- /dev/null
+++ b/src/main/java/org/springframework/data/geo/GeoJacksonModule.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2014-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.geo;
+
+import tools.jackson.core.Version;
+import tools.jackson.databind.annotation.JsonDeserialize;
+import tools.jackson.databind.module.SimpleModule;
+
+import java.io.Serial;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Custom module to deserialize the geo-spatial value objects using Jackson 3.
+ *
+ * @author Oliver Gierke
+ * @author Mark Paluch
+ * @since 4.0
+ */
+@SuppressWarnings("unused")
+public class GeoJacksonModule extends SimpleModule {
+
+ private static final @Serial long serialVersionUID = 1L;
+
+ /**
+ * Creates a new {@link GeoJacksonModule} registering mixins for common geo-spatial types.
+ */
+ public GeoJacksonModule() {
+
+ super("Spring Data Geo Mixins", new Version(1, 0, 0, null, "org.springframework.data", "spring-data-commons-geo"));
+
+ setMixInAnnotation(Distance.class, DistanceMixin.class);
+ setMixInAnnotation(Point.class, PointMixin.class);
+ setMixInAnnotation(Box.class, BoxMixin.class);
+ setMixInAnnotation(Circle.class, CircleMixin.class);
+ setMixInAnnotation(Polygon.class, PolygonMixin.class);
+ }
+
+ @JsonIgnoreProperties("unit")
+ static abstract class DistanceMixin {
+
+ DistanceMixin(@JsonProperty("value") double value,
+ @JsonProperty("metric") @JsonDeserialize(as = Metrics.class) Metric metic) {}
+
+ @JsonIgnore
+ abstract double getNormalizedValue();
+ }
+
+ static abstract class PointMixin {
+ PointMixin(@JsonProperty("x") double x, @JsonProperty("y") double y) {}
+ }
+
+ static abstract class CircleMixin {
+ CircleMixin(@JsonProperty("center") Point center, @JsonProperty("radius") Distance radius) {}
+ }
+
+ static abstract class BoxMixin {
+ BoxMixin(@JsonProperty("first") Point first, @JsonProperty("second") Point point) {}
+ }
+
+ static abstract class PolygonMixin {
+ PolygonMixin(@JsonProperty("points") List points) {}
+ }
+}
diff --git a/src/main/java/org/springframework/data/geo/GeoModule.java b/src/main/java/org/springframework/data/geo/GeoModule.java
index e60639b2ec..17f9b065a6 100644
--- a/src/main/java/org/springframework/data/geo/GeoModule.java
+++ b/src/main/java/org/springframework/data/geo/GeoModule.java
@@ -30,8 +30,10 @@
*
* @author Oliver Gierke
* @since 1.8
+ * @deprecated since 4.0, use {@link GeoJacksonModule} instead.
*/
@SuppressWarnings("unused")
+@Deprecated(since = "4.0", forRemoval = true)
public class GeoModule extends SimpleModule {
private static final @Serial long serialVersionUID = 1L;
diff --git a/src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java b/src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java
index 75b08c3145..7ce69ecab3 100644
--- a/src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java
+++ b/src/main/java/org/springframework/data/repository/init/Jackson2RepositoryPopulatorFactoryBean.java
@@ -27,7 +27,9 @@
* @author Oliver Gierke
* @author Christoph Strobl
* @since 1.6
+ * @deprecated since 4.0, in favor of {@link JacksonRepositoryPopulatorFactoryBean}.
*/
+@Deprecated(since = "4.0", forRemoval = true)
public class Jackson2RepositoryPopulatorFactoryBean extends AbstractRepositoryPopulatorFactoryBean {
private @Nullable ObjectMapper mapper;
diff --git a/src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java b/src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java
index 783fa9ec7d..4514fe869d 100644
--- a/src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java
+++ b/src/main/java/org/springframework/data/repository/init/Jackson2ResourceReader.java
@@ -40,7 +40,9 @@
* @author Mark Paluch
* @author Johannes Englmeier
* @since 1.6
+ * @deprecated since 4.0, in favor of {@link JacksonResourceReader}.
*/
+@Deprecated(since = "4.0", forRemoval = true)
public class Jackson2ResourceReader implements ResourceReader {
private static final String DEFAULT_TYPE_KEY = "_class";
diff --git a/src/main/java/org/springframework/data/repository/init/JacksonRepositoryPopulatorFactoryBean.java b/src/main/java/org/springframework/data/repository/init/JacksonRepositoryPopulatorFactoryBean.java
new file mode 100644
index 0000000000..f6ea4a9128
--- /dev/null
+++ b/src/main/java/org/springframework/data/repository/init/JacksonRepositoryPopulatorFactoryBean.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.repository.init;
+
+import tools.jackson.databind.ObjectMapper;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.beans.factory.FactoryBean;
+
+/**
+ * {@link FactoryBean} to set up a {@link ResourceReaderRepositoryPopulator} with a {@link JacksonResourceReader}.
+ *
+ * @author Mark Paluch
+ * @author Oliver Gierke
+ * @author Christoph Strobl
+ * @since 4.0
+ */
+public class JacksonRepositoryPopulatorFactoryBean extends AbstractRepositoryPopulatorFactoryBean {
+
+ private @Nullable ObjectMapper mapper;
+
+ /**
+ * Configures the {@link ObjectMapper} to be used.
+ *
+ * @param mapper can be {@literal null}.
+ */
+ public void setMapper(@Nullable ObjectMapper mapper) {
+ this.mapper = mapper;
+ }
+
+ @Override
+ protected ResourceReader getResourceReader() {
+ return mapper == null ? new JacksonResourceReader() : new JacksonResourceReader(mapper);
+ }
+}
diff --git a/src/main/java/org/springframework/data/repository/init/JacksonResourceReader.java b/src/main/java/org/springframework/data/repository/init/JacksonResourceReader.java
new file mode 100644
index 0000000000..483db7f6b8
--- /dev/null
+++ b/src/main/java/org/springframework/data/repository/init/JacksonResourceReader.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2013-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.repository.init;
+
+import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.core.io.Resource;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+/**
+ * A {@link ResourceReader} using Jackson to read JSON into objects.
+ *
+ * @author Oliver Gierke
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @author Johannes Englmeier
+ * @since 4.0
+ */
+public class JacksonResourceReader implements ResourceReader {
+
+ private static final String DEFAULT_TYPE_KEY = "_class";
+ private static final ObjectMapper DEFAULT_MAPPER = JsonMapper.builder()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build();
+
+ private final ObjectMapper mapper;
+ private String typeKey = DEFAULT_TYPE_KEY;
+
+ /**
+ * Creates a new {@link JacksonResourceReader}.
+ */
+ public JacksonResourceReader() {
+ this(DEFAULT_MAPPER);
+ }
+
+ /**
+ * Creates a new {@link JacksonResourceReader} using the given {@link ObjectMapper}.
+ *
+ * @param mapper
+ */
+ public JacksonResourceReader(ObjectMapper mapper) {
+ this.mapper = mapper;
+ }
+
+ /**
+ * Configures the JSON document's key to look up the type to instantiate the object. Defaults to
+ * {@link JacksonResourceReader#DEFAULT_TYPE_KEY}.
+ *
+ * @param typeKey
+ */
+ public void setTypeKey(@Nullable String typeKey) {
+ this.typeKey = typeKey == null ? DEFAULT_TYPE_KEY : typeKey;
+ }
+
+ @Override
+ public Object readFrom(Resource resource, @Nullable ClassLoader classLoader) throws Exception {
+
+ Assert.notNull(resource, "Resource must not be null");
+
+ InputStream stream = resource.getInputStream();
+ JsonNode node = mapper.readerFor(JsonNode.class).readTree(stream);
+
+ if (node.isArray()) {
+
+ Iterator elements = node.iterator();
+ List result = new ArrayList<>();
+
+ while (elements.hasNext()) {
+ JsonNode element = elements.next();
+ result.add(readSingle(element, classLoader));
+ }
+
+ return result;
+ }
+
+ return readSingle(node, classLoader);
+ }
+
+ /**
+ * Reads the given {@link JsonNode} into an instance of the type encoded in it using the configured type key.
+ *
+ * @param node must not be {@literal null}.
+ * @param classLoader can be {@literal null}.
+ * @return
+ */
+ private Object readSingle(JsonNode node, @Nullable ClassLoader classLoader) throws IOException {
+
+ JsonNode typeNode = node.findValue(typeKey);
+
+ if (typeNode == null) {
+ throw new IllegalArgumentException(String.format("Could not find type for type key '%s'", typeKey));
+ }
+
+ String typeName = typeNode.asString();
+ Class> type = ClassUtils.resolveClassName(typeName, classLoader);
+
+ return mapper.readerFor(type).readValue(node);
+ }
+}
diff --git a/src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java b/src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java
index 20c7b86d78..b0d6526062 100644
--- a/src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java
+++ b/src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java
@@ -131,18 +131,27 @@ private static boolean hasJsonPathAnnotation(Class> type) {
return false;
}
- private static class InputMessageProjecting implements MethodInterceptor {
-
- private final DocumentContext context;
-
- public InputMessageProjecting(DocumentContext context) {
- this.context = context;
- }
+ private record InputMessageProjecting(DocumentContext context) implements MethodInterceptor {
@Override
public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
+
+ switch (method.getName()) {
+ case "equals" -> {
+ // Only consider equal when proxies are identical.
+ return (invocation.getThis() == invocation.getArguments()[0]);
+ }
+ case "hashCode" -> {
+ // Use hashCode of EntityManager proxy.
+ return context.hashCode();
+ }
+ case "toString" -> {
+ return context.jsonString();
+ }
+ }
+
TypeInformation> returnType = TypeInformation.fromReturnTypeOf(method);
ResolvableType type = ResolvableType.forMethodReturnType(method);
boolean isCollectionResult = type.getRawClass() != null && Collection.class.isAssignableFrom(type.getRawClass());
diff --git a/src/main/java/org/springframework/data/web/ProjectingJackson2HttpMessageConverter.java b/src/main/java/org/springframework/data/web/ProjectingJackson2HttpMessageConverter.java
index 183b151637..3517ad0914 100644
--- a/src/main/java/org/springframework/data/web/ProjectingJackson2HttpMessageConverter.java
+++ b/src/main/java/org/springframework/data/web/ProjectingJackson2HttpMessageConverter.java
@@ -48,7 +48,9 @@
* @author Christoph Strobl
* @soundtrack Richard Spaven - Ice Is Nice (Spaven's 5ive)
* @since 1.13
+ * @deprecated since 4.0, in favor of {@link ProjectingJacksonHttpMessageConverter}.
*/
+@Deprecated(since = "4.0", forRemoval = true)
public class ProjectingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter
implements BeanClassLoaderAware, BeanFactoryAware {
diff --git a/src/main/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverter.java b/src/main/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverter.java
new file mode 100644
index 0000000000..9862bf26a0
--- /dev/null
+++ b/src/main/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverter.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.web;
+
+import tools.jackson.core.JsonGenerator;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.ObjectReader;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanClassLoaderAware;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
+import org.springframework.util.Assert;
+
+import com.jayway.jsonpath.Configuration;
+import com.jayway.jsonpath.InvalidJsonException;
+import com.jayway.jsonpath.TypeRef;
+import com.jayway.jsonpath.spi.json.AbstractJsonProvider;
+import com.jayway.jsonpath.spi.mapper.MappingException;
+import com.jayway.jsonpath.spi.mapper.MappingProvider;
+
+/**
+ * {@link HttpMessageConverter} implementation to enable projected JSON binding to interfaces annotated with
+ * {@link ProjectedPayload}.
+ *
+ * @author Mark Paluch
+ * @author Oliver Gierke
+ * @author Christoph Strobl
+ * @soundtrack Richard Spaven - Ice Is Nice (Spaven's 5ive)
+ * @since 4.0
+ */
+public class ProjectingJacksonHttpMessageConverter extends JacksonJsonHttpMessageConverter
+ implements BeanClassLoaderAware, BeanFactoryAware {
+
+ private final SpelAwareProxyProjectionFactory projectionFactory;
+ private final Map, Boolean> supportedTypesCache = new ConcurrentHashMap<>();
+
+ /**
+ * Creates a new {@link ProjectingJacksonHttpMessageConverter} using a default {@link ObjectMapper}.
+ */
+ public ProjectingJacksonHttpMessageConverter() {
+ this.projectionFactory = initProjectionFactory(getObjectMapper());
+ }
+
+ /**
+ * Creates a new {@link ProjectingJacksonHttpMessageConverter} for the given {@link ObjectMapper}.
+ *
+ * @param mapper must not be {@literal null}.
+ */
+ public ProjectingJacksonHttpMessageConverter(ObjectMapper mapper) {
+
+ super(mapper);
+
+ this.projectionFactory = initProjectionFactory(mapper);
+ }
+
+ /**
+ * Creates a new {@link SpelAwareProxyProjectionFactory} with the {@link JsonProjectingMethodInterceptorFactory}
+ * registered for the given {@link ObjectMapper}.
+ *
+ * @param mapper must not be {@literal null}.
+ * @return
+ */
+ private static SpelAwareProxyProjectionFactory initProjectionFactory(ObjectMapper mapper) {
+
+ Assert.notNull(mapper, "ObjectMapper must not be null");
+
+ SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
+ projectionFactory.registerMethodInvokerFactory(new JsonProjectingMethodInterceptorFactory(
+ new JacksonJsonProvider(mapper), new JacksonMappingProvider(mapper)));
+
+ return projectionFactory;
+ }
+
+ @Override
+ public void setBeanClassLoader(ClassLoader classLoader) {
+ projectionFactory.setBeanClassLoader(classLoader);
+ }
+
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ projectionFactory.setBeanFactory(beanFactory);
+ }
+
+ @Override
+ protected boolean supports(Class> clazz) {
+
+ if (clazz.isInterface()) {
+
+ Boolean result = supportedTypesCache.get(clazz);
+
+ if (result != null) {
+ return result;
+ }
+
+ result = AnnotationUtils.findAnnotation(clazz, ProjectedPayload.class) != null;
+ supportedTypesCache.put(clazz, result);
+
+ return result;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) {
+
+ if (!super.canRead(type, mediaType)) {
+ return false;
+ }
+
+ Class> clazz = type.resolve();
+ if (clazz == null) {
+ return false;
+ }
+
+ return supports(clazz);
+ }
+
+ @Override
+ public boolean canWrite(Class> clazz, @Nullable MediaType mediaType) {
+ return false;
+ }
+
+ @Override
+ public Object read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map hints)
+ throws IOException, HttpMessageNotReadableException {
+ return projectionFactory.createProjection(type.resolve(Object.class), inputMessage.getBody());
+ }
+
+ record JacksonMappingProvider(ObjectMapper objectMapper) implements MappingProvider {
+
+ @Override
+ public @Nullable T map(@Nullable Object source, Class targetType, Configuration configuration) {
+ if (source == null) {
+ return null;
+ }
+ try {
+ return objectMapper.convertValue(source, targetType);
+ } catch (Exception e) {
+ throw new MappingException(e);
+ }
+
+ }
+
+ @Override
+ public @Nullable T map(@Nullable Object source, final TypeRef targetType, Configuration configuration) {
+ if (source == null) {
+ return null;
+ }
+
+ tools.jackson.databind.JavaType type = objectMapper.getTypeFactory().constructType(targetType.getType());
+
+ try {
+ return objectMapper.convertValue(source, type);
+ } catch (Exception e) {
+ throw new MappingException(e);
+ }
+
+ }
+ }
+
+ static class JacksonJsonProvider extends AbstractJsonProvider {
+
+ private static final ObjectMapper defaultObjectMapper = new ObjectMapper();
+ private static final ObjectReader defaultObjectReader = defaultObjectMapper.reader().forType(Object.class);
+
+ protected ObjectMapper objectMapper;
+ protected ObjectReader objectReader;
+
+ public ObjectMapper getObjectMapper() {
+ return objectMapper;
+ }
+
+ /**
+ * Initialize the JacksonProvider with the default ObjectMapper and ObjectReader
+ */
+ public JacksonJsonProvider() {
+ this(defaultObjectMapper, defaultObjectReader);
+ }
+
+ /**
+ * Initialize the JacksonProvider with a custom ObjectMapper.
+ *
+ * @param objectMapper the ObjectMapper to use
+ */
+ public JacksonJsonProvider(ObjectMapper objectMapper) {
+ this(objectMapper, objectMapper.readerFor(Object.class));
+ }
+
+ /**
+ * Initialize the JacksonProvider with a custom ObjectMapper and ObjectReader.
+ *
+ * @param objectMapper the ObjectMapper to use
+ * @param objectReader the ObjectReader to use
+ */
+ public JacksonJsonProvider(ObjectMapper objectMapper, tools.jackson.databind.ObjectReader objectReader) {
+ this.objectMapper = objectMapper;
+ this.objectReader = objectReader;
+ }
+
+ @Override
+ public Object parse(String json) throws InvalidJsonException {
+ return objectReader.readValue(json);
+ }
+
+ @Override
+ public Object parse(InputStream jsonStream, String charset) throws InvalidJsonException {
+ try {
+ return objectReader.readValue(new InputStreamReader(jsonStream, charset));
+ } catch (IOException e) {
+ throw new InvalidJsonException(e);
+ }
+ }
+
+ @Override
+ public String toJson(Object obj) {
+ StringWriter writer = new StringWriter();
+ try {
+ JsonGenerator generator = objectMapper.writer().createGenerator(writer);
+ objectMapper.writeValue(generator, obj);
+ writer.flush();
+ writer.close();
+ generator.close();
+ return writer.getBuffer().toString();
+ } catch (IOException e) {
+ throw new InvalidJsonException(e);
+ }
+ }
+
+ @Override
+ public List createArray() {
+ return new LinkedList<>();
+ }
+
+ @Override
+ public Object createMap() {
+ return new LinkedHashMap();
+ }
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java b/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java
index 1956940ad5..e2e4ca5007 100644
--- a/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java
+++ b/src/main/java/org/springframework/data/web/aot/WebRuntimeHints.java
@@ -23,6 +23,7 @@
import org.springframework.aot.hint.TypeReference;
import org.springframework.data.web.PagedModel;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
+import org.springframework.data.web.config.SpringDataJackson3Configuration;
import org.springframework.data.web.config.SpringDataJacksonConfiguration.PageModule;
import org.springframework.util.ClassUtils;
@@ -30,17 +31,24 @@
* {@link RuntimeHintsRegistrar} providing hints for web usage.
*
* @author Christoph Strobl
+ * @author Mark Paluch
* @since 3.2.3
*/
class WebRuntimeHints implements RuntimeHintsRegistrar {
+ private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
+ WebRuntimeHints.class.getClassLoader());
+
+ private static final boolean JACKSON3_PRESENT = ClassUtils.isPresent("tools.jackson.databind.ObjectMapper",
+ WebRuntimeHints.class.getClassLoader());
+
@Override
public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
hints.reflection().registerType(org.springframework.data.web.config.SpringDataWebSettings.class, hint -> hint
.withMembers(MemberCategory.INVOKE_DECLARED_METHODS).onReachableType(EnableSpringDataWebSupport.class));
- if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)) {
+ if (JACKSON2_PRESENT || JACKSON3_PRESENT) {
// Page Model for Jackson Rendering
hints.reflection().registerType(org.springframework.data.web.PagedModel.class,
@@ -49,24 +57,53 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader)
hints.reflection().registerType(PagedModel.PageMetadata.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INVOKE_PUBLIC_METHODS);
- // Type that might not be seen otherwise
- hints.reflection().registerType(TypeReference.of("org.springframework.data.domain.Unpaged"),
- hint -> hint.onReachableType(PageModule.class));
-
- // Jackson Converters used via @JsonSerialize in SpringDataJacksonConfiguration
- hints.reflection().registerType(
- TypeReference
- .of("org.springframework.data.web.config.SpringDataJacksonConfiguration$PageModule$PageModelConverter"),
- hint -> {
- hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
- hint.onReachableType(PageModule.class);
- });
- hints.reflection().registerType(TypeReference.of(
- "org.springframework.data.web.config.SpringDataJacksonConfiguration$PageModule$PlainPageSerializationWarning"),
- hint -> {
- hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
- hint.onReachableType(PageModule.class);
- });
+ hints.reflection().registerType(TypeReference.of("org.springframework.data.domain.Unpaged"));
+
+ if (JACKSON2_PRESENT) {
+ contributeJackson2Hints(hints);
+ }
+
+ if (JACKSON3_PRESENT) {
+ contributeJackson3Hints(hints);
+ }
}
}
+
+ @SuppressWarnings("removal")
+ private static void contributeJackson2Hints(RuntimeHints hints) {
+
+ // Jackson Converters used via @JsonSerialize in SpringDataJacksonConfiguration
+ hints.reflection().registerType(
+ TypeReference
+ .of("org.springframework.data.web.config.SpringDataJacksonConfiguration$PageModule$PageModelConverter"),
+ hint -> {
+ hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
+ hint.onReachableType(PageModule.class);
+ });
+ hints.reflection().registerType(TypeReference.of(
+ "org.springframework.data.web.config.SpringDataJacksonConfiguration$PageModule$PlainPageSerializationWarning"),
+ hint -> {
+ hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
+ hint.onReachableType(PageModule.class);
+ });
+ }
+
+ private static void contributeJackson3Hints(RuntimeHints hints) {
+
+ // Jackson Converters used via @JsonSerialize in SpringDataJacksonConfiguration
+ hints.reflection().registerType(
+ TypeReference
+ .of("org.springframework.data.web.config.SpringDataJackson3Configuration$PageModule$PageModelConverter"),
+ hint -> {
+ hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
+ hint.onReachableType(SpringDataJackson3Configuration.PageModule.class);
+ });
+ hints.reflection().registerType(TypeReference.of(
+ "org.springframework.data.web.config.SpringDataJackson3Configuration$PageModule$PlainPageSerializationWarning"),
+ hint -> {
+ hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS);
+ hint.onReachableType(SpringDataJackson3Configuration.PageModule.class);
+ });
+ }
+
}
diff --git a/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java b/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java
index aae4f2015d..f05cf2a1d9 100644
--- a/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java
+++ b/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java
@@ -143,7 +143,12 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) {
resourceLoader//
.filter(it -> ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", it))//
.map(it -> SpringFactoriesLoader.loadFactoryNames(SpringDataJacksonModules.class, it))//
- .ifPresent(it -> imports.addAll(it));
+ .ifPresent(imports::addAll);
+
+ resourceLoader//
+ .filter(it -> ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", it))//
+ .map(it -> SpringFactoriesLoader.loadFactoryNames(SpringDataJackson3Modules.class, it))//
+ .ifPresent(imports::addAll);
return imports.toArray(new String[imports.size()]);
}
diff --git a/src/main/java/org/springframework/data/web/config/SpringDataJackson3Configuration.java b/src/main/java/org/springframework/data/web/config/SpringDataJackson3Configuration.java
new file mode 100644
index 0000000000..800ded7abd
--- /dev/null
+++ b/src/main/java/org/springframework/data/web/config/SpringDataJackson3Configuration.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2014-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.web.config;
+
+import tools.jackson.databind.annotation.JsonSerialize;
+import tools.jackson.databind.module.SimpleModule;
+import tools.jackson.databind.ser.BeanPropertyWriter;
+import tools.jackson.databind.ser.ValueSerializerModifier;
+import tools.jackson.databind.ser.std.ToStringSerializerBase;
+import tools.jackson.databind.util.StdConverter;
+
+import java.io.Serial;
+import java.util.List;
+
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.geo.GeoModule;
+import org.springframework.data.web.PagedModel;
+import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode;
+import org.springframework.util.ClassUtils;
+
+/**
+ * JavaConfig class to export Jackson 3-specific configuration.
+ *
+ * @author Oliver Gierke
+ * @author Mark Paluch
+ * @since 4.0
+ */
+public class SpringDataJackson3Configuration implements SpringDataJackson3Modules {
+
+ @Nullable
+ @Autowired(required = false) SpringDataWebSettings settings;
+
+ @Bean
+ public GeoModule jackson3GeoModule() {
+ return new GeoModule();
+ }
+
+ @Bean
+ public PageModule jackson3pageModule() {
+ return new PageModule(settings);
+ }
+
+ /**
+ * A Jackson module customizing the serialization of {@link PageImpl} instances depending on the
+ * {@link SpringDataWebSettings} handed into the instance. In case of {@link PageSerializationMode#DIRECT} being
+ * configured, a no-op {@link StdConverter} is registered to issue a one-time warning about the mode being used (as
+ * it's not recommended). {@link PageSerializationMode#VIA_DTO} would register a converter wrapping {@link PageImpl}
+ * instances into {@link PagedModel}.
+ *
+ * @author Oliver Drotbohm
+ */
+ public static class PageModule extends SimpleModule {
+
+ private static final @Serial long serialVersionUID = 275254460581626332L;
+
+ private static final String UNPAGED_TYPE_NAME = "org.springframework.data.domain.Unpaged";
+ private static final Class> UNPAGED_TYPE;
+
+ static {
+ UNPAGED_TYPE = ClassUtils.resolveClassName(UNPAGED_TYPE_NAME, PageModule.class.getClassLoader());
+ }
+
+ /**
+ * Creates a new {@link PageModule} for the given {@link SpringDataWebSettings}.
+ *
+ * @param settings can be {@literal null}.
+ */
+ public PageModule(@Nullable SpringDataWebSettings settings) {
+
+ addSerializer(UNPAGED_TYPE, new UnpagedAsInstanceSerializer());
+
+ if (settings == null || settings.pageSerializationMode() == PageSerializationMode.DIRECT) {
+ setSerializerModifier(new WarningLoggingModifier());
+
+ } else {
+ setMixInAnnotation(PageImpl.class, WrappingMixing.class);
+ }
+ }
+
+ /**
+ * A Jackson serializer rendering instances of {@code org.springframework.data.domain.Unpaged} as {@code INSTANCE}
+ * as it was previous rendered.
+ *
+ * @author Oliver Drotbohm
+ */
+ static class UnpagedAsInstanceSerializer extends ToStringSerializerBase {
+
+ public UnpagedAsInstanceSerializer() {
+ super(Object.class);
+ }
+
+ @Override
+ public String valueToString(@Nullable Object value) {
+ return "INSTANCE";
+ }
+ }
+
+ @JsonSerialize(converter = PageModelConverter.class)
+ abstract static class WrappingMixing {}
+
+ static class PageModelConverter extends StdConverter, PagedModel>> {
+
+ @Override
+ public @Nullable PagedModel> convert(@Nullable Page> value) {
+ return value == null ? null : new PagedModel<>(value);
+ }
+ }
+
+ /**
+ * A {@link ValueSerializerModifier} that logs a warning message if an instance of {@link Page} will be rendered.
+ *
+ * @author Oliver Drotbohm
+ */
+ static class WarningLoggingModifier extends ValueSerializerModifier {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(WarningLoggingModifier.class);
+ private static final String MESSAGE = """
+ Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!
+ For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO))
+ or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables.
+ """;
+
+ private static final @Serial long serialVersionUID = 954857444010009875L;
+
+ private boolean warningRendered = false;
+
+ @Override
+ public List changeProperties(tools.jackson.databind.SerializationConfig config,
+ tools.jackson.databind.BeanDescription.Supplier beanDesc, List beanProperties) {
+
+ if (Page.class.isAssignableFrom(beanDesc.getBeanClass()) && !warningRendered) {
+
+ this.warningRendered = true;
+ LOGGER.warn(MESSAGE);
+ }
+
+ return super.changeProperties(config, beanDesc, beanProperties);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/web/config/SpringDataJackson3Modules.java b/src/main/java/org/springframework/data/web/config/SpringDataJackson3Modules.java
new file mode 100644
index 0000000000..7db0817e74
--- /dev/null
+++ b/src/main/java/org/springframework/data/web/config/SpringDataJackson3Modules.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.web.config;
+
+/**
+ * Marker interface to describe configuration classes that ship Jackson modules that are supposed to be added to the
+ * Jackson 3 {@link tools.jackson.databind.ObjectMapper} configured for {@link EnableSpringDataWebSupport}.
+ *
+ * @author Oliver Gierke
+ * @author Mark Paluch
+ * @since 4.0
+ */
+public interface SpringDataJackson3Modules {}
diff --git a/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java b/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java
index 4e4014a5cd..67f2320747 100644
--- a/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java
+++ b/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java
@@ -15,6 +15,7 @@
*/
package org.springframework.data.web.config;
+
import java.io.Serial;
import java.util.List;
@@ -44,10 +45,15 @@
* JavaConfig class to export Jackson specific configuration.
*
* @author Oliver Gierke
+ * @author Mark Paluch
+ * @deprecated since 4.0, in favor of {@link SpringDataJackson3Configuration} which uses Jackson 3.
*/
+@SuppressWarnings("removal")
+@Deprecated(since = "4.0", forRemoval = true)
public class SpringDataJacksonConfiguration implements SpringDataJacksonModules {
- @Nullable @Autowired(required = false) SpringDataWebSettings settings;
+ @Nullable
+ @Autowired(required = false) SpringDataWebSettings settings;
@Bean
public GeoModule jacksonGeoModule() {
@@ -61,12 +67,10 @@ public PageModule pageModule() {
/**
* A Jackson module customizing the serialization of {@link PageImpl} instances depending on the
- * {@link SpringDataWebSettings} handed into the instance. In case of
- * {@link org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode#DIRECT} being
+ * {@link SpringDataWebSettings} handed into the instance. In case of {@link PageSerializationMode#DIRECT} being
* configured, a no-op {@link StdConverter} is registered to issue a one-time warning about the mode being used (as
- * it's not recommended).
- * {@link org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode#VIA_DTO} would register
- * a converter wrapping {@link PageImpl} instances into {@link PagedModel}.
+ * it's not recommended). {@link PageSerializationMode#VIA_DTO} would register a converter wrapping {@link PageImpl}
+ * instances into {@link PagedModel}.
*
* @author Oliver Drotbohm
*/
@@ -99,7 +103,7 @@ public PageModule(@Nullable SpringDataWebSettings settings) {
}
/**
- * A Jackson serializer rendering instances of {@link org.springframework.data.domain.Unpaged} as {@code INSTANCE}
+ * A Jackson serializer rendering instances of {@code org.springframework.data.domain.Unpaged} as {@code INSTANCE}
* as it was previous rendered.
*
* @author Oliver Drotbohm
@@ -119,7 +123,7 @@ public String valueToString(@Nullable Object value) {
}
@JsonSerialize(converter = PageModelConverter.class)
- abstract class WrappingMixing {}
+ abstract static class WrappingMixing {}
static class PageModelConverter extends StdConverter, PagedModel>> {
@@ -161,4 +165,5 @@ public List changeProperties(SerializationConfig config, Bea
}
}
}
+
}
diff --git a/src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java b/src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java
index 4269bf34d1..927c92aa2a 100644
--- a/src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java
+++ b/src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java
@@ -23,5 +23,7 @@
*
* @author Oliver Gierke
* @since 1.13
+ * @deprecated since 4.0, in favor of {@link SpringDataJackson3Configuration} which uses Jackson 3.
*/
+@Deprecated(since = "4.0", forRemoval = true)
public interface SpringDataJacksonModules {}
diff --git a/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java b/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java
index 505bb8c875..d402ecf459 100644
--- a/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java
+++ b/src/main/java/org/springframework/data/web/config/SpringDataWebConfiguration.java
@@ -15,6 +15,7 @@
*/
package org.springframework.data.web.config;
+import java.util.ArrayList;
import java.util.List;
import org.jspecify.annotations.Nullable;
@@ -33,18 +34,19 @@
import org.springframework.data.web.OffsetScrollPositionHandlerMethodArgumentResolver;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.data.web.ProjectingJackson2HttpMessageConverter;
+import org.springframework.data.web.ProjectingJacksonHttpMessageConverter;
import org.springframework.data.web.ProxyingHandlerMethodArgumentResolver;
import org.springframework.data.web.SortHandlerMethodArgumentResolver;
import org.springframework.data.web.XmlBeamHttpMessageConverter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Configuration class to register {@link PageableHandlerMethodArgumentResolver},
@@ -151,18 +153,43 @@ public void addArgumentResolvers(List argumentRes
}
@Override
- public void extendMessageConverters(List> converters) {
+ public void configureMessageConverters(HttpMessageConverters.Builder builder) {
- if (ClassUtils.isPresent("com.jayway.jsonpath.DocumentContext", context.getClassLoader())
- && ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", context.getClassLoader())) {
+ List> converters = new ArrayList<>();
+ configureMessageConverters(converters);
- ObjectMapper mapper = context.getBeanProvider(ObjectMapper.class).getIfUnique(ObjectMapper::new);
+ for (HttpMessageConverter> converter : converters) {
+ builder.additionalMessageConverter(converter);
+ }
+ }
+
+ @Override
+ public void configureMessageConverters(List> converters) {
+
+ if (ClassUtils.isPresent("com.jayway.jsonpath.DocumentContext", context.getClassLoader())) {
+
+ if (ClassUtils.isPresent("tools.jackson.databind.ObjectReader", context.getClassLoader())) {
+
+ tools.jackson.databind.ObjectMapper mapper = context.getBeanProvider(tools.jackson.databind.ObjectMapper.class)
+ .getIfUnique(tools.jackson.databind.ObjectMapper::new);
+
+ ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter(mapper);
+ converter.setBeanFactory(context);
+ forwardBeanClassLoader(converter);
+
+ converters.add(0, converter);
+ } else if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", context.getClassLoader())) {
+
+ com.fasterxml.jackson.databind.ObjectMapper mapper = context
+ .getBeanProvider(com.fasterxml.jackson.databind.ObjectMapper.class)
+ .getIfUnique(com.fasterxml.jackson.databind.ObjectMapper::new);
- ProjectingJackson2HttpMessageConverter converter = new ProjectingJackson2HttpMessageConverter(mapper);
- converter.setBeanFactory(context);
- forwardBeanClassLoader(converter);
+ ProjectingJackson2HttpMessageConverter converter = new ProjectingJackson2HttpMessageConverter(mapper);
+ converter.setBeanFactory(context);
+ forwardBeanClassLoader(converter);
- converters.add(0, converter);
+ converters.add(0, converter);
+ }
}
if (ClassUtils.isPresent("org.xmlbeam.XBProjector", context.getClassLoader())) {
diff --git a/src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt b/src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt
index 3224afff8d..b8a94b80ad 100644
--- a/src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt
+++ b/src/main/kotlin/org/springframework/data/mapping/KPropertyPath.kt
@@ -49,7 +49,7 @@ internal class KIterablePropertyPath(
* @author Tjeu Kayim
* @author Mikhail Polivakha
*/
-internal fun asString(property: KProperty<*>): String {
+fun asString(property: KProperty<*>): String {
return when (property) {
is KPropertyPath<*, *> ->
"${asString(property.parent)}.${property.child.name}"
diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories
index 506aaa02df..1ad50e1622 100644
--- a/src/main/resources/META-INF/spring.factories
+++ b/src/main/resources/META-INF/spring.factories
@@ -1,4 +1,5 @@
org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SpringDataJacksonConfiguration
+org.springframework.data.web.config.SpringDataJackson3Modules=org.springframework.data.web.config.SpringDataJackson3Configuration
org.springframework.data.util.CustomCollectionRegistrar=org.springframework.data.util.CustomCollections.VavrCollections, \
org.springframework.data.util.CustomCollections.EclipseCollections
org.springframework.beans.BeanInfoFactory=org.springframework.data.util.KotlinBeanInfoFactory
diff --git a/src/test/java/org/springframework/data/geo/GeoJacksonModuleIntegrationTests.java b/src/test/java/org/springframework/data/geo/GeoJacksonModuleIntegrationTests.java
new file mode 100755
index 0000000000..87a6d4cab4
--- /dev/null
+++ b/src/test/java/org/springframework/data/geo/GeoJacksonModuleIntegrationTests.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2014-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.geo;
+
+import static org.assertj.core.api.Assertions.*;
+
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Integration tests for {@link GeoModule}.
+ *
+ * @author Oliver Gierke
+ * @author Mark Paluch
+ */
+class GeoJacksonModuleIntegrationTests {
+
+ ObjectMapper mapper;
+
+ @BeforeEach
+ void setUp() {
+
+ this.mapper = JsonMapper.builder().addModule(new GeoJacksonModule()).build();
+ }
+
+ @Test // DATACMNS-475
+ void deserializesDistance() throws Exception {
+
+ var json = "{\"value\":10.0,\"metric\":\"KILOMETERS\"}";
+ var reference = new Distance(10.0, Metrics.KILOMETERS);
+
+ assertThat(mapper.readValue(json, Distance.class)).isEqualTo(reference);
+ assertThat(mapper.writeValueAsString(reference)).isEqualTo(json);
+ }
+
+ @Test // DATACMNS-475
+ void deserializesPoint() throws Exception {
+
+ var json = "{\"x\":10.0,\"y\":20.0}";
+ var reference = new Point(10.0, 20.0);
+
+ assertThat(mapper.readValue(json, Point.class)).isEqualTo(reference);
+ assertThat(mapper.writeValueAsString(reference)).isEqualTo(json);
+ }
+
+ @Test // DATACMNS-475
+ void deserializesCircle() throws Exception {
+
+ var json = "{\"center\":{\"x\":10.0,\"y\":20.0},\"radius\":{\"value\":10.0,\"metric\":\"KILOMETERS\"}}";
+ var reference = new Circle(new Point(10.0, 20.0), new Distance(10, Metrics.KILOMETERS));
+
+ assertThat(mapper.readValue(json, Circle.class)).isEqualTo(reference);
+ assertThat(mapper.writeValueAsString(reference)).isEqualTo(json);
+ }
+
+ @Test // DATACMNS-475
+ void deserializesBox() throws Exception {
+
+ var json = "{\"first\":{\"x\":1.0,\"y\":2.0},\"second\":{\"x\":2.0,\"y\":3.0}}";
+ var reference = new Box(new Point(1, 2), new Point(2, 3));
+
+ assertThat(mapper.readValue(json, Box.class)).isEqualTo(reference);
+ assertThat(mapper.writeValueAsString(reference)).isEqualTo(json);
+ }
+
+ @Test // DATACMNS-475
+ void deserializesPolygon() throws Exception {
+
+ var json = "{\"points\":[{\"x\":1.0,\"y\":2.0},{\"x\":2.0,\"y\":3.0},{\"x\":3.0,\"y\":4.0}]}";
+ var reference = new Polygon(new Point(1, 2), new Point(2, 3), new Point(3, 4));
+
+ assertThat(mapper.readValue(json, Polygon.class)).isEqualTo(reference);
+ assertThat(mapper.writeValueAsString(reference)).isEqualTo(json);
+ }
+}
diff --git a/src/test/java/org/springframework/data/repository/init/JacksonResourceReaderIntegrationTests.java b/src/test/java/org/springframework/data/repository/init/JacksonResourceReaderIntegrationTests.java
new file mode 100755
index 0000000000..72616925ad
--- /dev/null
+++ b/src/test/java/org/springframework/data/repository/init/JacksonResourceReaderIntegrationTests.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2013-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.repository.init;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Collection;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.core.io.ClassPathResource;
+
+/**
+ * Integration tests for {@link JacksonResourceReader}.
+ *
+ * @author Mark Paluch
+ */
+class JacksonResourceReaderIntegrationTests {
+
+ @Test
+ void readsFileWithMultipleObjects() throws Exception {
+
+ ResourceReader reader = new JacksonResourceReader();
+ var result = reader.readFrom(new ClassPathResource("data.json", getClass()), null);
+
+ assertThat(result).isInstanceOf(Collection.class);
+ assertThat((Collection>) result).hasSize(1);
+ }
+}
diff --git a/src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java b/src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java
index 252c1a832a..586fa6ecaa 100755
--- a/src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java
+++ b/src/test/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactoryUnitTests.java
@@ -17,6 +17,8 @@
import static org.assertj.core.api.Assertions.*;
+import tools.jackson.databind.ObjectMapper;
+
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
@@ -25,14 +27,12 @@
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.util.ObjectUtils;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.json.JsonProvider;
-import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
/**
@@ -59,8 +59,8 @@ void setUp() {
var projectionFactory = new SpelAwareProxyProjectionFactory();
var objectMapper = new ObjectMapper();
- MappingProvider mappingProvider = new JacksonMappingProvider(objectMapper);
- JsonProvider jsonProvider = new JacksonJsonProvider(objectMapper);
+ MappingProvider mappingProvider = new ProjectingJacksonHttpMessageConverter.JacksonMappingProvider(objectMapper);
+ JsonProvider jsonProvider = new ProjectingJacksonHttpMessageConverter.JacksonJsonProvider(objectMapper);
projectionFactory
.registerMethodInvokerFactory(new JsonProjectingMethodInterceptorFactory(jsonProvider, mappingProvider));
diff --git a/src/test/java/org/springframework/data/web/PageImplJsonJackson2SerializationUnitTests.java b/src/test/java/org/springframework/data/web/PageImplJsonJackson2SerializationUnitTests.java
new file mode 100644
index 0000000000..4aa616c1bf
--- /dev/null
+++ b/src/test/java/org/springframework/data/web/PageImplJsonJackson2SerializationUnitTests.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.web;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode;
+import org.springframework.data.web.config.SpringDataJacksonConfiguration;
+import org.springframework.data.web.config.SpringDataWebSettings;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.jayway.jsonpath.JsonPath;
+
+/**
+ * Unit tests for PageImpl serialization.
+ *
+ * @author Oliver Drotbohm
+ * @author Mark Paluch
+ */
+class PageImplJsonJackson2SerializationUnitTests {
+
+ @Test // GH-3024
+ void serializesPageImplAsJson() {
+ assertJsonRendering(PageSerializationMode.DIRECT, "$.pageable", "$.last", "$.first");
+ }
+
+ @Test // GH-3024
+ void serializesPageImplAsPagedModel() {
+ assertJsonRendering(PageSerializationMode.VIA_DTO, "$.content", "$.page");
+ }
+
+ @Test // GH-3137
+ void serializesCustomPageAsPageImpl() {
+ assertJsonRendering(PageSerializationMode.DIRECT, new Extension<>("header"), "$.pageable", "$.last", "$.first");
+ }
+
+ private static void assertJsonRendering(PageSerializationMode mode, String... jsonPaths) {
+ assertJsonRendering(mode, new PageImpl<>(Collections.emptyList()), jsonPaths);
+ }
+
+ private static void assertJsonRendering(PageSerializationMode mode, PageImpl> page, String... jsonPaths) {
+
+ SpringDataWebSettings settings = new SpringDataWebSettings(mode);
+
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.registerModule(new SpringDataJacksonConfiguration.PageModule(settings));
+
+ assertThatNoException().isThrownBy(() -> {
+
+ String result = mapper.writeValueAsString(page);
+
+ for (String jsonPath : jsonPaths) {
+ assertThat(JsonPath. read(result, jsonPath)).isNotNull();
+ }
+ });
+ }
+
+ static class Extension extends PageImpl {
+
+ private Object header;
+
+ public Extension(Object header) {
+ super(Collections.emptyList());
+ }
+
+ public Object getHeader() {
+ return header;
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/web/PageImplJsonSerializationUnitTests.java b/src/test/java/org/springframework/data/web/PageImplJsonSerializationUnitTests.java
index 77f88028bb..7ebab17cac 100644
--- a/src/test/java/org/springframework/data/web/PageImplJsonSerializationUnitTests.java
+++ b/src/test/java/org/springframework/data/web/PageImplJsonSerializationUnitTests.java
@@ -17,21 +17,25 @@
import static org.assertj.core.api.Assertions.*;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.json.JsonMapper;
+
import java.util.Collections;
import org.junit.jupiter.api.Test;
+
import org.springframework.data.domain.PageImpl;
import org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode;
-import org.springframework.data.web.config.SpringDataJacksonConfiguration;
+import org.springframework.data.web.config.SpringDataJackson3Configuration;
import org.springframework.data.web.config.SpringDataWebSettings;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.JsonPath;
/**
* Unit tests for PageImpl serialization.
*
* @author Oliver Drotbohm
+ * @author Mark Paluch
*/
class PageImplJsonSerializationUnitTests {
@@ -58,8 +62,8 @@ private static void assertJsonRendering(PageSerializationMode mode, PageImpl>
SpringDataWebSettings settings = new SpringDataWebSettings(mode);
- ObjectMapper mapper = new ObjectMapper();
- mapper.registerModule(new SpringDataJacksonConfiguration.PageModule(settings));
+ ObjectMapper mapper = JsonMapper.builder().addModule(new SpringDataJackson3Configuration.PageModule(settings))
+ .build();
assertThatNoException().isThrownBy(() -> {
diff --git a/src/test/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverterUnitTests.java b/src/test/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverterUnitTests.java
new file mode 100755
index 0000000000..b8a1b96794
--- /dev/null
+++ b/src/test/java/org/springframework/data/web/ProjectingJacksonHttpMessageConverterUnitTests.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.web;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.http.MediaType;
+
+/**
+ * Unit tests for {@link ProjectingJacksonHttpMessageConverter}.
+ *
+ * @author Oliver Gierke
+ * @author Mark Paluch
+ */
+class ProjectingJacksonHttpMessageConverterUnitTests {
+
+ ProjectingJacksonHttpMessageConverter converter = new ProjectingJacksonHttpMessageConverter();
+ MediaType ANYTHING_JSON = MediaType.parseMediaType("application/*+json");
+
+ @Test // DATCMNS-885
+ void canReadJsonIntoAnnotatedInterface() {
+ assertThat(converter.canRead(SampleInterface.class, ANYTHING_JSON)).isTrue();
+ }
+
+ @Test // DATCMNS-885
+ void cannotReadUnannotatedInterface() {
+ assertThat(converter.canRead(UnannotatedInterface.class, ANYTHING_JSON)).isFalse();
+ }
+
+ @Test // DATCMNS-885
+ void cannotReadClass() {
+ assertThat(converter.canRead(SampleClass.class, ANYTHING_JSON)).isFalse();
+ }
+
+ @Test // DATACMNS-972
+ void doesNotConsiderTypeVariableBoundTo() throws Throwable {
+
+ var method = BaseController.class.getDeclaredMethod("createEntity", AbstractDto.class);
+
+ assertThat(converter.canRead(ResolvableType.forMethodParameter(method, 0), ANYTHING_JSON)).isFalse();
+ }
+
+ @Test // DATACMNS-972
+ void genericTypeOnConcreteOne() throws Throwable {
+
+ var method = ConcreteController.class.getMethod("createEntity", AbstractDto.class);
+
+ assertThat(converter.canRead(ResolvableType.forMethodParameter(method, 0), ANYTHING_JSON)).isFalse();
+ }
+
+ @ProjectedPayload
+ interface SampleInterface {}
+
+ interface UnannotatedInterface {}
+
+ class SampleClass {}
+
+ class AbstractDto {}
+
+ abstract class BaseController {
+ public void createEntity(D dto) {}
+ }
+
+ class ConcreteController extends BaseController {}
+}
diff --git a/src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java b/src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java
index 5413974db6..9a35b4ba5e 100644
--- a/src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java
+++ b/src/test/java/org/springframework/data/web/aot/WebRuntimeHintsUnitTests.java
@@ -15,11 +15,11 @@
*/
package org.springframework.data.web.aot;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
+
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
@@ -48,7 +48,21 @@ void shouldRegisterRuntimeHintWhenJacksonPresent() {
@Test // GH-3033, GH-3171
@ClassPathExclusions(packages = { "com.fasterxml.jackson.databind" })
- void shouldRegisterRuntimeHintWithTypeNameWhenJacksonNotPresent() {
+ void shouldRegisterRuntimeHintWithTypeNameWhenJackson2NotPresent() {
+
+ ReflectionHints reflectionHints = new ReflectionHints();
+ RuntimeHints runtimeHints = mock(RuntimeHints.class);
+ when(runtimeHints.reflection()).thenReturn(reflectionHints);
+
+ new WebRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader());
+
+ assertThat(runtimeHints).matches(RuntimeHintsPredicates.reflection()
+ .onType(TypeReference.of("org.springframework.data.web.config.SpringDataWebSettings")));
+ }
+
+ @Test // GH-3292
+ @ClassPathExclusions(packages = { "tools.jackson" })
+ void shouldRegisterRuntimeHintWithTypeNameWhenJackson3NotPresent() {
ReflectionHints reflectionHints = new ReflectionHints();
RuntimeHints runtimeHints = mock(RuntimeHints.class);
diff --git a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java
index d680dc3c29..2d565fd915 100755
--- a/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java
+++ b/src/test/java/org/springframework/data/web/config/EnableSpringDataWebSupportIntegrationTests.java
@@ -19,10 +19,14 @@
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+import tools.jackson.databind.JacksonModule;
+import tools.jackson.databind.json.JsonMapper;
+
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.Test;
+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
@@ -41,10 +45,9 @@
import org.springframework.data.web.ProxyingHandlerMethodArgumentResolver;
import org.springframework.data.web.SortHandlerMethodArgumentResolver;
import org.springframework.data.web.WebTestUtils;
-import org.springframework.data.web.config.SpringDataJacksonConfiguration.PageModule;
-import org.springframework.http.converter.HttpMessageConverter;
-import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
-import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.data.web.config.SpringDataJackson3Configuration.PageModule;
+import org.springframework.http.converter.HttpMessageConverters;
+import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@@ -52,7 +55,6 @@
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.util.UriComponentsBuilder;
-import com.fasterxml.jackson.databind.Module;
/**
* Integration tests for {@link EnableSpringDataWebSupport}.
@@ -124,7 +126,7 @@ SimpleEntityPathResolver entityPathResolver() {
@Configuration
static class PageSampleConfig extends WebMvcConfigurationSupport {
- @Autowired private List modules;
+ @Autowired private List modules;
@Bean
PageSampleController controller() {
@@ -132,10 +134,11 @@ PageSampleController controller() {
}
@Override
- protected void configureMessageConverters(List> converters) {
- Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json().modules(modules);
- converters.add(0, new MappingJackson2HttpMessageConverter(builder.build()));
+ protected void configureMessageConverters(HttpMessageConverters.Builder builder) {
+ builder.jsonMessageConverter(new JacksonJsonHttpMessageConverter(
+ JsonMapper.builder().addModules(modules.toArray(new JacksonModule[0])).build()));
}
+
}
@EnableSpringDataWebSupport
@@ -188,7 +191,7 @@ void registersJacksonSpecificBeanDefinitions() {
}
@Test // DATACMNS-475
- @ClassPathExclusions(packages = { "com.fasterxml.jackson.databind" })
+ @ClassPathExclusions(packages = { "com.fasterxml.jackson.databind", "tools.jackson.databind" })
void doesNotRegisterJacksonSpecificComponentsIfJacksonNotPresent() {
ApplicationContext context = WebTestUtils.createApplicationContext(SampleConfig.class);
diff --git a/src/test/java/org/springframework/data/web/config/SpringDataWebConfigurationIntegrationTests.java b/src/test/java/org/springframework/data/web/config/SpringDataWebConfigurationIntegrationTests.java
index 6586701e13..748df322a8 100644
--- a/src/test/java/org/springframework/data/web/config/SpringDataWebConfigurationIntegrationTests.java
+++ b/src/test/java/org/springframework/data/web/config/SpringDataWebConfigurationIntegrationTests.java
@@ -16,26 +16,31 @@
package org.springframework.data.web.config;
import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import tools.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
-import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.classloadersupport.HidingClassLoader;
-import org.springframework.data.web.ProjectingJackson2HttpMessageConverter;
+import org.springframework.data.web.ProjectingJacksonHttpMessageConverter;
import org.springframework.data.web.XmlBeamHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.instrument.classloading.ShadowingClassLoader;
import org.springframework.util.ReflectionUtils;
+
import org.xmlbeam.XBProjector;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.DocumentContext;
/**
@@ -50,45 +55,48 @@ class SpringDataWebConfigurationIntegrationTests {
@Test // DATACMNS-987
void shouldNotLoadJacksonConverterWhenJacksonNotPresent() {
- List> converters = new ArrayList>();
+ HttpMessageConverters.Builder builder = mock(HttpMessageConverters.Builder.class);
- createConfigWithClassLoader(HidingClassLoader.hide(ObjectMapper.class),
- it -> it.extendMessageConverters(converters));
+ createConfigWithClassLoader(
+ HidingClassLoader.hide(ObjectMapper.class, com.fasterxml.jackson.databind.ObjectMapper.class),
+ it -> it.configureMessageConverters(builder));
- assertThat(converters).areNot(instanceWithClassName(ProjectingJackson2HttpMessageConverter.class));
+ verify(builder).additionalMessageConverter(any(XmlBeamHttpMessageConverter.class));
+ verifyNoMoreInteractions(builder);
}
@Test // DATACMNS-987
void shouldNotLoadJacksonConverterWhenJaywayNotPresent() {
- List> converters = new ArrayList>();
+ HttpMessageConverters.Builder builder = mock(HttpMessageConverters.Builder.class);
createConfigWithClassLoader(HidingClassLoader.hide(DocumentContext.class),
- it -> it.extendMessageConverters(converters));
+ it -> it.configureMessageConverters(builder));
- assertThat(converters).areNot(instanceWithClassName(ProjectingJackson2HttpMessageConverter.class));
+ verify(builder).additionalMessageConverter(any(XmlBeamHttpMessageConverter.class));
+ verifyNoMoreInteractions(builder);
}
@Test // DATACMNS-987
void shouldNotLoadXBeamConverterWhenXBeamNotPresent() throws Exception {
- List> converters = new ArrayList>();
+ HttpMessageConverters.Builder builder = mock(HttpMessageConverters.Builder.class);
ClassLoader classLoader = HidingClassLoader.hide(XBProjector.class);
- createConfigWithClassLoader(classLoader, it -> it.extendMessageConverters(converters));
+ createConfigWithClassLoader(classLoader, it -> it.configureMessageConverters(builder));
- assertThat(converters).areNot(instanceWithClassName(XmlBeamHttpMessageConverter.class));
+ verify(builder, never()).additionalMessageConverter(any(XmlBeamHttpMessageConverter.class));
}
@Test // DATACMNS-987
void shouldLoadAllConvertersWhenDependenciesArePresent() throws Exception {
- List> converters = new ArrayList>();
+ HttpMessageConverters.Builder builder = mock(HttpMessageConverters.Builder.class);
- createConfigWithClassLoader(getClass().getClassLoader(), it -> it.extendMessageConverters(converters));
+ createConfigWithClassLoader(getClass().getClassLoader(), it -> it.configureMessageConverters(builder));
- assertThat(converters).haveAtLeastOne(instanceWithClassName(XmlBeamHttpMessageConverter.class));
- assertThat(converters).haveAtLeastOne(instanceWithClassName(ProjectingJackson2HttpMessageConverter.class));
+ verify(builder).additionalMessageConverter(any(XmlBeamHttpMessageConverter.class));
+ verify(builder).additionalMessageConverter(any(ProjectingJacksonHttpMessageConverter.class));
}
@Test // DATACMNS-1152
@@ -96,16 +104,19 @@ void usesCustomObjectMapper() {
createConfigWithClassLoader(getClass().getClassLoader(), it -> {
- List> converters = new ArrayList<>();
- it.extendMessageConverters(converters);
+ HttpMessageConverters.Builder builder = mock(HttpMessageConverters.Builder.class);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(HttpMessageConverter.class);
+
+ it.configureMessageConverters(builder);
+ verify(builder, atLeast(1)).additionalMessageConverter(captor.capture());
// Converters contains ProjectingJackson2HttpMessageConverter with custom ObjectMapper
- assertThat(converters).anySatisfy(converter -> {
- assertThat(converter).isInstanceOfSatisfying(ProjectingJackson2HttpMessageConverter.class, __ -> {
+
+ assertThat(captor.getAllValues()).anySatisfy(converter -> {
+ assertThat(converter).isInstanceOfSatisfying(ProjectingJacksonHttpMessageConverter.class, __ -> {
assertThat(__.getObjectMapper()).isSameAs(SomeConfiguration.MAPPER);
});
});
-
}, SomeConfiguration.class);
}
@@ -138,20 +149,6 @@ private static Class> loadWithout(Class> configurationClass, Class>... typ
return loader.loadClass(configurationClass.getName());
}
- /**
- * Creates a {@link Condition} that checks if an object is an instance of a class with the same name as the provided
- * class. This is necessary since we are dealing with multiple classloaders which would make a simple instanceof fail
- * all the time
- *
- * @param expectedClass the class that is expected (possibly loaded by a different classloader).
- * @return a {@link Condition}
- */
- private static Condition instanceWithClassName(Class> expectedClass) {
-
- return new Condition<>(it -> it.getClass().getName().equals(expectedClass.getName()), //
- "with class name %s", expectedClass.getName());
- }
-
@Configuration
static class SomeConfiguration {