Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Introduce Kotlin Serialization auto-configuration #46546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
dmitrysulman wants to merge 1 commit into spring-projects:main
base: main
Choose a base branch
Loading
from dmitrysulman:kotlin-serialization-auto-configuration
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ private void mailPrefixes(Config config) {
private void jsonPrefixes(Config config) {
config.accept("spring.jackson");
config.accept("spring.gson");
config.accept("spring.kotlin-serialization");
}

private void dataPrefixes(Config config) {
Expand Down
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Spring Boot provides integration with three JSON mapping libraries:
- Gson
- Jackson 3
- JSON-B
- Kotlin Serialization

Jackson is the preferred and default library.

Expand Down Expand Up @@ -68,3 +69,12 @@ To take more control, one or more javadoc:org.springframework.boot.autoconfigure
Auto-configuration for JSON-B is provided.
When the JSON-B API and an implementation are on the classpath a javadoc:jakarta.json.bind.Jsonb[] bean will be automatically configured.
The preferred JSON-B implementation is Eclipse Yasson for which dependency management is provided.



[[features.json.kotlin-serialization]]
== Kotlin Serialization

Auto-configuration for Kotlin Serialization is provided.
When `kotlinx-serialization-json` is on the classpath a https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/[Json] bean is automatically configured.
Several `+spring.kotlin-serialization.*+` configuration properties are provided for customizing the configuration.
3 changes: 3 additions & 0 deletions module/spring-boot-autoconfigure-classic-modules/build.gradle
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ dependencies {
api(project(":module:spring-boot-kafka")) {
transitive = false
}
api(project(":module:spring-boot-kotlin-serialization")) {
transitive = false
}
api(project(":module:spring-boot-ldap")) {
transitive = false
}
Expand Down
1 change: 1 addition & 0 deletions module/spring-boot-http-converter/build.gradle
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies {
optional(project(":module:spring-boot-gson"))
optional(project(":module:spring-boot-jackson"))
optional(project(":module:spring-boot-jsonb"))
optional(project(":module:spring-boot-kotlin-serialization"))
optional("com.google.code.gson:gson")
optional("jakarta.json.bind:jakarta.json.bind-api")
optional("org.springframework:spring-webmvc")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ static class JsonbPreferred {

}

@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "kotlin-serialization")
static class KotlinxSerialization {

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ public class HttpMessageConverters implements Iterable<HttpMessageConverter<?>>
MultiValueMap<Class<?>, Class<?>> equivalentConverters = new LinkedMultiValueMap<>();
putIfExists(equivalentConverters, "org.springframework.http.converter.json.JacksonJsonHttpMessageConverter",
"org.springframework.http.converter.json.MappingJackson2HttpMessageConverter",
"org.springframework.http.converter.json.GsonHttpMessageConverter");
"org.springframework.http.converter.json.GsonHttpMessageConverter",
"org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter");
putIfExists(equivalentConverters, "org.springframework.http.converter.json.MappingJackson2HttpMessageConverter",
"org.springframework.http.converter.json.GsonHttpMessageConverter");
"org.springframework.http.converter.json.GsonHttpMessageConverter",
"org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter");
EQUIVALENT_CONVERTERS = CollectionUtils.unmodifiableMultiValueMap(equivalentConverters);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,17 @@
* @author Sebastien Deleuze
* @author Stephane Nicoll
* @author Eddú Meléndez
* @author Dmitry Sulman
* @since 4.0.0
*/
@AutoConfiguration(afterName = { "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration",
"org.springframework.boot.jsonb.autoconfigure.JsonbAutoConfiguration",
"org.springframework.boot.gson.autoconfigure.GsonAutoConfiguration" })
"org.springframework.boot.gson.autoconfigure.GsonAutoConfiguration",
"org.springframework.boot.kotlin.serialization.autoconfigure.KotlinSerializationAutoConfiguration" })
@ConditionalOnClass(HttpMessageConverter.class)
@Conditional(NotReactiveWebApplicationCondition.class)
@Import({ JacksonHttpMessageConvertersConfiguration.class, GsonHttpMessageConvertersConfiguration.class,
JsonbHttpMessageConvertersConfiguration.class })
JsonbHttpMessageConvertersConfiguration.class, KotlinSerializationHttpMessageConvertersConfiguration.class })
public final class HttpMessageConvertersAutoConfiguration {

static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
Expand Down Expand Up @@ -66,8 +67,32 @@ static class JsonbPreferred {

}

@ConditionalOnMissingBean({ JacksonJsonHttpMessageConverter.class, GsonHttpMessageConverter.class })
static class JacksonAndGsonMissing {
@Conditional(JacksonAndGsonAndKotlinSerializationMissingCondition.class)
static class JacksonAndGsonAndKotlinSerializationMissing {

}

}

private static class JacksonAndGsonAndKotlinSerializationMissingCondition extends NoneNestedConditions {

JacksonAndGsonAndKotlinSerializationMissingCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@ConditionalOnBean(JacksonJsonHttpMessageConverter.class)
static class JacksonAvailable {

}

@ConditionalOnBean(GsonHttpMessageConverter.class)
static class GsonAvailable {

}

@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "kotlin-serialization")
static class KotlinxPreferred {

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.http.converter.autoconfigure;

import kotlinx.serialization.json.Json;

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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;

/**
* Configuration for HTTP message converters that use Kotlin Serialization.
*
* @author Dmitry Sulman
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Json.class)
class KotlinSerializationHttpMessageConvertersConfiguration {

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(Json.class)
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
havingValue = "kotlin-serialization")
static class KotlinSerializationHttpMessageConverterConfiguration {

@Bean
@ConditionalOnMissingBean
KotlinSerializationJsonHttpMessageConverter kotlinSerializationJsonHttpMessageConverter(Json json) {
return new KotlinSerializationJsonHttpMessageConverter(json);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"name": "spring.http.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', 'jsonb' and 'kotlin-serialization'. When other json mapping libraries are present, use a custom HttpMessageConverters bean to control the preferred mapper."
}
],
"hints": [
Expand All @@ -19,6 +19,9 @@
},
{
"value": "jsonb"
},
{
"value": "kotlin-serialization"
}
],
"providers": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.google.gson.Gson;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import kotlinx.serialization.json.Json;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
Expand All @@ -46,6 +47,7 @@
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -61,6 +63,7 @@
* @author Eddú Meléndez
* @author Moritz Halbritter
* @author Sebastien Deleuze
* @author Dmitry Sulman
*/
class HttpMessageConvertersAutoConfigurationTests {

Expand Down Expand Up @@ -128,6 +131,7 @@ void gsonCanBePreferred() {
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
});
}

Expand Down Expand Up @@ -159,9 +163,40 @@ void jsonbCanBePreferred() {
assertConverterBeanRegisteredWithHttpMessageConverters(context, JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
});
}

@Test
void kotlinSerializationNotAvailable() {
this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(Json.class);
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
});
}

@Test
void kotlinSerializationCustomConverter() {
this.contextRunner.withUserConfiguration(KotlinSerializationConverterConfig.class)
.withBean(Json.class, () -> Json.Default)
.run(assertConverter(KotlinSerializationJsonHttpMessageConverter.class,
"customKotlinSerializationJsonHttpMessageConverter"));
}

@Test
void kotlinSerializationCanBePreferred() {
allOptionsRunner().withPropertyValues("spring.http.converters.preferred-json-mapper:kotlin-serialization")
.run((context) -> {
assertConverterBeanExists(context, KotlinSerializationJsonHttpMessageConverter.class,
"kotlinSerializationJsonHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context,
KotlinSerializationJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class);
});
}

@Test
void stringDefaultConverter() {
this.contextRunner.run(assertConverter(StringHttpMessageConverter.class, "stringHttpMessageConverter"));
Expand Down Expand Up @@ -205,6 +240,7 @@ void jacksonIsPreferredByDefault() {
assertConverterBeanRegisteredWithHttpMessageConverters(context, JacksonJsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
});
}

Expand All @@ -215,6 +251,7 @@ void gsonIsPreferredIfJacksonIsNotAvailable() {
assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter");
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
});
}

Expand Down Expand Up @@ -266,7 +303,8 @@ void whenEncodingCharsetIsConfiguredThenStringMessageConverterUsesSpecificCharse
private ApplicationContextRunner allOptionsRunner() {
return this.contextRunner.withBean(Gson.class)
.withBean(JsonMapper.class)
.withBean(Jsonb.class, JsonbBuilder::create);
.withBean(Jsonb.class, JsonbBuilder::create)
.withBean(Json.class, () -> Json.Default);
}

private ContextConsumer<AssertableApplicationContext> assertConverter(
Expand Down Expand Up @@ -364,6 +402,16 @@ JsonbHttpMessageConverter customJsonbMessageConverter(Jsonb jsonb) {

}

@Configuration(proxyBeanMethods = false)
static class KotlinSerializationConverterConfig {

@Bean
KotlinSerializationJsonHttpMessageConverter customKotlinSerializationJsonHttpMessageConverter(Json json) {
return new KotlinSerializationJsonHttpMessageConverter(json);
}

}

@Configuration(proxyBeanMethods = false)
static class StringConverterConfig {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.springframework.http.converter.cbor.JacksonCborHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;

Expand Down Expand Up @@ -88,6 +89,25 @@ void addBeforeExistingEquivalentConverter() {
JacksonJsonHttpMessageConverter.class);
}

@Test
void addBeforeExistingAnotherEquivalentConverter() {
KotlinSerializationJsonHttpMessageConverter converter1 = new KotlinSerializationJsonHttpMessageConverter();
HttpMessageConverters converters = new HttpMessageConverters(converter1);
Stream<Class<?>> converterClasses = converters.getConverters().stream().map(HttpMessageConverter::getClass);
assertThat(converterClasses).containsSequence(KotlinSerializationJsonHttpMessageConverter.class,
JacksonJsonHttpMessageConverter.class);
}

@Test
void addBeforeExistingMultipleEquivalentConverters() {
GsonHttpMessageConverter converter1 = new GsonHttpMessageConverter();
KotlinSerializationJsonHttpMessageConverter converter2 = new KotlinSerializationJsonHttpMessageConverter();
HttpMessageConverters converters = new HttpMessageConverters(converter1, converter2);
Stream<Class<?>> converterClasses = converters.getConverters().stream().map(HttpMessageConverter::getClass);
assertThat(converterClasses).containsSequence(GsonHttpMessageConverter.class,
KotlinSerializationJsonHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class);
}

@Test
void addNewConverters() {
HttpMessageConverter<?> converter1 = mock(HttpMessageConverter.class);
Expand Down
Loading

AltStyle によって変換されたページ (->オリジナル) /