diff --git a/spring-web/src/main/java/org/springframework/http/support/JacksonHandlerInstantiator.java b/spring-web/src/main/java/org/springframework/http/support/JacksonHandlerInstantiator.java new file mode 100644 index 00000000000..cf2235fa2c6 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/support/JacksonHandlerInstantiator.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-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.http.support; + +import com.fasterxml.jackson.annotation.ObjectIdGenerator; +import com.fasterxml.jackson.annotation.ObjectIdResolver; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.DeserializationConfig; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.PropertyNamingStrategy; +import tools.jackson.databind.SerializationConfig; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.cfg.HandlerInstantiator; +import tools.jackson.databind.cfg.MapperConfig; +import tools.jackson.databind.deser.ValueInstantiator; +import tools.jackson.databind.introspect.Annotated; +import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.jsontype.TypeResolverBuilder; +import tools.jackson.databind.ser.VirtualBeanPropertyWriter; +import tools.jackson.databind.util.Converter; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.util.Assert; + +/** + * Allows for creating Jackson 3.x ({@link ValueSerializer}, {@link ValueDeserializer}, + * {@link KeyDeserializer}, {@link TypeResolverBuilder}, {@link TypeIdResolver}) + * beans with autowiring against a Spring {@link ApplicationContext}. + * + *

Also overrides all factory methods in {@link HandlerInstantiator}, + * including non-abstract ones for {@link ValueInstantiator}, {@link ObjectIdGenerator}, {@link ObjectIdResolver}, + * {@link PropertyNamingStrategy}, {@link Converter}, {@link VirtualBeanPropertyWriter}. + * + * @author Sebastien Deleuze + * @since 7.0 + * @see ApplicationContext#getAutowireCapableBeanFactory() + * @see tools.jackson.databind.cfg.HandlerInstantiator + */ +public class JacksonHandlerInstantiator extends HandlerInstantiator { + + private final AutowireCapableBeanFactory beanFactory; + + + /** + * Create a new AutowiredHandlerInstantiator for the given BeanFactory. + * @param beanFactory the target BeanFactory + */ + public JacksonHandlerInstantiator(AutowireCapableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory must not be null"); + this.beanFactory = beanFactory; + } + + @Override + @Nullable + public ValueDeserializer deserializerInstance(DeserializationConfig config, Annotated annotated, Class deserClass) { + return (ValueDeserializer) this.beanFactory.createBean(deserClass); + } + + @Override + public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class keyDeserClass) { + return (KeyDeserializer) this.beanFactory.createBean(keyDeserClass); + } + + @Override + public ValueSerializer serializerInstance(SerializationConfig config, Annotated annotated, Class serClass) { + return (ValueSerializer) this.beanFactory.createBean(serClass); + } + + @Override + public TypeResolverBuilder typeResolverBuilderInstance(MapperConfig config, Annotated annotated, Class builderClass) { + return (TypeResolverBuilder) this.beanFactory.createBean(builderClass); + } + + @Override + public TypeIdResolver typeIdResolverInstance(MapperConfig config, Annotated annotated, Class resolverClass) { + return (TypeIdResolver) this.beanFactory.createBean(resolverClass); + } + + @Override + public ValueInstantiator valueInstantiatorInstance(MapperConfig config, Annotated annotated, Class implClass) { + + return (ValueInstantiator) this.beanFactory.createBean(implClass); + } + + @Override + public ObjectIdGenerator objectIdGeneratorInstance(MapperConfig config, Annotated annotated, Class implClass) { + + return (ObjectIdGenerator) this.beanFactory.createBean(implClass); + } + + @Override + public ObjectIdResolver resolverIdGeneratorInstance(MapperConfig config, Annotated annotated, Class implClass) { + + return (ObjectIdResolver) this.beanFactory.createBean(implClass); + } + + @Override + public PropertyNamingStrategy namingStrategyInstance(MapperConfig config, Annotated annotated, Class implClass) { + + return (PropertyNamingStrategy) this.beanFactory.createBean(implClass); + } + + @Override + public Converter converterInstance(MapperConfig config, Annotated annotated, Class implClass) { + return (Converter) this.beanFactory.createBean(implClass); + } + + @Override + public VirtualBeanPropertyWriter virtualPropertyWriterInstance(MapperConfig config, Class implClass) { + return (VirtualBeanPropertyWriter) this.beanFactory.createBean(implClass); + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/support/JacksonHandlerInstantiatorTests.java b/spring-web/src/test/java/org/springframework/http/support/JacksonHandlerInstantiatorTests.java new file mode 100644 index 00000000000..1c58297dc99 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/support/JacksonHandlerInstantiatorTests.java @@ -0,0 +1,275 @@ +/* + * Copyright 2002-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.http.support; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DatabindContext; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.KeyDeserializer; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.annotation.JsonTypeIdResolver; +import tools.jackson.databind.annotation.JsonTypeResolver; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.NamedType; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.jsontype.TypeSerializer; +import tools.jackson.databind.jsontype.impl.StdTypeResolverBuilder; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test class for {@link JacksonHandlerInstantiator}. + * + * @author Sebastien Deleuze + */ +class JacksonHandlerInstantiatorTests { + + private JacksonHandlerInstantiator instantiator; + + private ObjectMapper objectMapper; + + + @BeforeEach + void setup() { + DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor(); + bpp.setBeanFactory(bf); + bf.addBeanPostProcessor(bpp); + bf.registerBeanDefinition("capitalizer", new RootBeanDefinition(Capitalizer.class)); + instantiator = new JacksonHandlerInstantiator(bf); + objectMapper = JsonMapper.builder().handlerInstantiator(instantiator).build(); + } + + + @Test + void autowiredSerializer() { + User user = new User("bob"); + String json = this.objectMapper.writeValueAsString(user); + assertThat(json).isEqualTo("{\"username\":\"BOB\"}"); + } + + @Test + void autowiredDeserializer() { + String json = "{\"username\":\"bob\"}"; + User user = this.objectMapper.readValue(json, User.class); + assertThat(user.getUsername()).isEqualTo("BOB"); + } + + @Test + void autowiredKeyDeserializer() { + String json = "{\"credentials\":{\"bob\":\"admin\"}}"; + SecurityRegistry registry = this.objectMapper.readValue(json, SecurityRegistry.class); + assertThat(registry.getCredentials()).containsKey("BOB"); + assertThat(registry.getCredentials()).doesNotContainKey("bob"); + } + + @Test + void applicationContextAwaretypeResolverBuilder() { + this.objectMapper.writeValueAsString(new Group()); + assertThat(CustomTypeResolverBuilder.isAutowiredFiledInitialized).isTrue(); + } + + @Test + void applicationContextAwareTypeIdResolver() { + this.objectMapper.writeValueAsString(new Group()); + assertThat(CustomTypeIdResolver.isAutowiredFiledInitialized).isTrue(); + } + + + public static class UserDeserializer extends ValueDeserializer { + + @Autowired + private Capitalizer capitalizer; + + @Override + public User deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) { + JsonNode node = jsonParser.readValueAsTree(); + return new User(this.capitalizer.capitalize(node.get("username").asString())); + } + } + + + public static class UserSerializer extends ValueSerializer { + + @Autowired + private Capitalizer capitalizer; + + @Override + public void serialize(User user, JsonGenerator jsonGenerator, SerializationContext serializationContext) { + + jsonGenerator.writeStartObject(); + jsonGenerator.writeStringProperty("username", this.capitalizer.capitalize(user.getUsername())); + jsonGenerator.writeEndObject(); + } + } + + + public static class UpperCaseKeyDeserializer extends KeyDeserializer { + + @Autowired + private Capitalizer capitalizer; + + @Override + public Object deserializeKey(String key, DeserializationContext context) { + return this.capitalizer.capitalize(key); + } + } + + + public static class CustomTypeResolverBuilder extends StdTypeResolverBuilder { + + @Autowired + private Capitalizer capitalizer; + + public static boolean isAutowiredFiledInitialized = false; + + @Override + public TypeSerializer buildTypeSerializer(SerializationContext serializationContext, JavaType baseType, + Collection subtypes) { + + isAutowiredFiledInitialized = (this.capitalizer != null); + return super.buildTypeSerializer(serializationContext, baseType, subtypes); + } + + @Override + public TypeDeserializer buildTypeDeserializer(DeserializationContext deserializationContext, + JavaType baseType, Collection subtypes) { + + return super.buildTypeDeserializer(deserializationContext, baseType, subtypes); + } + } + + + public static class CustomTypeIdResolver implements TypeIdResolver { + + @Autowired + private Capitalizer capitalizer; + + public static boolean isAutowiredFiledInitialized = false; + + public CustomTypeIdResolver() { + } + + @Override + public String idFromValueAndType(DatabindContext ctxt, Object o, Class type) { + return type.getClass().getName(); + } + + @Override + public JsonTypeInfo.Id getMechanism() { + return JsonTypeInfo.Id.CUSTOM; + } + + @Override + public String idFromValue(DatabindContext databindContext, Object value) { + isAutowiredFiledInitialized = (this.capitalizer != null); + return value.getClass().getName(); + } + + @Override + public void init(JavaType type) { + } + + @Override + public String idFromBaseType(DatabindContext ctxt) { + return null; + } + + @Override + public JavaType typeFromId(DatabindContext context, String id) { + return null; + } + + @Override + public String getDescForKnownTypeIds() { + return null; + } + } + + + @JsonDeserialize(using = UserDeserializer.class) + @JsonSerialize(using = UserSerializer.class) + public static class User { + + private String username; + + public User() { + } + + public User(String username) { + this.username = username; + } + + public String getUsername() { return this.username; } + } + + + public static class SecurityRegistry { + + @JsonDeserialize(keyUsing = UpperCaseKeyDeserializer.class) + private Map credentials = new HashMap<>(); + + public void addCredential(String username, String credential) { + this.credentials.put(username, credential); + } + + public Map getCredentials() { + return credentials; + } + } + + + @JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, property = "type") + @JsonTypeResolver(CustomTypeResolverBuilder.class) + @JsonTypeIdResolver(CustomTypeIdResolver.class) + public static class Group { + + public String getType() { + return Group.class.getName(); + } + } + + + public static class Capitalizer { + + public String capitalize(String text) { + return text.toUpperCase(); + } + } + +}