23 changed files with 1018 additions and 8 deletions
@ -0,0 +1,52 @@
@@ -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); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,48 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1 @@
|
||||
org.springframework.boot.kotlin.serialization.autoconfigure.KotlinSerializationAutoConfiguration |
||||
@ -0,0 +1,321 @@
@@ -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 } |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,27 @@
@@ -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