23 changed files with 1018 additions and 8 deletions
@ -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 @@ |
|||||||
|
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 } |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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