Move spring.mvc.converters.preferred-json-mapper to spring.http

This commit deprecates spring.mvc.converters.preferred-json-mapper
and replaces it with spring.http.converters.preferred-json-mapper.
If both properties are specified, the latter takes precedence.

Closes gh-44925
This commit is contained in:
Andy Wilkinson 2025-04-02 09:18:26 +01:00
parent f039c73db7
commit c604eaeb56
8 changed files with 209 additions and 28 deletions

View File

@ -0,0 +1,54 @@
/*
* 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.autoconfigure.http;
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.context.annotation.Conditional;
/**
* {@link Conditional @Conditional} that matches based on the preferred JSON mapper. A
* preference is expressed using the {@code spring.http.converters.preferred-json-mapper}
* configuration property, falling back to the
* {@code spring.mvc.converters.preferred-json-mapper} configuration property. When no
* preference is expressed Jackson is preferred by default.
*
* @author Andy Wilkinson
*/
@Conditional(OnPreferredJsonMapperCondition.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@interface ConditionalOnPreferredJsonMapper {
JsonMapper value();
enum JsonMapper {
GSON,
JACKSON,
JSONB,
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -22,8 +22,8 @@ import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.boot.autoconfigure.http.ConditionalOnPreferredJsonMapper.JsonMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@ -61,8 +61,7 @@ class GsonHttpMessageConvertersConfiguration {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "gson")
@ConditionalOnPreferredJsonMapper(JsonMapper.GSON)
static class GsonPreferred {
}
@ -85,8 +84,7 @@ class GsonHttpMessageConvertersConfiguration {
}
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "jsonb")
@ConditionalOnPreferredJsonMapper(JsonMapper.JSONB)
static class JsonbPreferred {
}

View File

@ -66,8 +66,6 @@ import org.springframework.http.converter.StringHttpMessageConverter;
@ImportRuntimeHints(HttpMessageConvertersAutoConfigurationRuntimeHints.class)
public class HttpMessageConvertersAutoConfiguration {
static final String PREFERRED_MAPPER_PROPERTY = "spring.mvc.converters.preferred-json-mapper";
@Bean
@ConditionalOnMissingBean
public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* 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.
@ -22,7 +22,7 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.ConditionalOnPreferredJsonMapper.JsonMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@ -40,8 +40,7 @@ class JacksonHttpMessageConvertersConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnBean(ObjectMapper.class)
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "jackson", matchIfMissing = true)
@ConditionalOnPreferredJsonMapper(JsonMapper.JACKSON)
static class MappingJackson2HttpMessageConverterConfiguration {
@Bean

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* 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.
@ -22,7 +22,7 @@ import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.ConditionalOnPreferredJsonMapper.JsonMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
@ -60,8 +60,7 @@ class JsonbHttpMessageConvertersConfiguration {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "jsonb")
@ConditionalOnPreferredJsonMapper(JsonMapper.JSONB)
static class JsonbPreferred {
}

View File

@ -0,0 +1,76 @@
/*
* 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.autoconfigure.http;
import java.util.Locale;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.http.ConditionalOnPreferredJsonMapper.JsonMapper;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* {@link SpringBootCondition} for
* {@link ConditionalOnPreferredJsonMapper @ConditionalOnPreferredJsonMapper}.
*
* @author Andy Wilkinson
*/
class OnPreferredJsonMapperCondition extends SpringBootCondition {
private static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper";
@Deprecated(since = "3.5.0", forRemoval = true)
private static final String DEPRECATED_PREFERRED_MAPPER_PROPERTY = "spring.mvc.converters.preferred-json-mapper";
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
JsonMapper conditionMapper = metadata.getAnnotations()
.get(ConditionalOnPreferredJsonMapper.class)
.getEnum("value", JsonMapper.class);
ConditionOutcome outcome = getMatchOutcome(context.getEnvironment(), PREFERRED_MAPPER_PROPERTY,
conditionMapper);
if (outcome != null) {
return outcome;
}
outcome = getMatchOutcome(context.getEnvironment(), DEPRECATED_PREFERRED_MAPPER_PROPERTY, conditionMapper);
if (outcome != null) {
return outcome;
}
ConditionMessage message = ConditionMessage
.forCondition(ConditionalOnPreferredJsonMapper.class, conditionMapper.name())
.because("no property was configured and Jackson is the default");
return (conditionMapper == JsonMapper.JACKSON) ? ConditionOutcome.match(message)
: ConditionOutcome.noMatch(message);
}
private ConditionOutcome getMatchOutcome(Environment environment, String key, JsonMapper conditionMapper) {
String property = environment.getProperty(key);
if (property == null) {
return null;
}
JsonMapper configuredMapper = JsonMapper.valueOf(property.toUpperCase(Locale.ROOT));
ConditionMessage message = ConditionMessage
.forCondition(ConditionalOnPreferredJsonMapper.class, configuredMapper.name())
.because("property '%s' had the value '%s'".formatted(key, property));
return (configuredMapper == conditionMapper) ? ConditionOutcome.match(message)
: ConditionOutcome.noMatch(message);
}
}

View File

@ -1615,11 +1615,8 @@
{
"name": "spring.http.converters.preferred-json-mapper",
"type": "java.lang.String",
"description": "Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment.",
"deprecation": {
"replacement": "spring.mvc.converters.preferred-json-mapper",
"level": "error"
}
"defaultValue": "jackson",
"description": "Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment. Supported values are 'jackson', 'gson', and 'jsonb'. When other json mapping libraries (such as kotlinx.serialization) are present, use a custom HttpMessageConverters bean to control the preferred mapper."
},
{
"name": "spring.http.encoding.charset",
@ -2107,7 +2104,11 @@
"name": "spring.mvc.converters.preferred-json-mapper",
"type": "java.lang.String",
"defaultValue": "jackson",
"description": "Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment. Supported values are 'jackson', 'gson', and 'jsonb'. When other json mapping libraries (such as kotlinx.serialization) are present, use a custom HttpMessageConverters bean to control the preferred mapper."
"description": "Preferred JSON mapper to use for HTTP message conversion. By default, auto-detected according to the environment. Supported values are 'jackson', 'gson', and 'jsonb'. When other json mapping libraries (such as kotlinx.serialization) are present, use a custom HttpMessageConverters bean to control the preferred mapper.",
"deprecation": {
"replacement": "spring.http.converters.preferred-json-mapper",
"level": "error"
}
},
{
"name": "spring.mvc.date-format",

View File

@ -32,7 +32,9 @@ import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConf
import org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration.MappingJackson2HttpMessageConverterConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@ -130,12 +132,41 @@ class HttpMessageConvertersAutoConfigurationTests {
@Test
void gsonCanBePreferred() {
allOptionsRunner().withPropertyValues("spring.mvc.converters.preferred-json-mapper:gson").run((context) -> {
assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(MappingJackson2HttpMessageConverter.class);
});
allOptionsRunner().withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.withPropertyValues("spring.http.converters.preferred-json-mapper:gson")
.run((context) -> {
assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(MappingJackson2HttpMessageConverter.class);
});
}
@Test
@Deprecated(since = "3.5.0", forRemoval = true)
void gsonCanBePreferredWithDeprecatedProperty() {
allOptionsRunner().withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.withPropertyValues("spring.mvc.converters.preferred-json-mapper:gson")
.run((context) -> {
assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(MappingJackson2HttpMessageConverter.class);
});
}
@Test
@Deprecated(since = "3.5.0", forRemoval = true)
void gsonCanBePreferredWithNonDeprecatedPropertyTakingPrecedence() {
allOptionsRunner().withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.withPropertyValues("spring.http.converters.preferred-json-mapper:gson",
"spring.mvc.converters.preferred-json-mapper:jackson")
.run((context) -> {
assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(MappingJackson2HttpMessageConverter.class);
});
}
@Test
@ -169,6 +200,31 @@ class HttpMessageConvertersAutoConfigurationTests {
});
}
@Test
@Deprecated(since = "3.5.0", forRemoval = true)
void jsonbCanBePreferredWithDeprecatedProperty() {
allOptionsRunner().withPropertyValues("spring.http.converters.preferred-json-mapper:jsonb").run((context) -> {
assertConverterBeanExists(context, JsonbHttpMessageConverter.class, "jsonbHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context, JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(MappingJackson2HttpMessageConverter.class);
});
}
@Test
@Deprecated(since = "3.5.0", forRemoval = true)
void jsonbCanBePreferredWithNonDeprecatedPropertyTakingPrecedence() {
allOptionsRunner()
.withPropertyValues("spring.http.converters.preferred-json-mapper:jsonb",
"spring.mvc.converters.preferred-json-mapper:gson")
.run((context) -> {
assertConverterBeanExists(context, JsonbHttpMessageConverter.class, "jsonbHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context, JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(MappingJackson2HttpMessageConverter.class);
});
}
@Test
void stringDefaultConverter() {
this.contextRunner.run(assertConverter(StringHttpMessageConverter.class, "stringHttpMessageConverter"));