Add customizer and property support for configuring Jackson factories

Closes gh-34709
This commit is contained in:
Andy Wilkinson
2026-02-13 11:31:17 +00:00
parent 6c820776b5
commit 918e59b817
7 changed files with 542 additions and 23 deletions
@@ -108,6 +108,8 @@ To ease the migration when working on an application that previously used Jackso
The context's javadoc:tools.jackson.databind.json.JsonMapper$Builder[] can be customized by one or more javadoc:org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer[] beans.
Such customizer beans can be ordered (Boot's own customizer has an order of 0), letting additional customization be applied both before and after Boot's customization.
Furthermore, the `JsonFactory` used by the builder and the mapper that it creates can be customized by one or more javadoc:org.springframework.boot.jackson.autoconfigure.JsonFactoryBuilderCustomizer[] beans.
Various `spring.jackson.factory` properties can also be used to configure the factory.
Any beans of type javadoc:tools.jackson.databind.JacksonModule[] are automatically registered with the auto-configured javadoc:tools.jackson.databind.json.JsonMapper$Builder[] and are applied to any javadoc:tools.jackson.databind.json.JsonMapper[] instances that it creates.
This provides an application-wide mechanism for contributing custom modules when you add new features to your application.
@@ -0,0 +1,39 @@
/*
* Copyright 2012-present 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 tools.jackson.dataformat.cbor.CBORFactory;
import tools.jackson.dataformat.cbor.CBORFactoryBuilder;
/**
* Callback interface that can be implemented by beans wishing to further customize the
* {@link CBORFactory} through {@link CBORFactoryBuilder} to fine-tune its
* auto-configuration.
*
* @author Andy Wilkinson
* @since 4.1.0
*/
@FunctionalInterface
public interface CborFactoryBuilderCustomizer {
/**
* Customize the CBORFactoryBuilder.
* @param cborFactoryBuilder the builder to customize
*/
void customize(CBORFactoryBuilder cborFactoryBuilder);
}
@@ -32,6 +32,11 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import org.jspecify.annotations.Nullable;
import tools.jackson.core.StreamReadConstraints;
import tools.jackson.core.StreamWriteConstraints;
import tools.jackson.core.base.DecorableTSFactory.DecorableTSFBuilder;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonFactoryBuilder;
import tools.jackson.databind.JacksonModule;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.PropertyNamingStrategies;
@@ -40,7 +45,11 @@ import tools.jackson.databind.cfg.ConstructorDetector;
import tools.jackson.databind.cfg.DateTimeFeature;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.dataformat.cbor.CBORFactory;
import tools.jackson.dataformat.cbor.CBORFactoryBuilder;
import tools.jackson.dataformat.cbor.CBORMapper;
import tools.jackson.dataformat.xml.XmlFactory;
import tools.jackson.dataformat.xml.XmlFactoryBuilder;
import tools.jackson.dataformat.xml.XmlMapper;
import org.springframework.aot.hint.ReflectionHints;
@@ -55,10 +64,14 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.context.properties.PropertyMapper;
import org.springframework.boot.jackson.JacksonComponentModule;
import org.springframework.boot.jackson.JacksonMixinModule;
import org.springframework.boot.jackson.JacksonMixinModuleEntries;
import org.springframework.boot.jackson.autoconfigure.JacksonProperties.ConstructorDetectorStrategy;
import org.springframework.boot.jackson.autoconfigure.JacksonProperties.Factory.Constraints;
import org.springframework.boot.jackson.autoconfigure.JacksonProperties.Factory.Constraints.Read;
import org.springframework.boot.jackson.autoconfigure.JacksonProperties.Factory.Constraints.Write;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -94,11 +107,21 @@ public final class JacksonAutoConfiguration {
return new JacksonComponentModule();
}
@Bean
@ConditionalOnMissingBean
JsonFactory jsonFactory(List<JsonFactoryBuilderCustomizer> customizers) {
JsonFactoryBuilder builder = JsonFactory.builder();
for (JsonFactoryBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
return builder.build();
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ConditionalOnMissingBean
JsonMapper.Builder jsonMapperBuilder(List<JsonMapperBuilderCustomizer> customizers) {
JsonMapper.Builder builder = JsonMapper.builder();
JsonMapper.Builder jsonMapperBuilder(List<JsonMapperBuilderCustomizer> customizers, JsonFactory jsonFactory) {
JsonMapper.Builder builder = JsonMapper.builder(jsonFactory);
customize(builder, customizers);
return builder;
}
@@ -137,12 +160,36 @@ public final class JacksonAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JacksonProperties.class)
static class JacksonJsonMapperBuilderCustomizerConfiguration {
static class JacksonJsonCustomizerConfiguration {
private final JacksonProperties jacksonProperties;
JacksonJsonCustomizerConfiguration(JacksonProperties jacksonProperties) {
this.jacksonProperties = jacksonProperties;
}
@Bean
StandardJsonMapperBuilderCustomizer standardJsonMapperBuilderCustomizer(JacksonProperties jacksonProperties,
ObjectProvider<JacksonModule> modules) {
return new StandardJsonMapperBuilderCustomizer(jacksonProperties, modules.stream().toList());
StandardJsonFactoryBuilderCustomizer standardJsonFactoryBuilderCustomizer() {
return new StandardJsonFactoryBuilderCustomizer(this.jacksonProperties);
}
@Bean
StandardJsonMapperBuilderCustomizer standardJsonMapperBuilderCustomizer(ObjectProvider<JacksonModule> modules) {
return new StandardJsonMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList());
}
static final class StandardJsonFactoryBuilderCustomizer
extends AbstractFactoryBuilderCustomizer<JsonFactoryBuilder> implements JsonFactoryBuilderCustomizer {
StandardJsonFactoryBuilderCustomizer(JacksonProperties jacksonProperties) {
super(jacksonProperties);
}
@Override
public void customize(JsonFactoryBuilder jsonFactoryBuilder) {
super.customize(jsonFactoryBuilder);
}
}
static final class StandardJsonMapperBuilderCustomizer
@@ -189,17 +236,27 @@ public final class JacksonAutoConfiguration {
@EnableConfigurationProperties(JacksonCborProperties.class)
static class CborConfiguration {
private final JacksonProperties jacksonProperties;
CborConfiguration(JacksonProperties jacksonProperties) {
this.jacksonProperties = jacksonProperties;
}
@Bean
@ConditionalOnMissingBean
CBORMapper cborMapper(CBORMapper.Builder builder) {
CBORFactory cborFactory(List<CborFactoryBuilderCustomizer> customizers) {
CBORFactoryBuilder builder = CBORFactory.builder();
for (CborFactoryBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
return builder.build();
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ConditionalOnMissingBean
CBORMapper.Builder cborMapperBuilder(List<CborMapperBuilderCustomizer> customizers) {
CBORMapper.Builder builder = CBORMapper.builder();
CBORMapper.Builder cborMapperBuilder(CBORFactory factory, List<CborMapperBuilderCustomizer> customizers) {
CBORMapper.Builder builder = CBORMapper.builder(factory);
customize(builder, customizers);
return builder;
}
@@ -211,12 +268,37 @@ public final class JacksonAutoConfiguration {
}
@Bean
StandardCborMapperBuilderCustomizer standardCborMapperBuilderCustomizer(JacksonProperties jacksonProperties,
ObjectProvider<JacksonModule> modules, JacksonCborProperties cborProperties) {
return new StandardCborMapperBuilderCustomizer(jacksonProperties, modules.stream().toList(),
@ConditionalOnMissingBean
CBORMapper cborMapper(CBORMapper.Builder builder) {
return builder.build();
}
@Bean
StandardCborFactoryBuilderCustomizer standardCborFactoryBuilderCustomizer() {
return new StandardCborFactoryBuilderCustomizer(this.jacksonProperties);
}
@Bean
StandardCborMapperBuilderCustomizer standardCborMapperBuilderCustomizer(ObjectProvider<JacksonModule> modules,
JacksonCborProperties cborProperties) {
return new StandardCborMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(),
cborProperties);
}
static final class StandardCborFactoryBuilderCustomizer
extends AbstractFactoryBuilderCustomizer<CBORFactoryBuilder> implements CborFactoryBuilderCustomizer {
StandardCborFactoryBuilderCustomizer(JacksonProperties jacksonProperties) {
super(jacksonProperties);
}
@Override
public void customize(CBORFactoryBuilder cborFactoryBuilder) {
super.customize(cborFactoryBuilder);
}
}
static class StandardCborMapperBuilderCustomizer extends AbstractMapperBuilderCustomizer<CBORMapper.Builder>
implements CborMapperBuilderCustomizer {
@@ -244,17 +326,27 @@ public final class JacksonAutoConfiguration {
@EnableConfigurationProperties(JacksonXmlProperties.class)
static class XmlConfiguration {
private final JacksonProperties jacksonProperties;
XmlConfiguration(JacksonProperties jacksonProperties) {
this.jacksonProperties = jacksonProperties;
}
@Bean
@ConditionalOnMissingBean
XmlMapper xmlMapper(XmlMapper.Builder builder) {
XmlFactory xmlFactory(List<XmlFactoryBuilderCustomizer> customizers) {
XmlFactoryBuilder builder = XmlFactory.builder();
for (XmlFactoryBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
return builder.build();
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ConditionalOnMissingBean
XmlMapper.Builder xmlMapperBuilder(List<XmlMapperBuilderCustomizer> customizers) {
XmlMapper.Builder builder = XmlMapper.builder();
XmlMapper.Builder xmlMapperBuilder(XmlFactory xmlFactory, List<XmlMapperBuilderCustomizer> customizers) {
XmlMapper.Builder builder = XmlMapper.builder(xmlFactory);
customize(builder, customizers);
return builder;
}
@@ -266,9 +358,21 @@ public final class JacksonAutoConfiguration {
}
@Bean
StandardXmlMapperBuilderCustomizer standardXmlMapperBuilderCustomizer(JacksonProperties jacksonProperties,
ObjectProvider<JacksonModule> modules, JacksonXmlProperties xmlProperties) {
return new StandardXmlMapperBuilderCustomizer(jacksonProperties, modules.stream().toList(), xmlProperties);
@ConditionalOnMissingBean
XmlMapper xmlMapper(XmlMapper.Builder builder) {
return builder.build();
}
@Bean
StandardXmlFactoryBuilderCustomizer standardXmlFactoryBuilderCustomizer() {
return new StandardXmlFactoryBuilderCustomizer(this.jacksonProperties);
}
@Bean
StandardXmlMapperBuilderCustomizer standardXmlMapperBuilderCustomizer(ObjectProvider<JacksonModule> modules,
JacksonXmlProperties xmlProperties) {
return new StandardXmlMapperBuilderCustomizer(this.jacksonProperties, modules.stream().toList(),
xmlProperties);
}
@Configuration(proxyBeanMethods = false)
@@ -291,6 +395,20 @@ public final class JacksonAutoConfiguration {
}
static final class StandardXmlFactoryBuilderCustomizer
extends AbstractFactoryBuilderCustomizer<XmlFactoryBuilder> implements XmlFactoryBuilderCustomizer {
StandardXmlFactoryBuilderCustomizer(JacksonProperties jacksonProperties) {
super(jacksonProperties);
}
@Override
public void customize(XmlFactoryBuilder xmlFactoryBuilder) {
super.customize(xmlFactoryBuilder);
}
}
static class StandardXmlMapperBuilderCustomizer extends AbstractMapperBuilderCustomizer<XmlMapper.Builder>
implements XmlMapperBuilderCustomizer {
@@ -344,6 +462,46 @@ public final class JacksonAutoConfiguration {
}
abstract static class AbstractFactoryBuilderCustomizer<B extends DecorableTSFBuilder<?, ?>> implements Ordered {
private final JacksonProperties jacksonProperties;
AbstractFactoryBuilderCustomizer(JacksonProperties jacksonProperties) {
this.jacksonProperties = jacksonProperties;
}
@Override
public int getOrder() {
return 0;
}
protected void customize(B builder) {
Constraints constraints = this.jacksonProperties.getFactory().getConstraints();
builder.streamReadConstraints(readConstraintsFrom(constraints.getRead()));
builder.streamWriteConstraints(writeConstraintsFrom(constraints.getWrite()));
}
private StreamReadConstraints readConstraintsFrom(Read read) {
PropertyMapper map = PropertyMapper.get();
StreamReadConstraints.Builder constraintsBuilder = StreamReadConstraints.builder();
map.from(read::getMaxDocumentLength).to(constraintsBuilder::maxDocumentLength);
map.from(read::getMaxNameLength).to(constraintsBuilder::maxNameLength);
map.from(read::getMaxNestingDepth).to(constraintsBuilder::maxNestingDepth);
map.from(read::getMaxNumberLength).to(constraintsBuilder::maxNumberLength);
map.from(read::getMaxStringLength).to(constraintsBuilder::maxStringLength);
map.from(read::getMaxTokenCount).to(constraintsBuilder::maxTokenCount);
return constraintsBuilder.build();
}
private StreamWriteConstraints writeConstraintsFrom(Write write) {
PropertyMapper map = PropertyMapper.get();
StreamWriteConstraints.Builder constraintsBuilder = StreamWriteConstraints.builder();
map.from(write::getMaxNestingDepth).to(constraintsBuilder::maxNestingDepth);
return constraintsBuilder.build();
}
}
abstract static class AbstractMapperBuilderCustomizer<B extends MapperBuilder<?, ?>> implements Ordered {
private final JacksonProperties jacksonProperties;
@@ -137,6 +137,8 @@ public class JacksonProperties {
private final Json json = new Json();
private final Factory factory = new Factory();
public @Nullable String getDateFormat() {
return this.dateFormat;
}
@@ -241,6 +243,10 @@ public class JacksonProperties {
return this.json;
}
public Factory getFactory() {
return this.factory;
}
public enum ConstructorDetectorStrategy {
/**
@@ -319,4 +325,133 @@ public class JacksonProperties {
}
public static class Factory {
private final Constraints constraints = new Constraints();
public Constraints getConstraints() {
return this.constraints;
}
public static class Constraints {
private final Read read = new Read();
private final Write write = new Write();
public Read getRead() {
return this.read;
}
public Write getWrite() {
return this.write;
}
public static class Read {
/**
* Maximum nesting depth. The depth is a count of objects and arrays that
* have not been closed.
*/
private int maxNestingDepth = 500;
/**
* Maximum allowed document length. A value less than or equal to zero
* indicates that any length is acceptable.
*/
private long maxDocumentLength = -1L;
/**
* Maximum allowed token count. A value less than or equal to zero
* indicates that any count is acceptable.
*/
private long maxTokenCount = -1L;
/**
* Maximum number length.
*/
private int maxNumberLength = 1_000;
/**
* Maximum string length.
*/
private int maxStringLength = 20_000_000;
/**
* Maximum name length.
*/
private int maxNameLength = 50_000;
public int getMaxNestingDepth() {
return this.maxNestingDepth;
}
public void setMaxNestingDepth(int maxNestingDepth) {
this.maxNestingDepth = maxNestingDepth;
}
public long getMaxDocumentLength() {
return this.maxDocumentLength;
}
public void setMaxDocumentLength(long maxDocumentLength) {
this.maxDocumentLength = maxDocumentLength;
}
public long getMaxTokenCount() {
return this.maxTokenCount;
}
public void setMaxTokenCount(long maxTokenCount) {
this.maxTokenCount = maxTokenCount;
}
public int getMaxNumberLength() {
return this.maxNumberLength;
}
public void setMaxNumberLength(int maxNumberLength) {
this.maxNumberLength = maxNumberLength;
}
public int getMaxStringLength() {
return this.maxStringLength;
}
public void setMaxStringLength(int maxStringLength) {
this.maxStringLength = maxStringLength;
}
public int getMaxNameLength() {
return this.maxNameLength;
}
public void setMaxNameLength(int maxNameLength) {
this.maxNameLength = maxNameLength;
}
}
public static class Write {
/**
* Maximum nesting depth. The depth is a count of objects and arrays that
* have not been closed.
*/
private int maxNestingDepth = 500;
public int getMaxNestingDepth() {
return this.maxNestingDepth;
}
public void setMaxNestingDepth(int maxNestingDepth) {
this.maxNestingDepth = maxNestingDepth;
}
}
}
}
}
@@ -0,0 +1,39 @@
/*
* Copyright 2012-present 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 tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonFactoryBuilder;
/**
* Callback interface that can be implemented by beans wishing to further customize the
* {@link JsonFactory} through {@link JsonFactoryBuilder} to fine-tune its
* auto-configuration.
*
* @author Andy Wilkinson
* @since 4.1.0
*/
@FunctionalInterface
public interface JsonFactoryBuilderCustomizer {
/**
* Customize the JsonFactoryBuilder.
* @param jsonFactoryBuilder the builder to customize
*/
void customize(JsonFactoryBuilder jsonFactoryBuilder);
}
@@ -0,0 +1,39 @@
/*
* Copyright 2012-present 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 tools.jackson.dataformat.xml.XmlFactory;
import tools.jackson.dataformat.xml.XmlFactoryBuilder;
/**
* Callback interface that can be implemented by beans wishing to further customize the
* {@link XmlFactory} through {@link XmlFactoryBuilder} to fine-tune its
* auto-configuration.
*
* @author Andy Wilkinson
* @since 4.1.0
*/
@FunctionalInterface
public interface XmlFactoryBuilderCustomizer {
/**
* Customize the XmlFactoryBuilder.
* @param xmlFactoryBuilder the builder to customize
*/
void customize(XmlFactoryBuilder xmlFactoryBuilder);
}
@@ -32,8 +32,12 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import tools.jackson.core.JsonGenerator;
import tools.jackson.core.StreamReadConstraints;
import tools.jackson.core.StreamReadFeature;
import tools.jackson.core.StreamWriteConstraints;
import tools.jackson.core.StreamWriteFeature;
import tools.jackson.core.TokenStreamFactory;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonReadFeature;
import tools.jackson.core.json.JsonWriteFeature;
import tools.jackson.databind.DeserializationFeature;
@@ -56,7 +60,9 @@ import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.json.JsonMapper.Builder;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.util.StdDateFormat;
import tools.jackson.dataformat.cbor.CBORFactory;
import tools.jackson.dataformat.cbor.CBORMapper;
import tools.jackson.dataformat.xml.XmlFactory;
import tools.jackson.dataformat.xml.XmlMapper;
import tools.jackson.module.kotlin.KotlinModule;
@@ -70,9 +76,12 @@ import org.springframework.boot.jackson.JacksonMixin;
import org.springframework.boot.jackson.JacksonMixinModule;
import org.springframework.boot.jackson.JacksonMixinModuleEntries;
import org.springframework.boot.jackson.ObjectValueSerializer;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.CborConfiguration.StandardCborFactoryBuilderCustomizer;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.CborConfiguration.StandardCborMapperBuilderCustomizer;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonAutoConfigurationRuntimeHints;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonJsonMapperBuilderCustomizerConfiguration.StandardJsonMapperBuilderCustomizer;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonJsonCustomizerConfiguration.StandardJsonFactoryBuilderCustomizer;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.JacksonJsonCustomizerConfiguration.StandardJsonMapperBuilderCustomizer;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.XmlConfiguration.StandardXmlFactoryBuilderCustomizer;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration.XmlConfiguration.StandardXmlMapperBuilderCustomizer;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ApplicationContext;
@@ -106,6 +115,12 @@ class JacksonAutoConfigurationTests {
protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class));
@EnumSource
@ParameterizedTest
void definesFactory(MapperType mapperType) {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(mapperType.factoryClass));
}
@EnumSource
@ParameterizedTest
void definesMapper(MapperType mapperType) {
@@ -118,6 +133,15 @@ class JacksonAutoConfigurationTests {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(mapperType.builderClass));
}
@EnumSource
@ParameterizedTest
void factoryBacksOffWhenCustomFactoryIsDefined(MapperType mapperType) {
this.contextRunner.withBean("customFactory", mapperType.factoryClass).run((context) -> {
assertThat(context).hasSingleBean(mapperType.factoryClass);
assertThat(context).hasBean("customFactory");
});
}
@EnumSource
@ParameterizedTest
void mapperBacksOffWhenCustomMapperIsDefined(MapperType mapperType) {
@@ -161,6 +185,24 @@ class JacksonAutoConfigurationTests {
.run((context) -> assertThat(context).hasSingleBean(StandardXmlMapperBuilderCustomizer.class));
}
@Test
void standardJsonFactoryBuilderCustomizerDoesNotBackOffWhenCustomizerIsDefined() {
this.contextRunner.withBean(JsonFactoryBuilderCustomizer.class, () -> mock(JsonFactoryBuilderCustomizer.class))
.run((context) -> assertThat(context).hasSingleBean(StandardJsonFactoryBuilderCustomizer.class));
}
@Test
void standardCborFactoryBuilderCustomizerDoesNotBackOffWhenCustomizerIsDefined() {
this.contextRunner.withBean(CborFactoryBuilderCustomizer.class, () -> mock(CborFactoryBuilderCustomizer.class))
.run((context) -> assertThat(context).hasSingleBean(StandardCborFactoryBuilderCustomizer.class));
}
@Test
void standardXmlFactoryBuilderCustomizerDoesNotBackOffWhenCustomizerIsDefined() {
this.contextRunner.withBean(XmlFactoryBuilderCustomizer.class, () -> mock(XmlFactoryBuilderCustomizer.class))
.run((context) -> assertThat(context).hasSingleBean(StandardXmlFactoryBuilderCustomizer.class));
}
@Test
void doubleModuleRegistration() {
this.contextRunner.withUserConfiguration(DoubleModulesConfig.class).run((context) -> {
@@ -814,6 +856,63 @@ class JacksonAutoConfigurationTests {
.doesNotHaveAnyElementsOfTypes(KotlinModule.class));
}
@EnumSource
@ParameterizedTest
void defaultStreamReadConstraintsMatchJacksonDefaults(MapperType mapperType) {
this.contextRunner.run((context) -> {
StreamReadConstraints streamReadConstraints = mapperType.getFactory(context).streamReadConstraints();
assertThat(streamReadConstraints.getMaxDocumentLength())
.isEqualTo(StreamReadConstraints.DEFAULT_MAX_DOC_LEN);
assertThat(streamReadConstraints.getMaxNameLength()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_NAME_LEN);
assertThat(streamReadConstraints.getMaxNestingDepth()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_DEPTH);
assertThat(streamReadConstraints.getMaxNumberLength()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_NUM_LEN);
assertThat(streamReadConstraints.getMaxStringLength())
.isEqualTo(StreamReadConstraints.DEFAULT_MAX_STRING_LEN);
assertThat(streamReadConstraints.getMaxTokenCount())
.isEqualTo(StreamReadConstraints.DEFAULT_MAX_TOKEN_COUNT);
});
}
@EnumSource
@ParameterizedTest
void customStreamReadConstraintsAreAppliedToAutoConfiguredFactory(MapperType mapperType) {
this.contextRunner
.withPropertyValues("spring.jackson.factory.constraints.read.max-document-length=1000",
"spring.jackson.factory.constraints.read.max-name-length=1001",
"spring.jackson.factory.constraints.read.max-nesting-depth=1002",
"spring.jackson.factory.constraints.read.max-number-length=1003",
"spring.jackson.factory.constraints.read.max-string-length=1004",
"spring.jackson.factory.constraints.read.max-token-count=1005")
.run((context) -> {
StreamReadConstraints streamReadConstraints = mapperType.getFactory(context).streamReadConstraints();
assertThat(streamReadConstraints.getMaxDocumentLength()).isEqualTo(1000);
assertThat(streamReadConstraints.getMaxNameLength()).isEqualTo(1001);
assertThat(streamReadConstraints.getMaxNestingDepth()).isEqualTo(1002);
assertThat(streamReadConstraints.getMaxNumberLength()).isEqualTo(1003);
assertThat(streamReadConstraints.getMaxStringLength()).isEqualTo(1004);
assertThat(streamReadConstraints.getMaxTokenCount()).isEqualTo(1005);
});
}
@EnumSource
@ParameterizedTest
void customStreamWriteConstraintsAreAppliedToAutoConfiguredFactory(MapperType mapperType) {
this.contextRunner.withPropertyValues("spring.jackson.factory.constraints.write.max-nesting-depth=1000")
.run((context) -> {
StreamWriteConstraints streamWriteConstraints = mapperType.getFactory(context).streamWriteConstraints();
assertThat(streamWriteConstraints.getMaxNestingDepth()).isEqualTo(1000);
});
}
@EnumSource
@ParameterizedTest
void defaultStreamWriteConstraintsMatchJacksonDefaults(MapperType mapperType) {
this.contextRunner.run((context) -> {
StreamWriteConstraints streamWriteConstraints = mapperType.getFactory(context).streamWriteConstraints();
assertThat(streamWriteConstraints.getMaxNestingDepth()).isEqualTo(StreamReadConstraints.DEFAULT_MAX_DEPTH);
});
}
static class MyDateFormat extends SimpleDateFormat {
MyDateFormat() {
@@ -1137,15 +1236,19 @@ class JacksonAutoConfigurationTests {
enum MapperType {
CBOR(CBORMapper.class, CBORMapper.Builder.class), JSON(JsonMapper.class, JsonMapper.Builder.class),
XML(XmlMapper.class, XmlMapper.Builder.class);
CBOR(CBORFactory.class, CBORMapper.class, CBORMapper.Builder.class),
JSON(JsonFactory.class, JsonMapper.class, JsonMapper.Builder.class),
XML(XmlFactory.class, XmlMapper.class, XmlMapper.Builder.class);
private final Class<? extends TokenStreamFactory> factoryClass;
private final Class<? extends ObjectMapper> mapperClass;
private final Class<? extends MapperBuilder<?, ?>> builderClass;
<M extends ObjectMapper, B extends MapperBuilder<M, B>> MapperType(Class<M> mapperClass,
Class<B> builderClass) {
<F extends TokenStreamFactory, M extends ObjectMapper, B extends MapperBuilder<M, B>> MapperType(
Class<F> factoryClass, Class<M> mapperClass, Class<B> builderClass) {
this.factoryClass = factoryClass;
this.mapperClass = mapperClass;
this.builderClass = builderClass;
}
@@ -1154,6 +1257,10 @@ class JacksonAutoConfigurationTests {
return context.getBean(this.mapperClass);
}
TokenStreamFactory getFactory(ApplicationContext context) {
return context.getBean(this.mapperClass).tokenStreamFactory();
}
}
}