Create spring-boot-jackson-module

This commit is contained in:
Andy Wilkinson
2025-03-14 15:27:21 +00:00
committed by Phillip Webb
parent 57006ee82e
commit 67a4428139
118 changed files with 192 additions and 159 deletions

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2012-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.boot.jackson;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;
/**
* {@link Component @Component} that provides {@link JsonSerializer},
* {@link JsonDeserializer} or {@link KeyDeserializer} implementations to be registered
* with Jackson when {@link JsonComponentModule} is in use. Can be used to annotate
* implementations directly or a class that contains them as inner-classes. For example:
* <pre class="code">
* &#064;JsonComponent
* public class CustomerJsonComponent {
*
* public static class Serializer extends JsonSerializer&lt;Customer&gt; {
*
* // ...
*
* }
*
* public static class Deserializer extends JsonDeserializer&lt;Customer&gt; {
*
* // ...
*
* }
*
* }
*
* </pre>
*
* @see JsonComponentModule
* @since 4.0.0
* @author Phillip Webb
* @author Paul Aly
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface JsonComponent {
/**
* The value may indicate a suggestion for a logical component name, to be turned into
* a Spring bean in case of an autodetected component.
* @return the component name
*/
@AliasFor(annotation = Component.class)
String value() default "";
/**
* The types that are handled by the provided serializer/deserializer. This attribute
* is mandatory for a {@link KeyDeserializer}, as the type cannot be inferred. For a
* {@link JsonSerializer} or {@link JsonDeserializer} it can be used to limit handling
* to a subclasses of type inferred from the generic.
* @return the types that should be handled by the component
* @since 2.2.0
*/
Class<?>[] type() default {};
/**
* The scope under which the serializer/deserializer should be registered with the
* module.
* @return the component's handle type
* @since 2.2.0
*/
Scope scope() default Scope.VALUES;
/**
* The various scopes under which a serializer/deserializer can be registered.
*/
enum Scope {
/**
* A serializer/deserializer for regular value content.
* @see JsonSerializer
* @see JsonDeserializer
*/
VALUES,
/**
* A serializer/deserializer for keys.
* @see JsonSerializer
* @see KeyDeserializer
*/
KEYS
}
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright 2012-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.boot.jackson;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.aot.BeanFactoryInitializationCode;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.jackson.JsonComponent.Scope;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Spring Bean and Jackson {@link Module} to register {@link JsonComponent @JsonComponent}
* annotated beans.
*
* @author Phillip Webb
* @author Paul Aly
* @since 4.0.0
* @see JsonComponent
*/
public class JsonComponentModule extends SimpleModule implements BeanFactoryAware, InitializingBean {
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void afterPropertiesSet() {
registerJsonComponents();
}
public void registerJsonComponents() {
BeanFactory beanFactory = this.beanFactory;
while (beanFactory != null) {
if (beanFactory instanceof ListableBeanFactory listableBeanFactory) {
addJsonBeans(listableBeanFactory);
}
beanFactory = (beanFactory instanceof HierarchicalBeanFactory hierarchicalBeanFactory)
? hierarchicalBeanFactory.getParentBeanFactory() : null;
}
}
private void addJsonBeans(ListableBeanFactory beanFactory) {
Map<String, Object> beans = beanFactory.getBeansWithAnnotation(JsonComponent.class);
for (Object bean : beans.values()) {
addJsonBean(bean);
}
}
private void addJsonBean(Object bean) {
MergedAnnotation<JsonComponent> annotation = MergedAnnotations
.from(bean.getClass(), SearchStrategy.TYPE_HIERARCHY)
.get(JsonComponent.class);
Class<?>[] types = annotation.getClassArray("type");
Scope scope = annotation.getEnum("scope", JsonComponent.Scope.class);
addJsonBean(bean, types, scope);
}
private void addJsonBean(Object bean, Class<?>[] types, Scope scope) {
if (bean instanceof JsonSerializer<?> jsonSerializer) {
addJsonSerializerBean(jsonSerializer, scope, types);
}
else if (bean instanceof JsonDeserializer<?> jsonDeserializer) {
addJsonDeserializerBean(jsonDeserializer, types);
}
else if (bean instanceof KeyDeserializer keyDeserializer) {
addKeyDeserializerBean(keyDeserializer, types);
}
for (Class<?> innerClass : bean.getClass().getDeclaredClasses()) {
if (isSuitableInnerClass(innerClass)) {
Object innerInstance = BeanUtils.instantiateClass(innerClass);
addJsonBean(innerInstance, types, scope);
}
}
}
private static boolean isSuitableInnerClass(Class<?> innerClass) {
return !Modifier.isAbstract(innerClass.getModifiers()) && (JsonSerializer.class.isAssignableFrom(innerClass)
|| JsonDeserializer.class.isAssignableFrom(innerClass)
|| KeyDeserializer.class.isAssignableFrom(innerClass));
}
@SuppressWarnings("unchecked")
private <T> void addJsonSerializerBean(JsonSerializer<T> serializer, JsonComponent.Scope scope, Class<?>[] types) {
Class<T> baseType = (Class<T>) ResolvableType.forClass(JsonSerializer.class, serializer.getClass())
.resolveGeneric();
addBeanToModule(serializer, baseType, types,
(scope == Scope.VALUES) ? this::addSerializer : this::addKeySerializer);
}
@SuppressWarnings("unchecked")
private <T> void addJsonDeserializerBean(JsonDeserializer<T> deserializer, Class<?>[] types) {
Class<T> baseType = (Class<T>) ResolvableType.forClass(JsonDeserializer.class, deserializer.getClass())
.resolveGeneric();
addBeanToModule(deserializer, baseType, types, this::addDeserializer);
}
private void addKeyDeserializerBean(KeyDeserializer deserializer, Class<?>[] types) {
Assert.notEmpty(types, "'types' must not be empty");
addBeanToModule(deserializer, Object.class, types, this::addKeyDeserializer);
}
@SuppressWarnings("unchecked")
private <E, T> void addBeanToModule(E element, Class<T> baseType, Class<?>[] types,
BiConsumer<Class<T>, E> consumer) {
if (ObjectUtils.isEmpty(types)) {
consumer.accept(baseType, element);
return;
}
for (Class<?> type : types) {
Assert.isAssignable(baseType, type);
consumer.accept((Class<T>) type, element);
}
}
static class JsonComponentBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor {
@Override
public BeanFactoryInitializationAotContribution processAheadOfTime(
ConfigurableListableBeanFactory beanFactory) {
String[] jsonComponents = beanFactory.getBeanNamesForAnnotation(JsonComponent.class);
Map<Class<?>, List<Class<?>>> innerComponents = new HashMap<>();
for (String jsonComponent : jsonComponents) {
Class<?> type = beanFactory.getType(jsonComponent, true);
for (Class<?> declaredClass : type.getDeclaredClasses()) {
if (isSuitableInnerClass(declaredClass)) {
innerComponents.computeIfAbsent(type, (t) -> new ArrayList<>()).add(declaredClass);
}
}
}
return innerComponents.isEmpty() ? null : new JsonComponentAotContribution(innerComponents);
}
}
private static final class JsonComponentAotContribution implements BeanFactoryInitializationAotContribution {
private final Map<Class<?>, List<Class<?>>> innerComponents;
private JsonComponentAotContribution(Map<Class<?>, List<Class<?>>> innerComponents) {
this.innerComponents = innerComponents;
}
@Override
public void applyTo(GenerationContext generationContext,
BeanFactoryInitializationCode beanFactoryInitializationCode) {
ReflectionHints reflection = generationContext.getRuntimeHints().reflection();
this.innerComponents.forEach((outer, inners) -> {
reflection.registerType(outer);
inners.forEach((inner) -> reflection.registerType(inner, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
});
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2012-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.boot.jackson;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* Provides a mixin class implementation that registers with Jackson when using
* {@link JsonMixinModule}.
*
* @author Guirong Hu
* @see JsonMixinModule
* @since 4.0.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JsonMixin {
/**
* Alias for the {@link #type()} attribute. Allows for more concise annotation
* declarations e.g.: {@code @JsonMixin(MyType.class)} instead of
* {@code @JsonMixin(type=MyType.class)}.
* @return the mixed-in classes
* @since 2.7.0
*/
@AliasFor("type")
Class<?>[] value() default {};
/**
* The types that are handled by the provided mix-in class. {@link #value()} is an
* alias for (and mutually exclusive with) this attribute.
* @return the mixed-in classes
* @since 2.7.0
*/
@AliasFor("value")
Class<?>[] type() default {};
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-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.boot.jackson;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.module.SimpleModule;
/**
* Spring Bean and Jackson {@link Module} to find and
* {@link SimpleModule#setMixInAnnotation(Class, Class) register}
* {@link JsonMixin @JsonMixin}-annotated classes.
*
* @author Guirong Hu
* @author Stephane Nicoll
* @since 4.0.0
* @see JsonMixin
*/
public class JsonMixinModule extends SimpleModule {
/**
* Register the specified {@link JsonMixinModuleEntries entries}.
* @param entries the entries to register to this instance
* @param classLoader the classloader to use
*/
public void registerEntries(JsonMixinModuleEntries entries, ClassLoader classLoader) {
entries.doWithEntry(classLoader, this::setMixInAnnotation);
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2012-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.boot.jackson;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Provide the mapping of json mixin class to consider.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
public final class JsonMixinModuleEntries {
private final Map<Object, Object> entries;
private JsonMixinModuleEntries(Builder builder) {
this.entries = new LinkedHashMap<>(builder.entries);
}
/**
* Create an instance using the specified {@link Builder}.
* @param mixins a consumer of the builder
* @return an instance with the state of the customized builder.
*/
public static JsonMixinModuleEntries create(Consumer<Builder> mixins) {
Builder builder = new Builder();
mixins.accept(builder);
return builder.build();
}
/**
* Scan the classpath for {@link JsonMixin @JsonMixin} in the specified
* {@code basePackages}.
* @param context the application context to use
* @param basePackages the base packages to consider
* @return an instance with the result of the scanning
*/
public static JsonMixinModuleEntries scan(ApplicationContext context, Collection<String> basePackages) {
return JsonMixinModuleEntries.create((builder) -> {
if (ObjectUtils.isEmpty(basePackages)) {
return;
}
JsonMixinComponentScanner scanner = new JsonMixinComponentScanner();
scanner.setEnvironment(context.getEnvironment());
scanner.setResourceLoader(context);
for (String basePackage : basePackages) {
if (StringUtils.hasText(basePackage)) {
for (BeanDefinition candidate : scanner.findCandidateComponents(basePackage)) {
Class<?> mixinClass = ClassUtils.resolveClassName(candidate.getBeanClassName(),
context.getClassLoader());
registerMixinClass(builder, mixinClass);
}
}
}
});
}
private static void registerMixinClass(Builder builder, Class<?> mixinClass) {
MergedAnnotation<JsonMixin> annotation = MergedAnnotations
.from(mixinClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
.get(JsonMixin.class);
Class<?>[] types = annotation.getClassArray("type");
Assert.state(!ObjectUtils.isEmpty(types),
() -> "@JsonMixin annotation on class '" + mixinClass.getName() + "' does not specify any types");
for (Class<?> type : types) {
builder.and(type, mixinClass);
}
}
/**
* Perform an action on each entry defined by this instance. If a class needs to be
* resolved from its class name, the specified {@link ClassLoader} is used.
* @param classLoader the classloader to use to resolve class name if necessary
* @param action the action to invoke on each type to mixin class entry
*/
public void doWithEntry(ClassLoader classLoader, BiConsumer<Class<?>, Class<?>> action) {
this.entries.forEach((type, mixin) -> action.accept(resolveClassNameIfNecessary(type, classLoader),
resolveClassNameIfNecessary(mixin, classLoader)));
}
private Class<?> resolveClassNameIfNecessary(Object nameOrType, ClassLoader classLoader) {
return (nameOrType instanceof Class<?> type) ? type
: ClassUtils.resolveClassName((String) nameOrType, classLoader);
}
/**
* Builder for {@link JsonMixinModuleEntries}.
*/
public static class Builder {
private final Map<Object, Object> entries;
Builder() {
this.entries = new LinkedHashMap<>();
}
/**
* Add a mapping for the specified class names.
* @param typeClassName the type class name
* @param mixinClassName the mixin class name
* @return {@code this}, to facilitate method chaining
*/
public Builder and(String typeClassName, String mixinClassName) {
this.entries.put(typeClassName, mixinClassName);
return this;
}
/**
* Add a mapping for the specified classes.
* @param type the type class
* @param mixinClass the mixin class
* @return {@code this}, to facilitate method chaining
*/
public Builder and(Class<?> type, Class<?> mixinClass) {
this.entries.put(type, mixinClass);
return this;
}
JsonMixinModuleEntries build() {
return new JsonMixinModuleEntries(this);
}
}
static class JsonMixinComponentScanner extends ClassPathScanningCandidateComponentProvider {
JsonMixinComponentScanner() {
addIncludeFilter(new AnnotationTypeFilter(JsonMixin.class));
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return true;
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2012-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.boot.jackson;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.lang.model.element.Modifier;
import org.springframework.aot.generate.AccessControl;
import org.springframework.aot.generate.GeneratedMethod;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.beans.factory.aot.BeanRegistrationCode;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments;
import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock;
/**
* {@link BeanRegistrationAotProcessor} that replaces any {@link JsonMixinModuleEntries}
* by an hard-coded equivalent. This has the effect of disabling scanning at runtime.
*
* @author Stephane Nicoll
*/
class JsonMixinModuleEntriesBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor {
@Override
public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) {
if (registeredBean.getBeanClass().equals(JsonMixinModuleEntries.class)) {
return BeanRegistrationAotContribution
.withCustomCodeFragments((codeFragments) -> new AotContribution(codeFragments, registeredBean));
}
return null;
}
static class AotContribution extends BeanRegistrationCodeFragmentsDecorator {
private static final Class<?> BEAN_TYPE = JsonMixinModuleEntries.class;
private final RegisteredBean registeredBean;
private final ClassLoader classLoader;
AotContribution(BeanRegistrationCodeFragments delegate, RegisteredBean registeredBean) {
super(delegate);
this.registeredBean = registeredBean;
this.classLoader = registeredBean.getBeanFactory().getBeanClassLoader();
}
@Override
public ClassName getTarget(RegisteredBean registeredBean) {
return ClassName.get(BEAN_TYPE);
}
@Override
public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext,
BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) {
JsonMixinModuleEntries entries = this.registeredBean.getBeanFactory()
.getBean(this.registeredBean.getBeanName(), JsonMixinModuleEntries.class);
contributeHints(generationContext.getRuntimeHints(), entries);
GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("getInstance", (method) -> {
method.addJavadoc("Get the bean instance for '$L'.", this.registeredBean.getBeanName());
method.addModifiers(Modifier.PRIVATE, Modifier.STATIC);
method.returns(BEAN_TYPE);
CodeBlock.Builder code = CodeBlock.builder();
code.add("return $T.create(", JsonMixinModuleEntries.class).beginControlFlow("(mixins) ->");
entries.doWithEntry(this.classLoader, (type, mixin) -> addEntryCode(code, type, mixin));
code.endControlFlow(")");
method.addCode(code.build());
});
return generatedMethod.toMethodReference().toCodeBlock();
}
private void addEntryCode(CodeBlock.Builder code, Class<?> type, Class<?> mixin) {
AccessControl accessForTypes = AccessControl.lowest(AccessControl.forClass(type),
AccessControl.forClass(mixin));
if (accessForTypes.isPublic()) {
code.addStatement("$L.and($T.class, $T.class)", "mixins", type, mixin);
}
else {
code.addStatement("$L.and($S, $S)", "mixins", type.getName(), mixin.getName());
}
}
private void contributeHints(RuntimeHints runtimeHints, JsonMixinModuleEntries entries) {
Set<Class<?>> mixins = new LinkedHashSet<>();
entries.doWithEntry(this.classLoader, (type, mixin) -> mixins.add(mixin));
new BindingReflectionHintsRegistrar().registerReflectionHints(runtimeHints.reflection(),
mixins.toArray(Class<?>[]::new));
}
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2012-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.boot.jackson;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.function.Function;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import org.springframework.util.Assert;
/**
* Helper base class for {@link JsonDeserializer} implementations that deserialize
* objects.
*
* @param <T> the supported object type
* @author Phillip Webb
* @since 4.0.0
* @see JsonObjectSerializer
*/
public abstract class JsonObjectDeserializer<T> extends com.fasterxml.jackson.databind.JsonDeserializer<T> {
@Override
public final T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
try {
ObjectCodec codec = jp.getCodec();
JsonNode tree = codec.readTree(jp);
return deserializeObject(jp, ctxt, codec, tree);
}
catch (Exception ex) {
if (ex instanceof IOException ioException) {
throw ioException;
}
throw new JsonMappingException(jp, "Object deserialize error", ex);
}
}
/**
* Deserialize JSON content into the value type this serializer handles.
* @param jsonParser the source parser used for reading JSON content
* @param context context that can be used to access information about this
* deserialization activity
* @param codec the {@link ObjectCodec} associated with the parser
* @param tree deserialized JSON content as tree expressed using set of
* {@link TreeNode} instances
* @return the deserialized object
* @throws IOException on error
* @see #deserialize(JsonParser, DeserializationContext)
*/
protected abstract T deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
JsonNode tree) throws IOException;
/**
* Helper method to extract a value from the given {@code jsonNode} or return
* {@code null} when the node itself is {@code null}.
* @param jsonNode the source node (may be {@code null})
* @param type the data type. May be {@link String}, {@link Boolean}, {@link Long},
* {@link Integer}, {@link Short}, {@link Double}, {@link Float}, {@link BigDecimal}
* or {@link BigInteger}.
* @param <D> the data type requested
* @param <R> the result type
* @param mapper a mapper to convert the value when it is not {@code null}
* @return the node value or {@code null}
* @since 3.4.0
*/
protected final <D, R> R nullSafeValue(JsonNode jsonNode, Class<D> type, Function<D, R> mapper) {
D value = nullSafeValue(jsonNode, type);
return (value != null) ? mapper.apply(value) : null;
}
/**
* Helper method to extract a value from the given {@code jsonNode} or return
* {@code null} when the node itself is {@code null}.
* @param jsonNode the source node (may be {@code null})
* @param type the data type. May be {@link String}, {@link Boolean}, {@link Long},
* {@link Integer}, {@link Short}, {@link Double}, {@link Float}, {@link BigDecimal}
* or {@link BigInteger}.
* @param <D> the data type requested
* @return the node value or {@code null}
*/
@SuppressWarnings({ "unchecked" })
protected final <D> D nullSafeValue(JsonNode jsonNode, Class<D> type) {
Assert.notNull(type, "'type' must not be null");
if (jsonNode == null) {
return null;
}
if (type == String.class) {
return (D) jsonNode.textValue();
}
if (type == Boolean.class) {
return (D) Boolean.valueOf(jsonNode.booleanValue());
}
if (type == Long.class) {
return (D) Long.valueOf(jsonNode.longValue());
}
if (type == Integer.class) {
return (D) Integer.valueOf(jsonNode.intValue());
}
if (type == Short.class) {
return (D) Short.valueOf(jsonNode.shortValue());
}
if (type == Double.class) {
return (D) Double.valueOf(jsonNode.doubleValue());
}
if (type == Float.class) {
return (D) Float.valueOf(jsonNode.floatValue());
}
if (type == BigDecimal.class) {
return (D) jsonNode.decimalValue();
}
if (type == BigInteger.class) {
return (D) jsonNode.bigIntegerValue();
}
throw new IllegalArgumentException("Unsupported value type " + type.getName());
}
/**
* Helper method to return a {@link JsonNode} from the tree.
* @param tree the source tree
* @param fieldName the field name to extract
* @return the {@link JsonNode}
*/
protected final JsonNode getRequiredNode(JsonNode tree, String fieldName) {
Assert.notNull(tree, "'tree' must not be null");
JsonNode node = tree.get(fieldName);
Assert.state(node != null && !(node instanceof NullNode), () -> "Missing JSON field '" + fieldName + "'");
return node;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2012-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.boot.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
/**
* Helper base class for {@link JsonSerializer} implementations that serialize objects.
*
* @param <T> the supported object type
* @author Phillip Webb
* @since 4.0.0
* @see JsonObjectDeserializer
*/
public abstract class JsonObjectSerializer<T> extends JsonSerializer<T> {
@Override
public final void serialize(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
try {
jgen.writeStartObject();
serializeObject(value, jgen, provider);
jgen.writeEndObject();
}
catch (Exception ex) {
if (ex instanceof IOException ioException) {
throw ioException;
}
throw new JsonMappingException(jgen, "Object serialize error", ex);
}
}
/**
* Serialize JSON content into the value type this serializer handles.
* @param value the source value
* @param jgen the JSON generator
* @param provider the serializer provider
* @throws IOException on error
*/
protected abstract void serializeObject(T value, JsonGenerator jgen, SerializerProvider provider)
throws IOException;
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-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.boot.jackson.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
/**
* Callback interface that can be implemented by beans wishing to further customize the
* {@link ObjectMapper} through {@link Jackson2ObjectMapperBuilder} retaining its default
* auto-configuration.
*
* @author Grzegorz Poznachowski
* @since 4.0.0
*/
@FunctionalInterface
@SuppressWarnings("removal")
public interface Jackson2ObjectMapperBuilderCustomizer {
/**
* Customize the JacksonObjectMapperBuilder.
* @param jacksonObjectMapperBuilder the JacksonObjectMapperBuilder to customize
*/
void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder);
}

View File

@@ -0,0 +1,380 @@
/*
* Copyright 2012-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.boot.jackson.autoconfigure;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.ConstructorDetector;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jackson.JsonComponentModule;
import org.springframework.boot.jackson.JsonMixinModule;
import org.springframework.boot.jackson.JsonMixinModuleEntries;
import org.springframework.boot.jackson.autoconfigure.JacksonProperties.ConstructorDetectorStrategy;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Scope;
import org.springframework.core.Ordered;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* Auto configuration for Jackson. The following auto-configuration will get applied:
* <ul>
* <li>an {@link ObjectMapper} in case none is already configured.</li>
* <li>a {@link Jackson2ObjectMapperBuilder} in case none is already configured.</li>
* <li>auto-registration for all {@link Module} beans with all {@link ObjectMapper} beans
* (including the defaulted ones).</li>
* </ul>
*
* @author Oliver Gierke
* @author Andy Wilkinson
* @author Marcel Overdijk
* @author Sebastien Deleuze
* @author Johannes Edmeier
* @author Phillip Webb
* @author Eddú Meléndez
* @author Ralf Ueberfuhr
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
@SuppressWarnings("removal")
public class JacksonAutoConfiguration {
private static final Map<?, Boolean> FEATURE_DEFAULTS;
static {
Map<Object, Boolean> featureDefaults = new HashMap<>();
featureDefaults.put(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
featureDefaults.put(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
FEATURE_DEFAULTS = Collections.unmodifiableMap(featureDefaults);
}
@Bean
public JsonComponentModule jsonComponentModule() {
return new JsonComponentModule();
}
@Configuration(proxyBeanMethods = false)
static class JacksonMixinConfiguration {
@Bean
static JsonMixinModuleEntries jsonMixinModuleEntries(ApplicationContext context) {
List<String> packages = AutoConfigurationPackages.has(context) ? AutoConfigurationPackages.get(context)
: Collections.emptyList();
return JsonMixinModuleEntries.scan(context, packages);
}
@Bean
JsonMixinModule jsonMixinModule(ApplicationContext context, JsonMixinModuleEntries entries) {
JsonMixinModule jsonMixinModule = new JsonMixinModule();
jsonMixinModule.registerEntries(entries, context.getClassLoader());
return jsonMixinModule;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ParameterNamesModule.class)
static class ParameterNamesModuleConfiguration {
@Bean
@ConditionalOnMissingBean
ParameterNamesModule parameterNamesModule() {
return new ParameterNamesModule(JsonCreator.Mode.DEFAULT);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperBuilderConfiguration {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
customize(builder, customizers);
return builder;
}
private void customize(Jackson2ObjectMapperBuilder builder,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
@EnableConfigurationProperties(JacksonProperties.class)
static class Jackson2ObjectMapperBuilderCustomizerConfiguration {
@Bean
StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(
JacksonProperties jacksonProperties, ObjectProvider<Module> modules) {
return new StandardJackson2ObjectMapperBuilderCustomizer(jacksonProperties, modules.stream().toList());
}
static final class StandardJackson2ObjectMapperBuilderCustomizer
implements Jackson2ObjectMapperBuilderCustomizer, Ordered {
private final JacksonProperties jacksonProperties;
private final Collection<Module> modules;
StandardJackson2ObjectMapperBuilderCustomizer(JacksonProperties jacksonProperties,
Collection<Module> modules) {
this.jacksonProperties = jacksonProperties;
this.modules = modules;
}
@Override
public int getOrder() {
return 0;
}
@Override
public void customize(Jackson2ObjectMapperBuilder builder) {
if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
}
if (this.jacksonProperties.getTimeZone() != null) {
builder.timeZone(this.jacksonProperties.getTimeZone());
}
configureFeatures(builder, FEATURE_DEFAULTS);
configureVisibility(builder, this.jacksonProperties.getVisibility());
configureFeatures(builder, this.jacksonProperties.getDeserialization());
configureFeatures(builder, this.jacksonProperties.getSerialization());
configureFeatures(builder, this.jacksonProperties.getMapper());
configureFeatures(builder, this.jacksonProperties.getParser());
configureFeatures(builder, this.jacksonProperties.getGenerator());
configureFeatures(builder, this.jacksonProperties.getDatatype().getEnum());
configureFeatures(builder, this.jacksonProperties.getDatatype().getJsonNode());
configureDateFormat(builder);
configurePropertyNamingStrategy(builder);
configureModules(builder);
configureLocale(builder);
configureDefaultLeniency(builder);
configureConstructorDetector(builder);
}
private void configureFeatures(Jackson2ObjectMapperBuilder builder, Map<?, Boolean> features) {
features.forEach((feature, value) -> {
if (value != null) {
if (value) {
builder.featuresToEnable(feature);
}
else {
builder.featuresToDisable(feature);
}
}
});
}
private void configureVisibility(Jackson2ObjectMapperBuilder builder,
Map<PropertyAccessor, JsonAutoDetect.Visibility> visibilities) {
visibilities.forEach(builder::visibility);
}
private void configureDateFormat(Jackson2ObjectMapperBuilder builder) {
// We support a fully qualified class name extending DateFormat or a date
// pattern string value
String dateFormat = this.jacksonProperties.getDateFormat();
if (dateFormat != null) {
try {
Class<?> dateFormatClass = ClassUtils.forName(dateFormat, null);
builder.dateFormat((DateFormat) BeanUtils.instantiateClass(dateFormatClass));
}
catch (ClassNotFoundException ex) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
// Since Jackson 2.6.3 we always need to set a TimeZone (see
// gh-4170). If none in our properties fallback to the Jackson's
// default
TimeZone timeZone = this.jacksonProperties.getTimeZone();
if (timeZone == null) {
timeZone = new ObjectMapper().getSerializationConfig().getTimeZone();
}
simpleDateFormat.setTimeZone(timeZone);
builder.dateFormat(simpleDateFormat);
}
}
}
private void configurePropertyNamingStrategy(Jackson2ObjectMapperBuilder builder) {
// We support a fully qualified class name extending Jackson's
// PropertyNamingStrategy or a string value corresponding to the constant
// names in PropertyNamingStrategy which hold default provided
// implementations
String strategy = this.jacksonProperties.getPropertyNamingStrategy();
if (strategy != null) {
try {
configurePropertyNamingStrategyClass(builder, ClassUtils.forName(strategy, null));
}
catch (ClassNotFoundException ex) {
configurePropertyNamingStrategyField(builder, strategy);
}
}
}
private void configurePropertyNamingStrategyClass(Jackson2ObjectMapperBuilder builder,
Class<?> propertyNamingStrategyClass) {
builder.propertyNamingStrategy(
(PropertyNamingStrategy) BeanUtils.instantiateClass(propertyNamingStrategyClass));
}
private void configurePropertyNamingStrategyField(Jackson2ObjectMapperBuilder builder, String fieldName) {
// Find the field (this way we automatically support new constants
// that may be added by Jackson in the future)
Field field = findPropertyNamingStrategyField(fieldName);
Assert.state(field != null, () -> "Constant named '" + fieldName + "' not found");
try {
builder.propertyNamingStrategy((PropertyNamingStrategy) field.get(null));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private Field findPropertyNamingStrategyField(String fieldName) {
return ReflectionUtils.findField(com.fasterxml.jackson.databind.PropertyNamingStrategies.class,
fieldName, PropertyNamingStrategy.class);
}
private void configureModules(Jackson2ObjectMapperBuilder builder) {
builder.modulesToInstall((modules) -> modules.addAll(this.modules));
}
private void configureLocale(Jackson2ObjectMapperBuilder builder) {
Locale locale = this.jacksonProperties.getLocale();
if (locale != null) {
builder.locale(locale);
}
}
private void configureDefaultLeniency(Jackson2ObjectMapperBuilder builder) {
Boolean defaultLeniency = this.jacksonProperties.getDefaultLeniency();
if (defaultLeniency != null) {
builder.postConfigurer((objectMapper) -> objectMapper.setDefaultLeniency(defaultLeniency));
}
}
private void configureConstructorDetector(Jackson2ObjectMapperBuilder builder) {
ConstructorDetectorStrategy strategy = this.jacksonProperties.getConstructorDetector();
if (strategy != null) {
builder.postConfigurer((objectMapper) -> {
switch (strategy) {
case USE_PROPERTIES_BASED ->
objectMapper.setConstructorDetector(ConstructorDetector.USE_PROPERTIES_BASED);
case USE_DELEGATING ->
objectMapper.setConstructorDetector(ConstructorDetector.USE_DELEGATING);
case EXPLICIT_ONLY ->
objectMapper.setConstructorDetector(ConstructorDetector.EXPLICIT_ONLY);
default -> objectMapper.setConstructorDetector(ConstructorDetector.DEFAULT);
}
});
}
}
}
}
static class JacksonAutoConfigurationRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
if (ClassUtils.isPresent("com.fasterxml.jackson.databind.PropertyNamingStrategy", classLoader)) {
registerPropertyNamingStrategyHints(hints.reflection());
}
}
/**
* Register hints for the {@code configurePropertyNamingStrategyField} method to
* use.
* @param hints reflection hints
*/
private void registerPropertyNamingStrategyHints(ReflectionHints hints) {
registerPropertyNamingStrategyHints(hints, PropertyNamingStrategies.class);
}
private void registerPropertyNamingStrategyHints(ReflectionHints hints, Class<?> type) {
Stream.of(type.getDeclaredFields())
.filter(this::isPropertyNamingStrategyField)
.forEach(hints::registerField);
}
private boolean isPropertyNamingStrategyField(Field candidate) {
return ReflectionUtils.isPublicStaticFinal(candidate)
&& candidate.getType().isAssignableFrom(PropertyNamingStrategy.class);
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-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.boot.jackson.autoconfigure;
import org.springframework.boot.autoconfigure.preinitialize.BackgroundPreinitializer;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
/**
* {@link JacksonBackgroundPreinitializer} for Jackson.
*
* @author Phillip Webb
*/
@SuppressWarnings("removal")
final class JacksonBackgroundPreinitializer implements BackgroundPreinitializer {
@Override
public void preinitialize() throws Exception {
Jackson2ObjectMapperBuilder.json().build();
}
}

View File

@@ -0,0 +1,253 @@
/*
* Copyright 2012-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.boot.jackson.autoconfigure;
import java.util.EnumMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.EnumFeature;
import com.fasterxml.jackson.databind.cfg.JsonNodeFeature;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties to configure Jackson.
*
* @author Andy Wilkinson
* @author Marcel Overdijk
* @author Johannes Edmeier
* @author Eddú Meléndez
* @since 4.0.0
*/
@ConfigurationProperties("spring.jackson")
public class JacksonProperties {
/**
* Date format string or a fully-qualified date format class name. For instance,
* 'yyyy-MM-dd HH:mm:ss'.
*/
private String dateFormat;
/**
* One of the constants on Jackson's PropertyNamingStrategies. Can also be a
* fully-qualified class name of a PropertyNamingStrategy implementation.
*/
private String propertyNamingStrategy;
/**
* Jackson visibility thresholds that can be used to limit which methods (and fields)
* are auto-detected.
*/
private final Map<PropertyAccessor, JsonAutoDetect.Visibility> visibility = new EnumMap<>(PropertyAccessor.class);
/**
* Jackson on/off features that affect the way Java objects are serialized.
*/
private final Map<SerializationFeature, Boolean> serialization = new EnumMap<>(SerializationFeature.class);
/**
* Jackson on/off features that affect the way Java objects are deserialized.
*/
private final Map<DeserializationFeature, Boolean> deserialization = new EnumMap<>(DeserializationFeature.class);
/**
* Jackson general purpose on/off features.
*/
private final Map<MapperFeature, Boolean> mapper = new EnumMap<>(MapperFeature.class);
/**
* Jackson on/off features for parsers.
*/
private final Map<JsonParser.Feature, Boolean> parser = new EnumMap<>(JsonParser.Feature.class);
/**
* Jackson on/off features for generators.
*/
private final Map<JsonGenerator.Feature, Boolean> generator = new EnumMap<>(JsonGenerator.Feature.class);
/**
* Controls the inclusion of properties during serialization. Configured with one of
* the values in Jackson's JsonInclude.Include enumeration.
*/
private JsonInclude.Include defaultPropertyInclusion;
/**
* Global default setting (if any) for leniency.
*/
private Boolean defaultLeniency;
/**
* Strategy to use to auto-detect constructor, and in particular behavior with
* single-argument constructors.
*/
private ConstructorDetectorStrategy constructorDetector;
/**
* Time zone used when formatting dates. For instance, "America/Los_Angeles" or
* "GMT+10".
*/
private TimeZone timeZone = null;
/**
* Locale used for formatting.
*/
private Locale locale;
private final Datatype datatype = new Datatype();
public String getDateFormat() {
return this.dateFormat;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
public String getPropertyNamingStrategy() {
return this.propertyNamingStrategy;
}
public void setPropertyNamingStrategy(String propertyNamingStrategy) {
this.propertyNamingStrategy = propertyNamingStrategy;
}
public Map<PropertyAccessor, JsonAutoDetect.Visibility> getVisibility() {
return this.visibility;
}
public Map<SerializationFeature, Boolean> getSerialization() {
return this.serialization;
}
public Map<DeserializationFeature, Boolean> getDeserialization() {
return this.deserialization;
}
public Map<MapperFeature, Boolean> getMapper() {
return this.mapper;
}
public Map<JsonParser.Feature, Boolean> getParser() {
return this.parser;
}
public Map<JsonGenerator.Feature, Boolean> getGenerator() {
return this.generator;
}
public JsonInclude.Include getDefaultPropertyInclusion() {
return this.defaultPropertyInclusion;
}
public void setDefaultPropertyInclusion(JsonInclude.Include defaultPropertyInclusion) {
this.defaultPropertyInclusion = defaultPropertyInclusion;
}
public Boolean getDefaultLeniency() {
return this.defaultLeniency;
}
public void setDefaultLeniency(Boolean defaultLeniency) {
this.defaultLeniency = defaultLeniency;
}
public ConstructorDetectorStrategy getConstructorDetector() {
return this.constructorDetector;
}
public void setConstructorDetector(ConstructorDetectorStrategy constructorDetector) {
this.constructorDetector = constructorDetector;
}
public TimeZone getTimeZone() {
return this.timeZone;
}
public void setTimeZone(TimeZone timeZone) {
this.timeZone = timeZone;
}
public Locale getLocale() {
return this.locale;
}
public void setLocale(Locale locale) {
this.locale = locale;
}
public Datatype getDatatype() {
return this.datatype;
}
public enum ConstructorDetectorStrategy {
/**
* Use heuristics to see if "properties" mode is to be used.
*/
DEFAULT,
/**
* Assume "properties" mode if not explicitly annotated otherwise.
*/
USE_PROPERTIES_BASED,
/**
* Assume "delegating" mode if not explicitly annotated otherwise.
*/
USE_DELEGATING,
/**
* Refuse to decide implicit mode and instead throw an InvalidDefinitionException
* for ambiguous cases.
*/
EXPLICIT_ONLY
}
public static class Datatype {
/**
* Jackson on/off features for enums.
*/
private final Map<EnumFeature, Boolean> enumFeatures = new EnumMap<>(EnumFeature.class);
/**
* Jackson on/off features for JsonNodes.
*/
private final Map<JsonNodeFeature, Boolean> jsonNode = new EnumMap<>(JsonNodeFeature.class);
public Map<EnumFeature, Boolean> getEnum() {
return this.enumFeatures;
}
public Map<JsonNodeFeature, Boolean> getJsonNode() {
return this.jsonNode;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-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.
*/
/**
* Auto-configuration for Jackson.
*/
package org.springframework.boot.jackson.autoconfigure;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2019 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.
*/
/**
* Custom enhancements and support for the Jackson project.
*/
package org.springframework.boot.jackson;

View File

@@ -0,0 +1,20 @@
{
"properties": [
{
"name": "spring.jackson.constructor-detector",
"defaultValue": "default"
},
{
"name": "spring.jackson.datatype.enum",
"description": "Jackson on/off features for enums."
},
{
"name": "spring.jackson.joda-date-time-format",
"type": "java.lang.String",
"description": "Joda date time format string. If not configured, \"date-format\" is used as a fallback if it is configured with a format string.",
"deprecation": {
"level": "error"
}
}
]
}

View File

@@ -0,0 +1,3 @@
# Background Preinitializers
org.springframework.boot.autoconfigure.preinitialize.BackgroundPreinitializer=\
org.springframework.boot.jackson.autoconfigure.JacksonBackgroundPreinitializer

View File

@@ -0,0 +1,8 @@
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration$JacksonAutoConfigurationRuntimeHints
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
org.springframework.boot.jackson.JsonComponentModule$JsonComponentBeanFactoryInitializationAotProcessor
org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\
org.springframework.boot.jackson.JsonMixinModuleEntriesBeanRegistrationAotProcessor

View File

@@ -0,0 +1 @@
org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration

View File

@@ -0,0 +1,245 @@
/*
* Copyright 2012-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.boot.jackson;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.jackson.JsonComponentModule.JsonComponentBeanFactoryInitializationAotProcessor;
import org.springframework.boot.jackson.JsonComponentModuleTests.ComponentWithInnerAbstractClass.AbstractSerializer;
import org.springframework.boot.jackson.JsonComponentModuleTests.ComponentWithInnerAbstractClass.ConcreteSerializer;
import org.springframework.boot.jackson.JsonComponentModuleTests.ComponentWithInnerAbstractClass.NotSuitable;
import org.springframework.boot.jackson.types.Name;
import org.springframework.boot.jackson.types.NameAndAge;
import org.springframework.boot.jackson.types.NameAndCareer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link JsonComponentModule}.
*
* @author Phillip Webb
* @author Vladimir Tsanev
* @author Paul Aly
*/
class JsonComponentModuleTests {
private AnnotationConfigApplicationContext context;
@AfterEach
void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test
void moduleShouldRegisterSerializers() throws Exception {
load(OnlySerializer.class);
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
assertSerialize(module);
}
@Test
void moduleShouldRegisterDeserializers() throws Exception {
load(OnlyDeserializer.class);
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
assertDeserialize(module);
}
@Test
void moduleShouldRegisterInnerClasses() throws Exception {
load(NameAndAgeJsonComponent.class);
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
assertSerialize(module);
assertDeserialize(module);
}
@Test
void moduleShouldAllowInnerAbstractClasses() throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JsonComponentModule.class,
ComponentWithInnerAbstractClass.class);
JsonComponentModule module = context.getBean(JsonComponentModule.class);
assertSerialize(module);
context.close();
}
@Test
void moduleShouldRegisterKeySerializers() throws Exception {
load(OnlyKeySerializer.class);
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
assertKeySerialize(module);
}
@Test
void moduleShouldRegisterKeyDeserializers() throws Exception {
load(OnlyKeyDeserializer.class);
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
assertKeyDeserialize(module);
}
@Test
void moduleShouldRegisterInnerClassesForKeyHandlers() throws Exception {
load(NameAndAgeJsonKeyComponent.class);
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
assertKeySerialize(module);
assertKeyDeserialize(module);
}
@Test
void moduleShouldRegisterOnlyForSpecifiedClasses() throws Exception {
load(NameAndCareerJsonComponent.class);
JsonComponentModule module = this.context.getBean(JsonComponentModule.class);
assertSerialize(module, new NameAndCareer("spring", "developer"), "{\"name\":\"spring\"}");
assertSerialize(module);
assertDeserializeForSpecifiedClasses(module);
}
@Test
void aotContributionRegistersReflectionHintsForSuitableInnerClasses() {
load(ComponentWithInnerAbstractClass.class);
ConfigurableListableBeanFactory beanFactory = this.context.getBeanFactory();
BeanFactoryInitializationAotContribution contribution = new JsonComponentBeanFactoryInitializationAotProcessor()
.processAheadOfTime(beanFactory);
TestGenerationContext generationContext = new TestGenerationContext();
contribution.applyTo(generationContext, null);
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
assertThat(RuntimeHintsPredicates.reflection().onType(ComponentWithInnerAbstractClass.class))
.accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection()
.onType(ConcreteSerializer.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection()
.onType(AbstractSerializer.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
.negate()).accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection()
.onType(NotSuitable.class)
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
.negate()).accepts(runtimeHints);
}
private void load(Class<?>... configs) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(configs);
context.register(JsonComponentModule.class);
context.refresh();
this.context = context;
}
private void assertSerialize(Module module, Name value, String expectedJson) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
String json = mapper.writeValueAsString(value);
assertThat(json).isEqualToIgnoringWhitespace(expectedJson);
}
private void assertSerialize(Module module) throws Exception {
assertSerialize(module, new NameAndAge("spring", 100), "{\"name\":\"spring\",\"age\":100}");
}
private void assertDeserialize(Module module) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
NameAndAge nameAndAge = mapper.readValue("{\"name\":\"spring\",\"age\":100}", NameAndAge.class);
assertThat(nameAndAge.getName()).isEqualTo("spring");
assertThat(nameAndAge.getAge()).isEqualTo(100);
}
private void assertDeserializeForSpecifiedClasses(JsonComponentModule module) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
assertThatExceptionOfType(JsonMappingException.class)
.isThrownBy(() -> mapper.readValue("{\"name\":\"spring\",\"age\":100}", NameAndAge.class));
NameAndCareer nameAndCareer = mapper.readValue("{\"name\":\"spring\",\"career\":\"developer\"}",
NameAndCareer.class);
assertThat(nameAndCareer.getName()).isEqualTo("spring");
assertThat(nameAndCareer.getCareer()).isEqualTo("developer");
}
private void assertKeySerialize(Module module) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
Map<NameAndAge, Boolean> map = new HashMap<>();
map.put(new NameAndAge("spring", 100), true);
String json = mapper.writeValueAsString(map);
assertThat(json).isEqualToIgnoringWhitespace("{\"spring is 100\": true}");
}
private void assertKeyDeserialize(Module module) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
TypeReference<Map<NameAndAge, Boolean>> typeRef = new TypeReference<>() {
};
Map<NameAndAge, Boolean> map = mapper.readValue("{\"spring is 100\": true}", typeRef);
assertThat(map).containsEntry(new NameAndAge("spring", 100), true);
}
@JsonComponent
static class OnlySerializer extends NameAndAgeJsonComponent.Serializer {
}
@JsonComponent
static class OnlyDeserializer extends NameAndAgeJsonComponent.Deserializer {
}
@JsonComponent
static class ComponentWithInnerAbstractClass {
abstract static class AbstractSerializer extends NameAndAgeJsonComponent.Serializer {
}
static class ConcreteSerializer extends AbstractSerializer {
}
static class NotSuitable {
}
}
@JsonComponent(scope = JsonComponent.Scope.KEYS)
static class OnlyKeySerializer extends NameAndAgeJsonKeyComponent.Serializer {
}
@JsonComponent(scope = JsonComponent.Scope.KEYS, type = NameAndAge.class)
static class OnlyKeyDeserializer extends NameAndAgeJsonKeyComponent.Deserializer {
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2012-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.boot.jackson;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.BiConsumer;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.boot.jackson.scan.a.RenameMixInClass;
import org.springframework.boot.jackson.types.Name;
import org.springframework.boot.jackson.types.NameAndAge;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.aot.ApplicationContextAotGenerator;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
import org.springframework.core.test.tools.Compiled;
import org.springframework.core.test.tools.TestCompiler;
import org.springframework.javapoet.ClassName;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
/**
* Tests for {@link JsonMixinModuleEntriesBeanRegistrationAotProcessor}.
*
* @author Stephane Nicoll
*/
@CompileWithForkedClassLoader
class JsonMixinModuleEntriesBeanRegistrationAotProcessorTests {
private final TestGenerationContext generationContext = new TestGenerationContext();
private final GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
@Test
void processAheadOfTimeShouldRegisterBindingHintsForMixins() {
registerEntries(RenameMixInClass.class);
processAheadOfTime();
RuntimeHints runtimeHints = this.generationContext.getRuntimeHints();
assertThat(RuntimeHintsPredicates.reflection()
.onType(RenameMixInClass.class)
.withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(runtimeHints);
}
@Test
void processAheadOfTimeWhenPublicClassShouldRegisterClass() {
registerEntries(RenameMixInClass.class);
compile((freshContext, compiled) -> {
assertThat(freshContext.getBean(TestConfiguration.class).scanningInvoked).isFalse();
JsonMixinModuleEntries jsonMixinModuleEntries = freshContext.getBean(JsonMixinModuleEntries.class);
assertThat(jsonMixinModuleEntries).extracting("entries", InstanceOfAssertFactories.MAP)
.containsExactly(entry(Name.class, RenameMixInClass.class),
entry(NameAndAge.class, RenameMixInClass.class));
});
}
@Test
void processAheadOfTimeWhenNonAccessibleClassShouldRegisterClassName() {
Class<?> privateMixinClass = ClassUtils
.resolveClassName("org.springframework.boot.jackson.scan.e.PrivateMixInClass", null);
registerEntries(privateMixinClass);
compile((freshContext, compiled) -> {
assertThat(freshContext.getBean(TestConfiguration.class).scanningInvoked).isFalse();
JsonMixinModuleEntries jsonMixinModuleEntries = freshContext.getBean(JsonMixinModuleEntries.class);
assertThat(jsonMixinModuleEntries).extracting("entries", InstanceOfAssertFactories.MAP)
.containsExactly(entry(Name.class.getName(), privateMixinClass.getName()),
entry(NameAndAge.class.getName(), privateMixinClass.getName()));
});
}
private ClassName processAheadOfTime() {
ClassName className = new ApplicationContextAotGenerator().processAheadOfTime(this.applicationContext,
this.generationContext);
this.generationContext.writeGeneratedContent();
return className;
}
@SuppressWarnings("unchecked")
private void compile(BiConsumer<GenericApplicationContext, Compiled> result) {
ClassName className = processAheadOfTime();
TestCompiler.forSystem().with(this.generationContext).compile((compiled) -> {
GenericApplicationContext freshApplicationContext = new GenericApplicationContext();
ApplicationContextInitializer<GenericApplicationContext> initializer = compiled
.getInstance(ApplicationContextInitializer.class, className.toString());
initializer.initialize(freshApplicationContext);
freshApplicationContext.refresh();
result.accept(freshApplicationContext, compiled);
});
}
private void registerEntries(Class<?>... basePackageClasses) {
List<String> packageNames = Arrays.stream(basePackageClasses).map(Class::getPackageName).toList();
this.applicationContext.registerBeanDefinition("configuration",
BeanDefinitionBuilder.rootBeanDefinition(TestConfiguration.class)
.addConstructorArgValue(packageNames)
.getBeanDefinition());
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration {
public boolean scanningInvoked;
private final Collection<String> packageNames;
TestConfiguration(Collection<String> packageNames) {
this.packageNames = packageNames;
}
@Bean
JsonMixinModuleEntries jsonMixinModuleEntries(ApplicationContext applicationContext) {
this.scanningInvoked = true;
return JsonMixinModuleEntries.scan(applicationContext, this.packageNames);
}
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2012-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.boot.jackson;
import java.util.Arrays;
import java.util.List;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.jackson.scan.a.RenameMixInClass;
import org.springframework.boot.jackson.scan.b.RenameMixInAbstractClass;
import org.springframework.boot.jackson.scan.c.RenameMixInInterface;
import org.springframework.boot.jackson.scan.d.EmptyMixInClass;
import org.springframework.boot.jackson.scan.f.EmptyMixIn;
import org.springframework.boot.jackson.types.Name;
import org.springframework.boot.jackson.types.NameAndAge;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link JsonMixinModule}.
*
* @author Guirong Hu
*/
class JsonMixinModuleTests {
private AnnotationConfigApplicationContext context;
@AfterEach
void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test
void jsonWithModuleEmptyMixInWithEmptyTypesShouldFail() {
assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> load(EmptyMixIn.class))
.withMessageContaining("Error creating bean with name 'jsonMixinModule'")
.withStackTraceContaining("@JsonMixin annotation on class "
+ "'org.springframework.boot.jackson.scan.f.EmptyMixIn' does not specify any types");
}
@Test
void jsonWithModuleWithRenameMixInClassShouldBeMixedIn() throws Exception {
load(RenameMixInClass.class);
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
assertMixIn(module, new Name("spring"), "{\"username\":\"spring\"}");
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
}
@Test
void jsonWithModuleWithEmptyMixInClassShouldNotBeMixedIn() throws Exception {
load(EmptyMixInClass.class);
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
assertMixIn(module, new Name("spring"), "{\"name\":\"spring\"}");
assertMixIn(module, new NameAndAge("spring", 100), "{\"name\":\"spring\",\"age\":100}");
}
@Test
void jsonWithModuleWithRenameMixInAbstractClassShouldBeMixedIn() throws Exception {
load(RenameMixInAbstractClass.class);
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
}
@Test
void jsonWithModuleWithRenameMixInInterfaceShouldBeMixedIn() throws Exception {
load(RenameMixInInterface.class);
JsonMixinModule module = this.context.getBean(JsonMixinModule.class);
assertMixIn(module, new NameAndAge("spring", 100), "{\"age\":100,\"username\":\"spring\"}");
}
private void load(Class<?>... basePackageClasses) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(JsonMixinModule.class, () -> createJsonMixinModule(context, basePackageClasses));
context.refresh();
this.context = context;
}
private JsonMixinModule createJsonMixinModule(AnnotationConfigApplicationContext context,
Class<?>... basePackageClasses) {
List<String> basePackages = Arrays.stream(basePackageClasses).map(ClassUtils::getPackageName).toList();
JsonMixinModuleEntries entries = JsonMixinModuleEntries.scan(context, basePackages);
JsonMixinModule jsonMixinModule = new JsonMixinModule();
jsonMixinModule.registerEntries(entries, context.getClassLoader());
return jsonMixinModule;
}
private void assertMixIn(Module module, Name value, String expectedJson) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
String json = mapper.writeValueAsString(value);
assertThat(json).isEqualToIgnoringWhitespace(expectedJson);
}
}

View File

@@ -0,0 +1,216 @@
/*
* Copyright 2012-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.boot.jackson;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.util.function.Function;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.NullNode;
import org.junit.jupiter.api.Test;
import org.springframework.boot.jackson.NameAndAgeJsonComponent.Deserializer;
import org.springframework.boot.jackson.types.NameAndAge;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JsonObjectDeserializer}.
*
* @author Phillip Webb
*/
class JsonObjectDeserializerTests {
private final TestJsonObjectDeserializer<Object> testDeserializer = new TestJsonObjectDeserializer<>();
@Test
void deserializeObjectShouldReadJson() throws Exception {
Deserializer deserializer = new NameAndAgeJsonComponent.Deserializer();
SimpleModule module = new SimpleModule();
module.addDeserializer(NameAndAge.class, deserializer);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
NameAndAge nameAndAge = mapper.readValue("{\"name\":\"spring\",\"age\":100}", NameAndAge.class);
assertThat(nameAndAge.getName()).isEqualTo("spring");
assertThat(nameAndAge.getAge()).isEqualTo(100);
}
@Test
void nullSafeValueWhenValueIsNullShouldReturnNull() {
String value = this.testDeserializer.testNullSafeValue(null, String.class);
assertThat(value).isNull();
}
@Test
void nullSafeValueWhenClassIsNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.testDeserializer.testNullSafeValue(mock(JsonNode.class), null))
.withMessageContaining("'type' must not be null");
}
@Test
void nullSafeValueWhenClassIsStringShouldReturnString() {
JsonNode node = mock(JsonNode.class);
given(node.textValue()).willReturn("abc");
String value = this.testDeserializer.testNullSafeValue(node, String.class);
assertThat(value).isEqualTo("abc");
}
@Test
void nullSafeValueWhenClassIsBooleanShouldReturnBoolean() {
JsonNode node = mock(JsonNode.class);
given(node.booleanValue()).willReturn(true);
Boolean value = this.testDeserializer.testNullSafeValue(node, Boolean.class);
assertThat(value).isTrue();
}
@Test
void nullSafeValueWhenClassIsLongShouldReturnLong() {
JsonNode node = mock(JsonNode.class);
given(node.longValue()).willReturn(10L);
Long value = this.testDeserializer.testNullSafeValue(node, Long.class);
assertThat(value).isEqualTo(10L);
}
@Test
void nullSafeValueWhenClassIsIntegerShouldReturnInteger() {
JsonNode node = mock(JsonNode.class);
given(node.intValue()).willReturn(10);
Integer value = this.testDeserializer.testNullSafeValue(node, Integer.class);
assertThat(value).isEqualTo(10);
}
@Test
void nullSafeValueWhenClassIsShortShouldReturnShort() {
JsonNode node = mock(JsonNode.class);
given(node.shortValue()).willReturn((short) 10);
Short value = this.testDeserializer.testNullSafeValue(node, Short.class);
assertThat(value).isEqualTo((short) 10);
}
@Test
void nullSafeValueWhenClassIsDoubleShouldReturnDouble() {
JsonNode node = mock(JsonNode.class);
given(node.doubleValue()).willReturn(1.1D);
Double value = this.testDeserializer.testNullSafeValue(node, Double.class);
assertThat(value).isEqualTo(1.1D);
}
@Test
void nullSafeValueWhenClassIsFloatShouldReturnFloat() {
JsonNode node = mock(JsonNode.class);
given(node.floatValue()).willReturn(1.1F);
Float value = this.testDeserializer.testNullSafeValue(node, Float.class);
assertThat(value).isEqualTo(1.1F);
}
@Test
void nullSafeValueWhenClassIsBigDecimalShouldReturnBigDecimal() {
JsonNode node = mock(JsonNode.class);
given(node.decimalValue()).willReturn(BigDecimal.TEN);
BigDecimal value = this.testDeserializer.testNullSafeValue(node, BigDecimal.class);
assertThat(value).isEqualTo(BigDecimal.TEN);
}
@Test
void nullSafeValueWhenClassIsBigIntegerShouldReturnBigInteger() {
JsonNode node = mock(JsonNode.class);
given(node.bigIntegerValue()).willReturn(BigInteger.TEN);
BigInteger value = this.testDeserializer.testNullSafeValue(node, BigInteger.class);
assertThat(value).isEqualTo(BigInteger.TEN);
}
@Test
void nullSafeValueWithMapperShouldTransformValue() {
JsonNode node = mock(JsonNode.class);
given(node.textValue()).willReturn("2023-12-01");
LocalDate result = this.testDeserializer.testNullSafeValue(node, String.class, LocalDate::parse);
assertThat(result).isEqualTo(LocalDate.of(2023, 12, 1));
}
@Test
void nullSafeValueWhenClassIsUnknownShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.testDeserializer.testNullSafeValue(mock(JsonNode.class), InputStream.class))
.withMessageContaining("Unsupported value type java.io.InputStream");
}
@Test
void getRequiredNodeWhenTreeIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.testDeserializer.testGetRequiredNode(null, "test"))
.withMessageContaining("'tree' must not be null");
}
@Test
void getRequiredNodeWhenNodeIsNullShouldThrowException() {
JsonNode tree = mock(JsonNode.class);
given(tree.get("test")).willReturn(null);
assertThatIllegalStateException().isThrownBy(() -> this.testDeserializer.testGetRequiredNode(tree, "test"))
.withMessageContaining("Missing JSON field 'test'");
}
@Test
void getRequiredNodeWhenNodeIsNullNodeShouldThrowException() {
JsonNode tree = mock(JsonNode.class);
given(tree.get("test")).willReturn(NullNode.instance);
assertThatIllegalStateException().isThrownBy(() -> this.testDeserializer.testGetRequiredNode(tree, "test"))
.withMessageContaining("Missing JSON field 'test'");
}
@Test
void getRequiredNodeWhenNodeIsFoundShouldReturnNode() {
JsonNode node = mock(JsonNode.class);
given(node.get("test")).willReturn(node);
assertThat(this.testDeserializer.testGetRequiredNode(node, "test")).isEqualTo(node);
}
static class TestJsonObjectDeserializer<T> extends JsonObjectDeserializer<T> {
@Override
protected T deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
JsonNode tree) {
return null;
}
<D, R> R testNullSafeValue(JsonNode jsonNode, Class<D> type, Function<D, R> mapper) {
return nullSafeValue(jsonNode, type, mapper);
}
<D> D testNullSafeValue(JsonNode jsonNode, Class<D> type) {
return nullSafeValue(jsonNode, type);
}
JsonNode testGetRequiredNode(JsonNode tree, String fieldName) {
return getRequiredNode(tree, fieldName);
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2012-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.boot.jackson;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.junit.jupiter.api.Test;
import org.springframework.boot.jackson.NameAndAgeJsonComponent.Serializer;
import org.springframework.boot.jackson.types.NameAndAge;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JsonObjectSerializer}.
*
* @author Phillip Webb
*/
class JsonObjectSerializerTests {
@Test
void serializeObjectShouldWriteJson() throws Exception {
Serializer serializer = new NameAndAgeJsonComponent.Serializer();
SimpleModule module = new SimpleModule();
module.addSerializer(NameAndAge.class, serializer);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
String json = mapper.writeValueAsString(new NameAndAge("spring", 100));
assertThat(json).isEqualToIgnoringWhitespace("{\"name\":\"spring\",\"age\":100}");
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2012-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.boot.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson.types.NameAndAge;
/**
* Sample {@link JsonComponent @JsonComponent} used for tests.
*
* @author Phillip Webb
*/
@JsonComponent
public class NameAndAgeJsonComponent {
static class Serializer extends JsonObjectSerializer<NameAndAge> {
@Override
protected void serializeObject(NameAndAge value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
jgen.writeStringField("name", value.getName());
jgen.writeNumberField("age", value.getAge());
}
}
static class Deserializer extends JsonObjectDeserializer<NameAndAge> {
@Override
protected NameAndAge deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
JsonNode tree) throws IOException {
String name = nullSafeValue(tree.get("name"), String.class);
Integer age = nullSafeValue(tree.get("age"), Integer.class);
return new NameAndAge(name, age);
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-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.boot.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson.types.NameAndAge;
/**
* Sample {@link JsonComponent @JsonComponent} used for tests.
*
* @author Paul Aly
*/
@JsonComponent(type = NameAndAge.class, scope = JsonComponent.Scope.KEYS)
public class NameAndAgeJsonKeyComponent {
static class Serializer extends JsonSerializer<NameAndAge> {
@Override
public void serialize(NameAndAge value, JsonGenerator jgen, SerializerProvider serializers) throws IOException {
jgen.writeFieldName(value.asKey());
}
}
static class Deserializer extends KeyDeserializer {
@Override
public NameAndAge deserializeKey(String key, DeserializationContext ctxt) throws IOException {
String[] keys = key.split("is");
return new NameAndAge(keys[0].trim(), Integer.parseInt(keys[1].trim()));
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-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.boot.jackson;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson.types.Name;
import org.springframework.boot.jackson.types.NameAndCareer;
/**
* Sample {@link JsonComponent @JsonComponent} used for tests.
*
* @author Paul Aly
*/
@JsonComponent(type = NameAndCareer.class)
public class NameAndCareerJsonComponent {
static class Serializer extends JsonObjectSerializer<Name> {
@Override
protected void serializeObject(Name value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStringField("name", value.getName());
}
}
static class Deserializer extends JsonObjectDeserializer<Name> {
@Override
protected Name deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
JsonNode tree) throws IOException {
String name = nullSafeValue(tree.get("name"), String.class);
String career = nullSafeValue(tree.get("career"), String.class);
return new NameAndCareer(name, career);
}
}
}

View File

@@ -0,0 +1,753 @@
/*
* Copyright 2012-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.boot.jackson.autoconfigure;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonCreator.Mode;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.cfg.ConstructorDetector;
import com.fasterxml.jackson.databind.cfg.ConstructorDetector.SingleArgConstructor;
import com.fasterxml.jackson.databind.cfg.EnumFeature;
import com.fasterxml.jackson.databind.cfg.JsonNodeFeature;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.boot.jackson.JsonMixin;
import org.springframework.boot.jackson.JsonMixinModule;
import org.springframework.boot.jackson.JsonMixinModuleEntries;
import org.springframework.boot.jackson.JsonObjectSerializer;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonAutoConfigurationRuntimeHints;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JacksonAutoConfiguration}.
*
* @author Dave Syer
* @author Oliver Gierke
* @author Andy Wilkinson
* @author Marcel Overdijk
* @author Sebastien Deleuze
* @author Johannes Edmeier
* @author Grzegorz Poznachowski
* @author Ralf Ueberfuhr
* @author Eddú Meléndez
*/
@SuppressWarnings("removal")
class JacksonAutoConfigurationTests {
protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class));
@Test
void doubleModuleRegistration() {
this.contextRunner.withUserConfiguration(DoubleModulesConfig.class).run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.writeValueAsString(new Foo())).isEqualTo("{\"foo\":\"bar\"}");
});
}
@Test
void jsonMixinModuleShouldBeAutoConfiguredWithBasePackages() {
this.contextRunner.withUserConfiguration(MixinConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(JsonMixinModule.class).hasSingleBean(JsonMixinModuleEntries.class);
JsonMixinModuleEntries moduleEntries = context.getBean(JsonMixinModuleEntries.class);
assertThat(moduleEntries).extracting("entries", InstanceOfAssertFactories.MAP)
.contains(entry(Person.class, EmptyMixin.class));
});
}
@Test
void noCustomDateFormat() {
this.contextRunner.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getDateFormat()).isInstanceOf(StdDateFormat.class);
});
}
@Test
void customDateFormat() {
this.contextRunner.withPropertyValues("spring.jackson.date-format:yyyyMMddHHmmss").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
DateFormat dateFormat = mapper.getDateFormat();
assertThat(dateFormat).isInstanceOf(SimpleDateFormat.class);
assertThat(((SimpleDateFormat) dateFormat).toPattern()).isEqualTo("yyyyMMddHHmmss");
});
}
@Test
void customDateFormatClass() {
this.contextRunner.withPropertyValues("spring.jackson.date-format:" + MyDateFormat.class.getName())
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getDateFormat()).isInstanceOf(MyDateFormat.class);
});
}
@Test
void noCustomPropertyNamingStrategy() {
this.contextRunner.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getPropertyNamingStrategy()).isNull();
});
}
@Test
void customPropertyNamingStrategyField() {
this.contextRunner.withPropertyValues("spring.jackson.property-naming-strategy:SNAKE_CASE").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getPropertyNamingStrategy()).isInstanceOf(SnakeCaseStrategy.class);
});
}
@Test
void customPropertyNamingStrategyClass() {
this.contextRunner.withPropertyValues(
"spring.jackson.property-naming-strategy:com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getPropertyNamingStrategy()).isInstanceOf(SnakeCaseStrategy.class);
});
}
@Test
void enableSerializationFeature() {
this.contextRunner.withPropertyValues("spring.jackson.serialization.indent_output:true").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(SerializationFeature.INDENT_OUTPUT.enabledByDefault()).isFalse();
assertThat(mapper.getSerializationConfig()
.hasSerializationFeatures(SerializationFeature.INDENT_OUTPUT.getMask())).isTrue();
});
}
@Test
void disableSerializationFeature() {
this.contextRunner.withPropertyValues("spring.jackson.serialization.write_dates_as_timestamps:false")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS.enabledByDefault()).isTrue();
assertThat(mapper.getSerializationConfig()
.hasSerializationFeatures(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS.getMask())).isFalse();
});
}
@Test
void enableDeserializationFeature() {
this.contextRunner.withPropertyValues("spring.jackson.deserialization.use_big_decimal_for_floats:true")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS.enabledByDefault()).isFalse();
assertThat(mapper.getDeserializationConfig()
.hasDeserializationFeatures(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS.getMask())).isTrue();
});
}
@Test
void disableDeserializationFeature() {
this.contextRunner.withPropertyValues("spring.jackson.deserialization.fail-on-unknown-properties:false")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig()
.hasDeserializationFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.getMask())).isFalse();
});
}
@Test
void enableMapperFeature() {
this.contextRunner.withPropertyValues("spring.jackson.mapper.require_setters_for_getters:true")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS.enabledByDefault()).isFalse();
assertThat(mapper.getSerializationConfig().isEnabled(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS))
.isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS))
.isTrue();
});
}
@Test
void disableMapperFeature() {
this.contextRunner.withPropertyValues("spring.jackson.mapper.use_annotations:false").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(MapperFeature.USE_ANNOTATIONS.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(MapperFeature.USE_ANNOTATIONS)).isFalse();
assertThat(mapper.getSerializationConfig().isEnabled(MapperFeature.USE_ANNOTATIONS)).isFalse();
});
}
@Test
void enableParserFeature() {
this.contextRunner.withPropertyValues("spring.jackson.parser.allow_single_quotes:true").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(JsonParser.Feature.ALLOW_SINGLE_QUOTES.enabledByDefault()).isFalse();
assertThat(mapper.getFactory().isEnabled(JsonParser.Feature.ALLOW_SINGLE_QUOTES)).isTrue();
});
}
@Test
void disableParserFeature() {
this.contextRunner.withPropertyValues("spring.jackson.parser.auto_close_source:false").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(JsonParser.Feature.AUTO_CLOSE_SOURCE.enabledByDefault()).isTrue();
assertThat(mapper.getFactory().isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE)).isFalse();
});
}
@Test
void enableGeneratorFeature() {
this.contextRunner.withPropertyValues("spring.jackson.generator.strict_duplicate_detection:true")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
JsonGenerator.Feature feature = JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION;
assertThat(feature.enabledByDefault()).isFalse();
assertThat(mapper.getFactory().isEnabled(feature)).isTrue();
});
}
@Test
void disableGeneratorFeature() {
this.contextRunner.withPropertyValues("spring.jackson.generator.auto_close_target:false").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(JsonGenerator.Feature.AUTO_CLOSE_TARGET.enabledByDefault()).isTrue();
assertThat(mapper.getFactory().isEnabled(JsonGenerator.Feature.AUTO_CLOSE_TARGET)).isFalse();
});
}
@Test
void defaultObjectMapperBuilder() {
this.contextRunner.run((context) -> {
Jackson2ObjectMapperBuilder builder = context.getBean(Jackson2ObjectMapperBuilder.class);
ObjectMapper mapper = builder.build();
assertThat(MapperFeature.DEFAULT_VIEW_INCLUSION.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)).isFalse();
assertThat(MapperFeature.DEFAULT_VIEW_INCLUSION.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)).isFalse();
assertThat(mapper.getSerializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)).isFalse();
assertThat(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES))
.isFalse();
});
}
@Test
void enableEnumFeature() {
this.contextRunner.withPropertyValues("spring.jackson.datatype.enum.write-enums-to-lowercase=true")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(EnumFeature.WRITE_ENUMS_TO_LOWERCASE.enabledByDefault()).isFalse();
assertThat(mapper.getSerializationConfig().isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE)).isTrue();
});
}
@Test
void disableJsonNodeFeature() {
this.contextRunner.withPropertyValues("spring.jackson.datatype.json-node.write-null-properties:false")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(JsonNodeFeature.WRITE_NULL_PROPERTIES.enabledByDefault()).isTrue();
assertThat(mapper.getDeserializationConfig().isEnabled(JsonNodeFeature.WRITE_NULL_PROPERTIES))
.isFalse();
});
}
@Test
void moduleBeansAndWellKnownModulesAreRegisteredWithTheObjectMapperBuilder() {
this.contextRunner.withUserConfiguration(ModuleConfig.class).run((context) -> {
ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build();
assertThat(context.getBean(CustomModule.class).getOwners()).contains(objectMapper);
assertThat(((DefaultSerializerProvider) objectMapper.getSerializerProviderInstance())
.hasSerializerFor(Baz.class, null)).isTrue();
});
}
@Test
void customModulesRegisteredByBuilderCustomizerShouldBeRetained() {
this.contextRunner.withUserConfiguration(ModuleConfig.class, CustomModuleBuilderCustomizerConfig.class)
.run((context) -> {
ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build();
assertThat(context.getBean(CustomModule.class).getOwners()).contains(objectMapper);
assertThat(objectMapper.getRegisteredModuleIds()).contains("module-A", "module-B",
CustomModule.class.getName());
});
}
@Test
void defaultSerializationInclusion() {
this.contextRunner.run((context) -> {
ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build();
assertThat(objectMapper.getSerializationConfig().getDefaultPropertyInclusion().getValueInclusion())
.isEqualTo(JsonInclude.Include.USE_DEFAULTS);
});
}
@Test
void customSerializationInclusion() {
this.contextRunner.withPropertyValues("spring.jackson.default-property-inclusion:non_null").run((context) -> {
ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build();
assertThat(objectMapper.getSerializationConfig().getDefaultPropertyInclusion().getValueInclusion())
.isEqualTo(JsonInclude.Include.NON_NULL);
});
}
@Test
void customTimeZoneFormattingADate() {
this.contextRunner.withPropertyValues("spring.jackson.time-zone:GMT+10", "spring.jackson.date-format:z")
.run((context) -> {
ObjectMapper objectMapper = context.getBean(Jackson2ObjectMapperBuilder.class).build();
Date date = new Date(1436966242231L);
assertThat(objectMapper.writeValueAsString(date)).isEqualTo("\"GMT+10:00\"");
});
}
@Test
void enableDefaultLeniency() {
this.contextRunner.withPropertyValues("spring.jackson.default-leniency:true").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
Person person = mapper.readValue("{\"birthDate\": \"2010-12-30\"}", Person.class);
assertThat(person.getBirthDate()).isNotNull();
});
}
@Test
void disableDefaultLeniency() {
this.contextRunner.withPropertyValues("spring.jackson.default-leniency:false").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThatExceptionOfType(InvalidFormatException.class)
.isThrownBy(() -> mapper.readValue("{\"birthDate\": \"2010-12-30\"}", Person.class))
.withMessageContaining("expected format")
.withMessageContaining("yyyyMMdd");
});
}
@Test
void constructorDetectorWithNoStrategyUseDefault() {
this.contextRunner.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.HEURISTIC);
assertThat(cd.requireCtorAnnotation()).isFalse();
assertThat(cd.allowJDKTypeConstructors()).isFalse();
});
}
@Test
void constructorDetectorWithDefaultStrategy() {
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=default").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.HEURISTIC);
assertThat(cd.requireCtorAnnotation()).isFalse();
assertThat(cd.allowJDKTypeConstructors()).isFalse();
});
}
@Test
void constructorDetectorWithUsePropertiesBasedStrategy() {
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=use-properties-based")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.PROPERTIES);
assertThat(cd.requireCtorAnnotation()).isFalse();
assertThat(cd.allowJDKTypeConstructors()).isFalse();
});
}
@Test
void constructorDetectorWithUseDelegatingStrategy() {
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=use-delegating").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.DELEGATING);
assertThat(cd.requireCtorAnnotation()).isFalse();
assertThat(cd.allowJDKTypeConstructors()).isFalse();
});
}
@Test
void constructorDetectorWithExplicitOnlyStrategy() {
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=explicit-only").run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.REQUIRE_MODE);
assertThat(cd.requireCtorAnnotation()).isFalse();
assertThat(cd.allowJDKTypeConstructors()).isFalse();
});
}
@Test
void additionalJacksonBuilderCustomization() {
this.contextRunner.withUserConfiguration(ObjectMapperBuilderCustomConfig.class).run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
assertThat(mapper.getDateFormat()).isInstanceOf(MyDateFormat.class);
});
}
@Test
void parameterNamesModuleIsAutoConfigured() {
assertParameterNamesModuleCreatorBinding(Mode.DEFAULT, JacksonAutoConfiguration.class);
}
@Test
void customParameterNamesModuleCanBeConfigured() {
assertParameterNamesModuleCreatorBinding(Mode.DELEGATING, ParameterNamesModuleConfig.class,
JacksonAutoConfiguration.class);
}
@Test
void writeDurationAsTimestampsDefault() {
this.contextRunner.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
Duration duration = Duration.ofHours(2);
assertThat(mapper.writeValueAsString(duration)).isEqualTo("\"PT2H\"");
});
}
@Test
void writeWithVisibility() {
this.contextRunner
.withPropertyValues("spring.jackson.visibility.getter:none", "spring.jackson.visibility.field:any")
.run((context) -> {
ObjectMapper mapper = context.getBean(ObjectMapper.class);
String json = mapper.writeValueAsString(new VisibilityBean());
assertThat(json).contains("property1");
assertThat(json).contains("property2");
assertThat(json).doesNotContain("property3");
});
}
@Test
void builderIsNotSharedAcrossMultipleInjectionPoints() {
this.contextRunner.withUserConfiguration(ObjectMapperBuilderConsumerConfig.class).run((context) -> {
ObjectMapperBuilderConsumerConfig consumer = context.getBean(ObjectMapperBuilderConsumerConfig.class);
assertThat(consumer.builderOne).isNotNull();
assertThat(consumer.builderTwo).isNotNull();
assertThat(consumer.builderOne).isNotSameAs(consumer.builderTwo);
});
}
@Test
void jsonComponentThatInjectsObjectMapperCausesBeanCurrentlyInCreationException() {
this.contextRunner.withUserConfiguration(CircularDependencySerializerConfiguration.class).run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure().hasRootCauseInstanceOf(BeanCurrentlyInCreationException.class);
});
}
@Test
void shouldRegisterPropertyNamingStrategyHints() {
RuntimeHints hints = new RuntimeHints();
new JacksonAutoConfigurationRuntimeHints().registerHints(hints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.reflection().onType(PropertyNamingStrategies.class)).accepts(hints);
}
private void assertParameterNamesModuleCreatorBinding(Mode expectedMode, Class<?>... configClasses) {
this.contextRunner.withUserConfiguration(configClasses).run((context) -> {
DeserializationConfig deserializationConfig = context.getBean(ObjectMapper.class)
.getDeserializationConfig();
AnnotationIntrospector annotationIntrospector = deserializationConfig.getAnnotationIntrospector()
.allIntrospectors()
.iterator()
.next();
assertThat(annotationIntrospector).hasFieldOrPropertyWithValue("creatorBinding", expectedMode);
});
}
static class MyDateFormat extends SimpleDateFormat {
MyDateFormat() {
super("yyyy-MM-dd HH:mm:ss");
}
}
@Configuration(proxyBeanMethods = false)
static class MockObjectMapperConfig {
@Bean
@Primary
ObjectMapper objectMapper() {
return mock(ObjectMapper.class);
}
}
@Configuration(proxyBeanMethods = false)
@Import(BazSerializer.class)
static class ModuleConfig {
@Bean
CustomModule jacksonModule() {
return new CustomModule();
}
}
@Configuration
static class DoubleModulesConfig {
@Bean
Module jacksonModule() {
SimpleModule module = new SimpleModule();
module.addSerializer(Foo.class, new JsonSerializer<>() {
@Override
public void serialize(Foo value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStartObject();
jgen.writeStringField("foo", "bar");
jgen.writeEndObject();
}
});
return module;
}
@Bean
@Primary
ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(jacksonModule());
return mapper;
}
}
@Configuration(proxyBeanMethods = false)
static class ParameterNamesModuleConfig {
@Bean
ParameterNamesModule parameterNamesModule() {
return new ParameterNamesModule(JsonCreator.Mode.DELEGATING);
}
}
@Configuration(proxyBeanMethods = false)
static class ObjectMapperBuilderCustomConfig {
@Bean
Jackson2ObjectMapperBuilderCustomizer customDateFormat() {
return (builder) -> builder.dateFormat(new MyDateFormat());
}
}
@Configuration(proxyBeanMethods = false)
static class CustomModuleBuilderCustomizerConfig {
@Bean
@Order(-1)
Jackson2ObjectMapperBuilderCustomizer highPrecedenceCustomizer() {
return (builder) -> builder.modulesToInstall((modules) -> modules.add(new SimpleModule("module-A")));
}
@Bean
@Order(1)
Jackson2ObjectMapperBuilderCustomizer lowPrecedenceCustomizer() {
return (builder) -> builder.modulesToInstall((modules) -> modules.add(new SimpleModule("module-B")));
}
}
@Configuration(proxyBeanMethods = false)
static class ObjectMapperBuilderConsumerConfig {
Jackson2ObjectMapperBuilder builderOne;
Jackson2ObjectMapperBuilder builderTwo;
@Bean
String consumerOne(Jackson2ObjectMapperBuilder builder) {
this.builderOne = builder;
return "one";
}
@Bean
String consumerTwo(Jackson2ObjectMapperBuilder builder) {
this.builderTwo = builder;
return "two";
}
}
protected static final class Foo {
private String name;
private Foo() {
}
static Foo create() {
return new Foo();
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
static class Bar {
private String propertyName;
String getPropertyName() {
return this.propertyName;
}
void setPropertyName(String propertyName) {
this.propertyName = propertyName;
}
}
@JsonComponent
static class BazSerializer extends JsonObjectSerializer<Baz> {
@Override
protected void serializeObject(Baz value, JsonGenerator jgen, SerializerProvider provider) {
}
}
static class Baz {
}
static class CustomModule extends SimpleModule {
private final Set<ObjectCodec> owners = new HashSet<>();
@Override
public void setupModule(SetupContext context) {
this.owners.add(context.getOwner());
}
Set<ObjectCodec> getOwners() {
return this.owners;
}
}
@SuppressWarnings("unused")
static class VisibilityBean {
private String property1;
public String property2;
String getProperty3() {
return null;
}
}
static class Person {
@JsonFormat(pattern = "yyyyMMdd")
private Date birthDate;
Date getBirthDate() {
return this.birthDate;
}
void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
}
@JsonMixin(type = Person.class)
static class EmptyMixin {
}
@AutoConfigurationPackage
static class MixinConfiguration {
}
@JsonComponent
static class CircularDependencySerializer extends JsonSerializer<String> {
CircularDependencySerializer(ObjectMapper objectMapper) {
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
}
}
@Import(CircularDependencySerializer.class)
@Configuration(proxyBeanMethods = false)
static class CircularDependencySerializerConfiguration {
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2012-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.boot.jackson.scan.a;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.jackson.JsonMixin;
import org.springframework.boot.jackson.types.Name;
import org.springframework.boot.jackson.types.NameAndAge;
@JsonMixin(type = { Name.class, NameAndAge.class })
public abstract class RenameMixInClass {
@JsonProperty("username")
public abstract String getName();
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2012-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.boot.jackson.scan.b;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.jackson.JsonMixin;
import org.springframework.boot.jackson.types.NameAndAge;
@JsonMixin(type = NameAndAge.class)
public abstract class RenameMixInAbstractClass {
@JsonProperty("username")
abstract String getName();
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2012-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.boot.jackson.scan.c;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.jackson.JsonMixin;
import org.springframework.boot.jackson.types.NameAndAge;
@JsonMixin(type = NameAndAge.class)
public interface RenameMixInInterface {
@JsonProperty("username")
String getName();
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2012-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.boot.jackson.scan.d;
import org.springframework.boot.jackson.JsonMixin;
import org.springframework.boot.jackson.types.Name;
import org.springframework.boot.jackson.types.NameAndAge;
@JsonMixin(type = { Name.class, NameAndAge.class })
public class EmptyMixInClass {
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2012-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.boot.jackson.scan.e;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.jackson.JsonMixin;
import org.springframework.boot.jackson.types.Name;
import org.springframework.boot.jackson.types.NameAndAge;
@JsonMixin(type = { Name.class, NameAndAge.class })
class PrivateMixInClass {
@JsonProperty("username")
String getName() {
return null;
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2012-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.boot.jackson.scan.f;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.jackson.JsonMixin;
@JsonMixin
public interface EmptyMixIn {
@JsonProperty("username")
String getName();
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2012-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.boot.jackson.types;
/**
* Sample object used for tests.
*
* @author Paul Aly
*/
public class Name {
protected final String name;
public Name(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2012-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.boot.jackson.types;
import org.springframework.util.ObjectUtils;
/**
* Sample object used for tests.
*
* @author Phillip Webb
* @author Paul Aly
*/
public final class NameAndAge extends Name {
private final int age;
public NameAndAge(String name, int age) {
super(name);
this.age = age;
}
public int getAge() {
return this.age;
}
public String asKey() {
return this.name + " is " + this.age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (obj instanceof NameAndAge other) {
boolean rtn = true;
rtn = rtn && ObjectUtils.nullSafeEquals(this.name, other.name);
rtn = rtn && ObjectUtils.nullSafeEquals(this.age, other.age);
return rtn;
}
return super.equals(obj);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ObjectUtils.nullSafeHashCode(this.name);
result = prime * result + ObjectUtils.nullSafeHashCode(this.age);
return result;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-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.boot.jackson.types;
/**
* Sample object used for tests.
*
* @author Paul Aly
*/
public class NameAndCareer extends Name {
private final String career;
public NameAndCareer(String name, String career) {
super(name);
this.career = career;
}
public String getCareer() {
return this.career;
}
}