Introduce Kotlin Serialization auto-configuration
See gh-46546 Signed-off-by: Dmitry Sulman <dmitry.sulman@gmail.com>
This commit is contained in:
parent
4b0b9e5e36
commit
c7621bb6be
|
@ -118,6 +118,7 @@ public abstract class DocumentConfigurationProperties extends DefaultTask {
|
|||
private void jsonPrefixes(Config config) {
|
||||
config.accept("spring.jackson");
|
||||
config.accept("spring.gson");
|
||||
config.accept("spring.kotlin-serialization");
|
||||
}
|
||||
|
||||
private void dataPrefixes(Config config) {
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -91,6 +91,12 @@ class GsonHttpMessageConvertersConfiguration {
|
|||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(name = HttpMessageConvertersAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
|
||||
havingValue = "kotlin-serialization")
|
||||
static class KotlinxSerialization {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -45,15 +45,17 @@ import org.springframework.http.converter.StringHttpMessageConverter;
|
|||
* @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";
|
||||
|
|
|
@ -23,6 +23,7 @@ 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.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
@ -66,8 +67,32 @@ class JsonbHttpMessageConvertersConfiguration {
|
|||
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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": [
|
||||
|
@ -19,6 +19,9 @@
|
|||
},
|
||||
{
|
||||
"value": "jsonb"
|
||||
},
|
||||
{
|
||||
"value": "kotlin-serialization"
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets;
|
|||
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;
|
||||
|
@ -46,6 +47,7 @@ import org.springframework.http.converter.StringHttpMessageConverter;
|
|||
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;
|
||||
|
@ -61,6 +63,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
* @author Eddú Meléndez
|
||||
* @author Moritz Halbritter
|
||||
* @author Sebastien Deleuze
|
||||
* @author Dmitry Sulman
|
||||
*/
|
||||
class HttpMessageConvertersAutoConfigurationTests {
|
||||
|
||||
|
@ -128,6 +131,7 @@ class HttpMessageConvertersAutoConfigurationTests {
|
|||
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class);
|
||||
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
|
||||
assertThat(context).doesNotHaveBean(JacksonJsonHttpMessageConverter.class);
|
||||
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -159,6 +163,37 @@ class HttpMessageConvertersAutoConfigurationTests {
|
|||
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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -205,6 +240,7 @@ class HttpMessageConvertersAutoConfigurationTests {
|
|||
assertConverterBeanRegisteredWithHttpMessageConverters(context, JacksonJsonHttpMessageConverter.class);
|
||||
assertThat(context).doesNotHaveBean(GsonHttpMessageConverter.class);
|
||||
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
|
||||
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -215,6 +251,7 @@ class HttpMessageConvertersAutoConfigurationTests {
|
|||
assertConverterBeanExists(context, GsonHttpMessageConverter.class, "gsonHttpMessageConverter");
|
||||
assertConverterBeanRegisteredWithHttpMessageConverters(context, GsonHttpMessageConverter.class);
|
||||
assertThat(context).doesNotHaveBean(JsonbHttpMessageConverter.class);
|
||||
assertThat(context).doesNotHaveBean(KotlinSerializationJsonHttpMessageConverter.class);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -266,7 +303,8 @@ class HttpMessageConvertersAutoConfigurationTests {
|
|||
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(
|
||||
|
@ -364,6 +402,16 @@ class HttpMessageConvertersAutoConfigurationTests {
|
|||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class KotlinSerializationConverterConfig {
|
||||
|
||||
@Bean
|
||||
KotlinSerializationJsonHttpMessageConverter customKotlinSerializationJsonHttpMessageConverter(Json json) {
|
||||
return new KotlinSerializationJsonHttpMessageConverter(json);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class StringConverterConfig {
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.springframework.http.converter.StringHttpMessageConverter;
|
|||
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;
|
||||
|
||||
|
@ -88,6 +89,25 @@ class HttpMessageConvertersTests {
|
|||
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);
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import org.springframework.boot.build.autoconfigure.CheckAutoConfigurationClasses
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id "java-library"
|
||||
id "org.jetbrains.kotlin.jvm"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "org.springframework.boot.auto-configuration"
|
||||
id "org.springframework.boot.configuration-properties"
|
||||
id "org.springframework.boot.deployed"
|
||||
id "org.springframework.boot.optional-dependencies"
|
||||
}
|
||||
|
||||
description = "Spring Boot Kotlin Serialization"
|
||||
|
||||
dependencies {
|
||||
api(project(":core:spring-boot"))
|
||||
api("org.jetbrains.kotlinx:kotlinx-serialization-json")
|
||||
|
||||
optional(project(":core:spring-boot-autoconfigure"))
|
||||
|
||||
testImplementation(project(":core:spring-boot-test"))
|
||||
testImplementation(project(":test-support:spring-boot-test-support"))
|
||||
|
||||
testRuntimeOnly("ch.qos.logback:logback-classic")
|
||||
testRuntimeOnly("org.jetbrains.kotlin:kotlin-reflect")
|
||||
}
|
||||
|
||||
tasks.named("checkAutoConfigurationClasses", CheckAutoConfigurationClasses.class) {
|
||||
doFirst {
|
||||
classpath = classpath.filter { !it.path.contains('/build/classes/kotlin/main') }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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.kotlin.serialization.autoconfigure;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import kotlinx.serialization.json.Json;
|
||||
import kotlinx.serialization.json.JsonBuilder;
|
||||
import kotlinx.serialization.json.JsonKt;
|
||||
import kotlinx.serialization.json.JsonNamingStrategy;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
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.context.annotation.Bean;
|
||||
import org.springframework.core.Ordered;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Kotlin Serialization.
|
||||
*
|
||||
* @author Dmitry Sulman
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(Json.class)
|
||||
@EnableConfigurationProperties(KotlinSerializationProperties.class)
|
||||
public final class KotlinSerializationAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
Json kotlinSerializationJson(List<KotlinSerializationJsonBuilderCustomizer> customizers) {
|
||||
Function1<JsonBuilder, Unit> builderAction = (jsonBuilder) -> {
|
||||
customizers.forEach((c) -> c.customize(jsonBuilder));
|
||||
return Unit.INSTANCE;
|
||||
};
|
||||
return JsonKt.Json(Json.Default, builderAction);
|
||||
}
|
||||
|
||||
@Bean
|
||||
StandardKotlinSerializationJsonBuilderCustomizer standardKotlinSerializationJsonBuilderCustomizer(
|
||||
KotlinSerializationProperties kotlinSerializationProperties) {
|
||||
return new StandardKotlinSerializationJsonBuilderCustomizer(kotlinSerializationProperties);
|
||||
}
|
||||
|
||||
static final class StandardKotlinSerializationJsonBuilderCustomizer
|
||||
implements KotlinSerializationJsonBuilderCustomizer, Ordered {
|
||||
|
||||
private final KotlinSerializationProperties properties;
|
||||
|
||||
StandardKotlinSerializationJsonBuilderCustomizer(KotlinSerializationProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(JsonBuilder jsonBuilder) {
|
||||
KotlinSerializationProperties properties = this.properties;
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(properties::getNamingStrategy).to(setNamingStrategy(jsonBuilder));
|
||||
map.from(properties::getPrettyPrint).to(jsonBuilder::setPrettyPrint);
|
||||
map.from(properties::getLenient).to(jsonBuilder::setLenient);
|
||||
map.from(properties::getIgnoreUnknownKeys).to(jsonBuilder::setIgnoreUnknownKeys);
|
||||
map.from(properties::getEncodeDefaults).to(jsonBuilder::setEncodeDefaults);
|
||||
map.from(properties::getExplicitNulls).to(jsonBuilder::setExplicitNulls);
|
||||
map.from(properties::getCoerceInputValues).to(jsonBuilder::setCoerceInputValues);
|
||||
map.from(properties::getAllowStructuredMapKeys).to(jsonBuilder::setAllowStructuredMapKeys);
|
||||
map.from(properties::getAllowSpecialFloatingPointValues)
|
||||
.to(jsonBuilder::setAllowSpecialFloatingPointValues);
|
||||
map.from(properties::getClassDiscriminator).to(jsonBuilder::setClassDiscriminator);
|
||||
map.from(properties::getClassDiscriminatorMode).to(jsonBuilder::setClassDiscriminatorMode);
|
||||
map.from(properties::getDecodeEnumsCaseInsensitive).to(jsonBuilder::setDecodeEnumsCaseInsensitive);
|
||||
map.from(properties::getUseAlternativeNames).to(jsonBuilder::setUseAlternativeNames);
|
||||
map.from(properties::getAllowTrailingComma).to(jsonBuilder::setAllowTrailingComma);
|
||||
map.from(properties::getAllowComments).to(jsonBuilder::setAllowComments);
|
||||
}
|
||||
|
||||
private Consumer<KotlinSerializationProperties.JsonNamingStrategy> setNamingStrategy(JsonBuilder builder) {
|
||||
return (strategy) -> {
|
||||
JsonNamingStrategy namingStrategy = switch (strategy) {
|
||||
case SNAKE_CASE -> JsonNamingStrategy.Builtins.getSnakeCase();
|
||||
case KEBAB_CASE -> JsonNamingStrategy.Builtins.getKebabCase();
|
||||
};
|
||||
builder.setNamingStrategy(namingStrategy);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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.kotlin.serialization.autoconfigure;
|
||||
|
||||
import kotlinx.serialization.json.Json;
|
||||
import kotlinx.serialization.json.JsonBuilder;
|
||||
|
||||
/**
|
||||
* Callback interface that can be implemented by beans wishing to further customize the
|
||||
* {@link Json} through {@link JsonBuilder} retaining its default configuration.
|
||||
*
|
||||
* @author Dmitry Sulman
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface KotlinSerializationJsonBuilderCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the Kotlin Serialization {@link Json} through {@link JsonBuilder}.
|
||||
* @param jsonBuilder the {@link JsonBuilder} to customize
|
||||
*/
|
||||
void customize(JsonBuilder jsonBuilder);
|
||||
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
* 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.kotlin.serialization.autoconfigure;
|
||||
|
||||
import kotlinx.serialization.json.ClassDiscriminatorMode;
|
||||
import kotlinx.serialization.json.Json;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Configuration properties to configure Kotlin Serialization {@link Json}.
|
||||
*
|
||||
* @author Dmitry Sulman
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@ConfigurationProperties("spring.kotlin-serialization")
|
||||
public class KotlinSerializationProperties {
|
||||
|
||||
/**
|
||||
* Specifies JsonNamingStrategy that should be used for all properties in classes for
|
||||
* serialization and deserialization.
|
||||
*/
|
||||
private @Nullable JsonNamingStrategy namingStrategy;
|
||||
|
||||
/**
|
||||
* Whether resulting JSON should be pretty-printed: formatted and optimized for human
|
||||
* readability.
|
||||
*/
|
||||
private @Nullable Boolean prettyPrint;
|
||||
|
||||
/**
|
||||
* Enable lenient mode that removes JSON specification restriction (RFC-4627) and
|
||||
* makes parser more liberal to the malformed input.
|
||||
*/
|
||||
private @Nullable Boolean lenient;
|
||||
|
||||
/**
|
||||
* Whether encounters of unknown properties in the input JSON should be ignored
|
||||
* instead of throwing SerializationException.
|
||||
*/
|
||||
private @Nullable Boolean ignoreUnknownKeys;
|
||||
|
||||
/**
|
||||
* Whether default values of Kotlin properties should be encoded.
|
||||
*/
|
||||
private @Nullable Boolean encodeDefaults;
|
||||
|
||||
/**
|
||||
* Whether null values should be encoded for nullable properties and must be present
|
||||
* in JSON object during decoding.
|
||||
*/
|
||||
private @Nullable Boolean explicitNulls;
|
||||
|
||||
/**
|
||||
* Enable coercing incorrect JSON values.
|
||||
*/
|
||||
private @Nullable Boolean coerceInputValues;
|
||||
|
||||
/**
|
||||
* Enable structured objects to be serialized as map keys by changing serialized form
|
||||
* of the map from JSON object (key-value pairs) to flat array like [k1, v1, k2, v2].
|
||||
*/
|
||||
private @Nullable Boolean allowStructuredMapKeys;
|
||||
|
||||
/**
|
||||
* Whether to remove JSON specification restriction on special floating-point values
|
||||
* such as 'NaN' and 'Infinity' and enable their serialization and deserialization as
|
||||
* float literals without quotes.
|
||||
*/
|
||||
private @Nullable Boolean allowSpecialFloatingPointValues;
|
||||
|
||||
/**
|
||||
* Name of the class descriptor property for polymorphic serialization.
|
||||
*/
|
||||
private @Nullable String classDiscriminator;
|
||||
|
||||
/**
|
||||
* Defines which classes and objects should have class discriminator added to the
|
||||
* output.
|
||||
*/
|
||||
private @Nullable ClassDiscriminatorMode classDiscriminatorMode;
|
||||
|
||||
/**
|
||||
* Enable decoding enum values in a case-insensitive manner.
|
||||
*/
|
||||
private @Nullable Boolean decodeEnumsCaseInsensitive;
|
||||
|
||||
/**
|
||||
* Whether Json instance makes use of JsonNames annotation.
|
||||
*/
|
||||
private @Nullable Boolean useAlternativeNames;
|
||||
|
||||
/**
|
||||
* Whether to allow parser to accept trailing (ending) commas in JSON objects and
|
||||
* arrays.
|
||||
*/
|
||||
private @Nullable Boolean allowTrailingComma;
|
||||
|
||||
/**
|
||||
* Whether to allow parser to accept C/Java-style comments in JSON input.
|
||||
*/
|
||||
private @Nullable Boolean allowComments;
|
||||
|
||||
public @Nullable JsonNamingStrategy getNamingStrategy() {
|
||||
return this.namingStrategy;
|
||||
}
|
||||
|
||||
public void setNamingStrategy(@Nullable JsonNamingStrategy namingStrategy) {
|
||||
this.namingStrategy = namingStrategy;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getPrettyPrint() {
|
||||
return this.prettyPrint;
|
||||
}
|
||||
|
||||
public void setPrettyPrint(@Nullable Boolean prettyPrint) {
|
||||
this.prettyPrint = prettyPrint;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getLenient() {
|
||||
return this.lenient;
|
||||
}
|
||||
|
||||
public void setLenient(@Nullable Boolean lenient) {
|
||||
this.lenient = lenient;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getIgnoreUnknownKeys() {
|
||||
return this.ignoreUnknownKeys;
|
||||
}
|
||||
|
||||
public void setIgnoreUnknownKeys(@Nullable Boolean ignoreUnknownKeys) {
|
||||
this.ignoreUnknownKeys = ignoreUnknownKeys;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getEncodeDefaults() {
|
||||
return this.encodeDefaults;
|
||||
}
|
||||
|
||||
public void setEncodeDefaults(@Nullable Boolean encodeDefaults) {
|
||||
this.encodeDefaults = encodeDefaults;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getExplicitNulls() {
|
||||
return this.explicitNulls;
|
||||
}
|
||||
|
||||
public void setExplicitNulls(@Nullable Boolean explicitNulls) {
|
||||
this.explicitNulls = explicitNulls;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getCoerceInputValues() {
|
||||
return this.coerceInputValues;
|
||||
}
|
||||
|
||||
public void setCoerceInputValues(@Nullable Boolean coerceInputValues) {
|
||||
this.coerceInputValues = coerceInputValues;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getAllowStructuredMapKeys() {
|
||||
return this.allowStructuredMapKeys;
|
||||
}
|
||||
|
||||
public void setAllowStructuredMapKeys(@Nullable Boolean allowStructuredMapKeys) {
|
||||
this.allowStructuredMapKeys = allowStructuredMapKeys;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getAllowSpecialFloatingPointValues() {
|
||||
return this.allowSpecialFloatingPointValues;
|
||||
}
|
||||
|
||||
public void setAllowSpecialFloatingPointValues(@Nullable Boolean allowSpecialFloatingPointValues) {
|
||||
this.allowSpecialFloatingPointValues = allowSpecialFloatingPointValues;
|
||||
}
|
||||
|
||||
public @Nullable String getClassDiscriminator() {
|
||||
return this.classDiscriminator;
|
||||
}
|
||||
|
||||
public void setClassDiscriminator(@Nullable String classDiscriminator) {
|
||||
this.classDiscriminator = classDiscriminator;
|
||||
}
|
||||
|
||||
public @Nullable ClassDiscriminatorMode getClassDiscriminatorMode() {
|
||||
return this.classDiscriminatorMode;
|
||||
}
|
||||
|
||||
public void setClassDiscriminatorMode(@Nullable ClassDiscriminatorMode classDiscriminatorMode) {
|
||||
this.classDiscriminatorMode = classDiscriminatorMode;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getDecodeEnumsCaseInsensitive() {
|
||||
return this.decodeEnumsCaseInsensitive;
|
||||
}
|
||||
|
||||
public void setDecodeEnumsCaseInsensitive(@Nullable Boolean decodeEnumsCaseInsensitive) {
|
||||
this.decodeEnumsCaseInsensitive = decodeEnumsCaseInsensitive;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getUseAlternativeNames() {
|
||||
return this.useAlternativeNames;
|
||||
}
|
||||
|
||||
public void setUseAlternativeNames(@Nullable Boolean useAlternativeNames) {
|
||||
this.useAlternativeNames = useAlternativeNames;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getAllowTrailingComma() {
|
||||
return this.allowTrailingComma;
|
||||
}
|
||||
|
||||
public void setAllowTrailingComma(@Nullable Boolean allowTrailingComma) {
|
||||
this.allowTrailingComma = allowTrailingComma;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getAllowComments() {
|
||||
return this.allowComments;
|
||||
}
|
||||
|
||||
public void setAllowComments(@Nullable Boolean allowComments) {
|
||||
this.allowComments = allowComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing strategies for JSON property naming. The values correspond to
|
||||
* {@link kotlinx.serialization.json.JsonNamingStrategy} implementations that cannot
|
||||
* be directly referenced.
|
||||
*/
|
||||
public enum JsonNamingStrategy {
|
||||
|
||||
/**
|
||||
* Snake case strategy.
|
||||
*/
|
||||
SNAKE_CASE,
|
||||
|
||||
/**
|
||||
* Kebab case strategy.
|
||||
*/
|
||||
KEBAB_CASE
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration for Kotlin Serialization.
|
||||
*/
|
||||
@NullMarked
|
||||
package org.springframework.boot.kotlin.serialization.autoconfigure;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
|
@ -0,0 +1 @@
|
|||
org.springframework.boot.kotlin.serialization.autoconfigure.KotlinSerializationAutoConfiguration
|
|
@ -0,0 +1,321 @@
|
|||
/*
|
||||
* 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.kotlin.serialization.autoconfigure
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations
|
||||
import org.springframework.boot.test.context.FilteredClassLoader
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
/**
|
||||
* Tests for [KotlinSerializationAutoConfiguration].
|
||||
*
|
||||
* @author Dmitry Sulman
|
||||
*/
|
||||
class KotlinSerializationAutoConfigurationTests {
|
||||
private val contextRunner = ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(KotlinSerializationAutoConfiguration::class.java))
|
||||
|
||||
@Test
|
||||
fun shouldSupplyBean() {
|
||||
this.contextRunner.run { context ->
|
||||
assertThat(context).hasSingleBean(Json::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shouldNotSupplyBean() {
|
||||
this.contextRunner
|
||||
.withClassLoader(FilteredClassLoader(Json::class.java))
|
||||
.run { context ->
|
||||
assertThat(context).doesNotHaveBean(Json::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializeToString() {
|
||||
this.contextRunner.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.encodeToString(DataObject("hello")))
|
||||
.isEqualTo("""{"stringField":"hello"}""")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserializeFromString() {
|
||||
this.contextRunner.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.decodeFromString<DataObject>("""{"stringField":"hello"}"""))
|
||||
.isEqualTo(DataObject("hello"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun customJsonBean() {
|
||||
this.contextRunner
|
||||
.withUserConfiguration(CustomKotlinSerializationConfig::class.java)
|
||||
.run { context ->
|
||||
assertThat(context).hasSingleBean(Json::class.java)
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.encodeToString(DataObject("hello")))
|
||||
.isEqualTo("""{"string_field":"hello"}""")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializeSnakeCase() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.naming-strategy=snake_case")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.encodeToString(DataObject("hello")))
|
||||
.isEqualTo("""{"string_field":"hello"}""")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializeKebabCase() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.naming-strategy=kebab_case")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.encodeToString(DataObject("hello")))
|
||||
.isEqualTo("""{"string-field":"hello"}""")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializePrettyPrint() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.pretty-print=true")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.encodeToString(DataObject("hello")))
|
||||
.isEqualTo(
|
||||
"""
|
||||
{
|
||||
"stringField": "hello"
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("JsonStandardCompliance")
|
||||
fun deserializeLenient() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.lenient=true")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.decodeFromString<DataObject>("""{"stringField":hello}"""))
|
||||
.isEqualTo(DataObject("hello"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserializeIgnoreUnknownKeys() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.ignore-unknown-keys=true")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.decodeFromString<DataObject>("""{"stringField":"hello", "anotherField":"value"}"""))
|
||||
.isEqualTo(DataObject("hello"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializeDefaults() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.encode-defaults=true")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.encodeToString(DataObjectWithDefault("hello")))
|
||||
.isEqualTo("""{"stringField":"hello","defaultField":"default"}""")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializeExplicitNullsFalse() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.explicit-nulls=false")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.encodeToString(DataObjectWithDefault(null, "hello")))
|
||||
.isEqualTo("""{"defaultField":"hello"}""")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserializeCoerceInputValues() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.coerce-input-values=true")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.decodeFromString<DataObjectWithDefault>("""{"stringField":"hello", "defaultField":null}"""))
|
||||
.isEqualTo(DataObjectWithDefault("hello", "default"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializeStructuredMapKeys() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.allow-structured-map-keys=true")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
val map = mapOf(
|
||||
DataObject("key1") to "value1",
|
||||
DataObject("key2") to "value2",
|
||||
)
|
||||
assertThat(json.encodeToString(map))
|
||||
.isEqualTo("""[{"stringField":"key1"},"value1",{"stringField":"key2"},"value2"]""")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializeSpecialFloatingPointValues() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.allow-special-floating-point-values=true")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.encodeToString(DataObjectDouble(Double.NaN)))
|
||||
.isEqualTo("""{"value":NaN}""")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializeClassDiscriminator() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.class-discriminator=class")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
val value: BaseClass = ChildClass("value")
|
||||
assertThat(json.encodeToString(value))
|
||||
.isEqualTo("""{"class":"child","stringField":"value"}""")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun serializeClassDiscriminatorNone() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.class-discriminator-mode=none")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
val value: BaseClass = ChildClass("value")
|
||||
assertThat(json.encodeToString(value))
|
||||
.isEqualTo("""{"stringField":"value"}""")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserializeEnumsCaseInsensitive() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.decode-enums-case-insensitive=true")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.decodeFromString<DataObjectEnumValues>("""{"values":["value_A", "alternative"]}"""))
|
||||
.isEqualTo(DataObjectEnumValues(listOf(EnumValue.VALUE_A, EnumValue.VALUE_B)))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deserializeAlternativeNames() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.use-alternative-names=false")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThatExceptionOfType(SerializationException::class.java).isThrownBy {
|
||||
json.decodeFromString<DataObject>("""{"alternative":"hello"}""")
|
||||
}.withMessageContaining("Encountered an unknown key")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("JsonStandardCompliance")
|
||||
fun deserializeTrailingComma() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.allow-trailing-comma=true")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.decodeFromString<DataObject>("""{"stringField":"hello",}"""))
|
||||
.isEqualTo(DataObject("hello"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("JsonStandardCompliance")
|
||||
fun deserializeComments() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.kotlin-serialization.allow-comments=true")
|
||||
.run { context ->
|
||||
val json = context.getBean(Json::class.java)
|
||||
assertThat(json.decodeFromString<DataObject>("""{"stringField":"hello" /*comment*/}"""))
|
||||
.isEqualTo(DataObject("hello"))
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private data class DataObject(@JsonNames("alternative") private val stringField: String)
|
||||
|
||||
@Serializable
|
||||
private data class DataObjectWithDefault(
|
||||
private val stringField: String?,
|
||||
private val defaultField: String = "default",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class DataObjectDouble(private val value: Double)
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
enum class EnumValue { VALUE_A, @JsonNames("Alternative") VALUE_B }
|
||||
|
||||
@Serializable
|
||||
private data class DataObjectEnumValues(private val values: List<EnumValue>)
|
||||
|
||||
@Serializable
|
||||
sealed class BaseClass {
|
||||
abstract val stringField: String
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("child")
|
||||
class ChildClass(override val stringField: String) : BaseClass()
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class CustomKotlinSerializationConfig {
|
||||
|
||||
@Bean
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun customKotlinSerializationJson(): Json {
|
||||
return Json { namingStrategy = JsonNamingStrategy.SnakeCase }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -2,3 +2,4 @@
|
|||
optional:org.springframework.boot.gson.autoconfigure.GsonAutoConfiguration
|
||||
org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration
|
||||
optional:org.springframework.boot.jsonb.autoconfigure.JsonbAutoConfiguration
|
||||
optional:org.springframework.boot.kotlin.serialization.autoconfigure.KotlinSerializationAutoConfiguration
|
||||
|
|
|
@ -2037,6 +2037,7 @@ bom {
|
|||
"spring-boot-jpa",
|
||||
"spring-boot-jsonb",
|
||||
"spring-boot-kafka",
|
||||
"spring-boot-kotlin-serialization",
|
||||
"spring-boot-ldap",
|
||||
"spring-boot-liquibase",
|
||||
"spring-boot-loader",
|
||||
|
@ -2117,6 +2118,7 @@ bom {
|
|||
"spring-boot-starter-json",
|
||||
"spring-boot-starter-jsonb",
|
||||
"spring-boot-starter-kafka",
|
||||
"spring-boot-starter-kotlin-serialization",
|
||||
"spring-boot-starter-ldap",
|
||||
"spring-boot-starter-liquibase",
|
||||
"spring-boot-starter-log4j2",
|
||||
|
|
|
@ -29,6 +29,9 @@ pluginManagement {
|
|||
if (requested.id.id == "org.jetbrains.kotlin.plugin.spring") {
|
||||
useVersion "${kotlinVersion}"
|
||||
}
|
||||
if (requested.id.id == "org.jetbrains.kotlin.plugin.serialization") {
|
||||
useVersion "${kotlinVersion}"
|
||||
}
|
||||
}
|
||||
}
|
||||
includeBuild("gradle/plugins")
|
||||
|
@ -123,6 +126,7 @@ include "module:spring-boot-jooq"
|
|||
include "module:spring-boot-jpa"
|
||||
include "module:spring-boot-jsonb"
|
||||
include "module:spring-boot-kafka"
|
||||
include "module:spring-boot-kotlin-serialization"
|
||||
include "module:spring-boot-ldap"
|
||||
include "module:spring-boot-liquibase"
|
||||
include "module:spring-boot-mail"
|
||||
|
@ -213,6 +217,7 @@ include "starter:spring-boot-starter-jooq"
|
|||
include "starter:spring-boot-starter-json"
|
||||
include "starter:spring-boot-starter-jsonb"
|
||||
include "starter:spring-boot-starter-kafka"
|
||||
include "starter:spring-boot-starter-kotlin-serialization"
|
||||
include "starter:spring-boot-starter-ldap"
|
||||
include "starter:spring-boot-starter-liquibase"
|
||||
include "starter:spring-boot-starter-log4j2"
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id "org.springframework.boot.starter"
|
||||
}
|
||||
|
||||
description = "Starter for Kotlin Serialization"
|
||||
|
||||
dependencies {
|
||||
api(project(":starter:spring-boot-starter"))
|
||||
|
||||
api(project(":module:spring-boot-kotlin-serialization"))
|
||||
}
|
Loading…
Reference in New Issue