Skip to content

Upgrade to Jackson 3 #3319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>4.0.0-SNAPSHOT</version>
<version>4.0.0-GH-3292-SNAPSHOT</version>

<name>Spring Data Core</name>
<description>Core Spring concepts underpinning every Spring Data module.</description>
Expand Down Expand Up @@ -70,6 +70,11 @@
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand Down Expand Up @@ -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].

Expand Down
80 changes: 80 additions & 0 deletions src/main/java/org/springframework/data/geo/GeoJacksonModule.java
Original file line number Diff line number Diff line change
@@ -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<Point> points) {}
}
}
2 changes: 2 additions & 0 deletions src/main/java/org/springframework/data/geo/GeoModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<JsonNode> elements = node.iterator();
List<Object> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Loading