Create spring-boot-jackson-module
This commit is contained in:
committed by
Phillip Webb
parent
57006ee82e
commit
67a4428139
@@ -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">
|
||||
* @JsonComponent
|
||||
* public class CustomerJsonComponent {
|
||||
*
|
||||
* public static class Serializer extends JsonSerializer<Customer> {
|
||||
*
|
||||
* // ...
|
||||
*
|
||||
* }
|
||||
*
|
||||
* public static class Deserializer extends JsonDeserializer<Customer> {
|
||||
*
|
||||
* // ...
|
||||
*
|
||||
* }
|
||||
*
|
||||
* }
|
||||
*
|
||||
* </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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {};
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Background Preinitializers
|
||||
org.springframework.boot.autoconfigure.preinitialize.BackgroundPreinitializer=\
|
||||
org.springframework.boot.jackson.autoconfigure.JacksonBackgroundPreinitializer
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user