Skip to content

Commit 456d741

Browse files
committed
Add support for configuring Jackson's ConstructorDetector
Closes gh-27178
1 parent 60e57f7 commit 456d741

File tree

5 files changed

+146
-0
lines changed

5 files changed

+146
-0
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@
3434
import com.fasterxml.jackson.databind.ObjectMapper;
3535
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
3636
import com.fasterxml.jackson.databind.SerializationFeature;
37+
import com.fasterxml.jackson.databind.cfg.ConstructorDetector;
3738
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
3839

3940
import org.springframework.beans.BeanUtils;
4041
import org.springframework.beans.factory.BeanFactoryUtils;
4142
import org.springframework.beans.factory.ListableBeanFactory;
4243
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
4344
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
45+
import org.springframework.boot.autoconfigure.jackson.JacksonProperties.ConstructorDetectorStrategy;
4446
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4547
import org.springframework.boot.jackson.JsonComponentModule;
4648
import org.springframework.context.ApplicationContext;
@@ -188,6 +190,7 @@ public void customize(Jackson2ObjectMapperBuilder builder) {
188190
configureModules(builder);
189191
configureLocale(builder);
190192
configureDefaultLeniency(builder);
193+
configureConstructorDetector(builder);
191194
}
192195

193196
private void configureFeatures(Jackson2ObjectMapperBuilder builder, Map<?, Boolean> features) {
@@ -297,6 +300,27 @@ private void configureDefaultLeniency(Jackson2ObjectMapperBuilder builder) {
297300
}
298301
}
299302

303+
private void configureConstructorDetector(Jackson2ObjectMapperBuilder builder) {
304+
ConstructorDetectorStrategy strategy = this.jacksonProperties.getConstructorDetector();
305+
if (strategy != null) {
306+
builder.postConfigurer((objectMapper) -> {
307+
switch (strategy) {
308+
case USE_PROPERTIES_BASED:
309+
objectMapper.setConstructorDetector(ConstructorDetector.USE_PROPERTIES_BASED);
310+
break;
311+
case USE_DELEGATING:
312+
objectMapper.setConstructorDetector(ConstructorDetector.USE_DELEGATING);
313+
break;
314+
case EXPLICIT_ONLY:
315+
objectMapper.setConstructorDetector(ConstructorDetector.EXPLICIT_ONLY);
316+
break;
317+
default:
318+
objectMapper.setConstructorDetector(ConstructorDetector.DEFAULT);
319+
}
320+
});
321+
}
322+
}
323+
300324
private static <T> Collection<T> getBeans(ListableBeanFactory beanFactory, Class<T> type) {
301325
return BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type).values();
302326
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ public class JacksonProperties {
9797
*/
9898
private Boolean defaultLeniency;
9999

100+
/**
101+
* Strategy to use to to auto-detect constructor, and in particular behavior with
102+
* single-argument constructors.
103+
*/
104+
private ConstructorDetectorStrategy constructorDetector;
105+
100106
/**
101107
* Time zone used when formatting dates. For instance, "America/Los_Angeles" or
102108
* "GMT+10".
@@ -164,6 +170,14 @@ public void setDefaultLeniency(Boolean defaultLeniency) {
164170
this.defaultLeniency = defaultLeniency;
165171
}
166172

173+
public ConstructorDetectorStrategy getConstructorDetector() {
174+
return this.constructorDetector;
175+
}
176+
177+
public void setConstructorDetector(ConstructorDetectorStrategy constructorDetector) {
178+
this.constructorDetector = constructorDetector;
179+
}
180+
167181
public TimeZone getTimeZone() {
168182
return this.timeZone;
169183
}
@@ -180,4 +194,29 @@ public void setLocale(Locale locale) {
180194
this.locale = locale;
181195
}
182196

197+
public enum ConstructorDetectorStrategy {
198+
199+
/**
200+
* Use heuristics to see if "properties" mode is to be used.
201+
*/
202+
DEFAULT,
203+
204+
/**
205+
* Assume "properties" mode if not explicitly annotated otherwise.
206+
*/
207+
USE_PROPERTIES_BASED,
208+
209+
/**
210+
* Assume "delegating" mode if not explicitly annotated otherwise.
211+
*/
212+
USE_DELEGATING,
213+
214+
/**
215+
* Refuse to decide implicit mode and instead throw an InvalidDefinitionException
216+
* for ambiguous cases.
217+
*/
218+
EXPLICIT_ONLY;
219+
220+
}
221+
183222
}

spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,10 @@
10481048
"name": "spring.integration.jdbc.initialize-schema",
10491049
"defaultValue": "embedded"
10501050
},
1051+
{
1052+
"name": "spring.jackson.constructor-detector",
1053+
"defaultValue": "default"
1054+
},
10511055
{
10521056
"name": "spring.jackson.joda-date-time-format",
10531057
"type": "java.lang.String",

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/Jackson211AutoConfigurationTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,25 @@ void customPropertyNamingStrategyClass() {
5454
});
5555
}
5656

57+
// ConstructorDetector only available as of Jackson 2.12
58+
@Override
59+
void constructorDetectorWithNoStrategyUseDefault() {
60+
}
61+
62+
@Override
63+
void constructorDetectorWithDefaultStrategy() {
64+
}
65+
66+
@Override
67+
void constructorDetectorWithUsePropertiesBasedStrategy() {
68+
}
69+
70+
@Override
71+
void constructorDetectorWithUseDelegatingStrategy() {
72+
}
73+
74+
@Override
75+
void constructorDetectorWithExplicitOnlyStrategy() {
76+
}
77+
5778
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
4242
import com.fasterxml.jackson.databind.SerializationFeature;
4343
import com.fasterxml.jackson.databind.SerializerProvider;
44+
import com.fasterxml.jackson.databind.cfg.ConstructorDetector;
45+
import com.fasterxml.jackson.databind.cfg.ConstructorDetector.SingleArgConstructor;
4446
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
4547
import com.fasterxml.jackson.databind.module.SimpleModule;
4648
import com.fasterxml.jackson.databind.util.StdDateFormat;
@@ -323,6 +325,62 @@ void disableDefaultLeniency() {
323325
});
324326
}
325327

328+
@Test
329+
void constructorDetectorWithNoStrategyUseDefault() {
330+
this.contextRunner.run((context) -> {
331+
ObjectMapper mapper = context.getBean(ObjectMapper.class);
332+
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
333+
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.HEURISTIC);
334+
assertThat(cd.requireCtorAnnotation()).isFalse();
335+
assertThat(cd.allowJDKTypeConstructors()).isFalse();
336+
});
337+
}
338+
339+
@Test
340+
void constructorDetectorWithDefaultStrategy() {
341+
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=default").run((context) -> {
342+
ObjectMapper mapper = context.getBean(ObjectMapper.class);
343+
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
344+
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.HEURISTIC);
345+
assertThat(cd.requireCtorAnnotation()).isFalse();
346+
assertThat(cd.allowJDKTypeConstructors()).isFalse();
347+
});
348+
}
349+
350+
@Test
351+
void constructorDetectorWithUsePropertiesBasedStrategy() {
352+
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=use-properties-based")
353+
.run((context) -> {
354+
ObjectMapper mapper = context.getBean(ObjectMapper.class);
355+
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
356+
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.PROPERTIES);
357+
assertThat(cd.requireCtorAnnotation()).isFalse();
358+
assertThat(cd.allowJDKTypeConstructors()).isFalse();
359+
});
360+
}
361+
362+
@Test
363+
void constructorDetectorWithUseDelegatingStrategy() {
364+
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=use-delegating").run((context) -> {
365+
ObjectMapper mapper = context.getBean(ObjectMapper.class);
366+
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
367+
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.DELEGATING);
368+
assertThat(cd.requireCtorAnnotation()).isFalse();
369+
assertThat(cd.allowJDKTypeConstructors()).isFalse();
370+
});
371+
}
372+
373+
@Test
374+
void constructorDetectorWithExplicitOnlyStrategy() {
375+
this.contextRunner.withPropertyValues("spring.jackson.constructor-detector=explicit-only").run((context) -> {
376+
ObjectMapper mapper = context.getBean(ObjectMapper.class);
377+
ConstructorDetector cd = mapper.getDeserializationConfig().getConstructorDetector();
378+
assertThat(cd.singleArgMode()).isEqualTo(SingleArgConstructor.REQUIRE_MODE);
379+
assertThat(cd.requireCtorAnnotation()).isFalse();
380+
assertThat(cd.allowJDKTypeConstructors()).isFalse();
381+
});
382+
}
383+
326384
@Test
327385
void additionalJacksonBuilderCustomization() {
328386
this.contextRunner.withUserConfiguration(ObjectMapperBuilderCustomConfig.class).run((context) -> {

0 commit comments

Comments
 (0)