Merge remote-tracking branch 'origin/master' into dev
diff --git a/buildSrc/src/main/kotlin/Java9Modularity.kt b/buildSrc/src/main/kotlin/Java9Modularity.kt
index e8c41fc..2743b00 100644
--- a/buildSrc/src/main/kotlin/Java9Modularity.kt
+++ b/buildSrc/src/main/kotlin/Java9Modularity.kt
@@ -18,9 +18,13 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.*
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.*
import org.jetbrains.kotlin.gradle.targets.jvm.*
+import org.jetbrains.kotlin.gradle.tasks.*
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
+import org.jetbrains.kotlin.tooling.core.*
import java.io.*
+import kotlin.reflect.*
+import kotlin.reflect.full.*
object Java9Modularity {
@@ -108,12 +112,14 @@
compileTask: KotlinCompile,
sourceFile: File
): TaskProvider<out KotlinJvmCompile> {
- apply<KotlinBaseApiPlugin>()
+ apply<KotlinApiPlugin>()
val verifyModuleTaskName = "verify${compileTask.name.removePrefix("compile").capitalize()}Module"
// work-around for https://youtrack.jetbrains.com/issue/KT-60542
- val verifyModuleTask = plugins
- .findPlugin(KotlinBaseApiPlugin::class)!!
- .registerKotlinJvmCompileTask(verifyModuleTaskName)
+ val kotlinApiPlugin = plugins.getPlugin(KotlinApiPlugin::class)
+ val verifyModuleTask = kotlinApiPlugin.registerKotlinJvmCompileTask(
+ verifyModuleTaskName,
+ compileTask.compilerOptions.moduleName.get()
+ )
verifyModuleTask {
group = VERIFICATION_GROUP
description = "Verify Kotlin sources for JPMS problems"
@@ -126,13 +132,14 @@
source(sourceFile)
destinationDirectory.set(temporaryDir)
multiPlatformEnabled.set(compileTask.multiPlatformEnabled)
- kotlinOptions {
- moduleName = compileTask.kotlinOptions.moduleName
- jvmTarget = "9"
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_9)
// To support LV override when set in aggregate builds
- languageVersion = compileTask.kotlinOptions.languageVersion
- freeCompilerArgs += listOf("-Xjdk-release=9", "-Xsuppress-version-warnings", "-Xexpect-actual-classes")
- options.optIn.addAll(compileTask.kotlinOptions.options.optIn)
+ languageVersion.set(compileTask.compilerOptions.languageVersion)
+ freeCompilerArgs.addAll(
+ listOf("-Xjdk-release=9", "-Xsuppress-version-warnings", "-Xexpect-actual-classes")
+ )
+ optIn.addAll(compileTask.kotlinOptions.options.optIn)
}
// work-around for https://youtrack.jetbrains.com/issue/KT-60583
inputs.files(
@@ -145,14 +152,24 @@
}
).withPropertyName("moduleInfosOfLibraries")
this as KotlinCompile
- // part of work-around for https://youtrack.jetbrains.com/issue/KT-60541
- @Suppress("DEPRECATION")
- ownModuleName.set(compileTask.kotlinOptions.moduleName)
- // part of work-around for https://youtrack.jetbrains.com/issue/KT-60541
- @Suppress("INVISIBLE_MEMBER")
- commonSourceSet.from(compileTask.commonSourceSet)
+ val kotlinPluginVersion = KotlinToolingVersion(kotlinApiPlugin.pluginVersion)
+ if (kotlinPluginVersion <= KotlinToolingVersion("1.9.255")) {
+ // part of work-around for https://youtrack.jetbrains.com/issue/KT-60541
+ @Suppress("UNCHECKED_CAST")
+ val ownModuleNameProp = (this::class.superclasses.first() as KClass<AbstractKotlinCompile<*>>)
+ .declaredMemberProperties
+ .find { it.name == "ownModuleName" }
+ ?.get(this) as? Property<String>
+ ownModuleNameProp?.set(compileTask.kotlinOptions.moduleName)
+ }
+
+ val taskKotlinLanguageVersion = compilerOptions.languageVersion.orElse(KotlinVersion.DEFAULT)
@OptIn(InternalKotlinGradlePluginApi::class)
- apply {
+ if (taskKotlinLanguageVersion.get() < KotlinVersion.KOTLIN_2_0) {
+ // part of work-around for https://youtrack.jetbrains.com/issue/KT-60541
+ @Suppress("INVISIBLE_MEMBER")
+ commonSourceSet.from(compileTask.commonSourceSet)
+ } else {
multiplatformStructure.refinesEdges.set(compileTask.multiplatformStructure.refinesEdges)
multiplatformStructure.fragments.set(compileTask.multiplatformStructure.fragments)
}
diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api
index 4b46dcc..720e584 100644
--- a/core/api/kotlinx-serialization-core.api
+++ b/core/api/kotlinx-serialization-core.api
@@ -44,6 +44,9 @@
public abstract fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
}
+public abstract interface annotation class kotlinx/serialization/KeepGeneratedSerializer : java/lang/annotation/Annotation {
+}
+
public abstract interface annotation class kotlinx/serialization/MetaSerializable : java/lang/annotation/Annotation {
}
diff --git a/core/commonMain/src/kotlinx/serialization/Annotations.kt b/core/commonMain/src/kotlinx/serialization/Annotations.kt
index 67104dc..081ee82 100644
--- a/core/commonMain/src/kotlinx/serialization/Annotations.kt
+++ b/core/commonMain/src/kotlinx/serialization/Annotations.kt
@@ -325,6 +325,23 @@
public annotation class Polymorphic
/**
+ * Instructs the serialization plugin to keep automatically generated implementation of [KSerializer]
+ * for the current class if a custom serializer is specified at the same time `@Serializable(with=SomeSerializer::class)`.
+ *
+ * Automatically generated serializer is available via `generatedSerializer()` function in companion object of serializable class.
+ *
+ * Generated serializers allow to use custom serializers on classes from which other serializable classes are inherited.
+ *
+ * Used only with the [Serializable] annotation.
+ *
+ * A compiler version `2.0.0` and higher is required.
+ */
+@InternalSerializationApi
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+public annotation class KeepGeneratedSerializer
+
+/**
* Marks declarations that are still **experimental** in kotlinx.serialization, which means that the design of the
* corresponding declarations has open issues which may (or may not) lead to their changes in the future.
* Roughly speaking, there is a chance that those declarations will be deprecated in the near future or
diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt
index 1892f6a..2489be2 100644
--- a/core/commonMain/src/kotlinx/serialization/Serializers.kt
+++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt
@@ -187,8 +187,7 @@
): KSerializer<Any?>? {
val rootClass = type.kclass()
val isNullable = type.isMarkedNullable
- val typeArguments = type.arguments
- .map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } }
+ val typeArguments = type.arguments.map(KTypeProjection::typeOrThrow)
val cachedSerializer = if (typeArguments.isEmpty()) {
findCachedSerializer(rootClass, isNullable)
diff --git a/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt b/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt
index c16945c..ef313cc 100644
--- a/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt
+++ b/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt
@@ -102,16 +102,22 @@
internal fun KType.kclass() = when (val t = classifier) {
is KClass<*> -> t
is KTypeParameter -> {
- error(
+ // If you are going to change this error message, please also actualize the message in the compiler intrinsics here:
+ // Kotlin/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/SerializationJvmIrIntrinsicSupport.kt#argumentTypeOrGenerateException
+ throw IllegalArgumentException(
"Captured type parameter $t from generic non-reified function. " +
- "Such functionality cannot be supported as $t is erased, either specify serializer explicitly or make " +
- "calling function inline with reified $t"
+ "Such functionality cannot be supported because $t is erased, either specify serializer explicitly or make " +
+ "calling function inline with reified $t."
)
}
- else -> error("Only KClass supported as classifier, got $t")
+ else -> throw IllegalArgumentException("Only KClass supported as classifier, got $t")
} as KClass<Any>
+// If you are going to change this error message, please also actualize the message in the compiler intrinsics here:
+// Kotlin/plugins/kotlinx-serialization/kotlinx-serialization.backend/src/org/jetbrains/kotlinx/serialization/compiler/backend/ir/SerializationJvmIrIntrinsicSupport.kt#argumentTypeOrGenerateException
+internal fun KTypeProjection.typeOrThrow(): KType = requireNotNull(type) { "Star projections in type arguments are not allowed, but had $type" }
+
/**
* Constructs KSerializer<D<T0, T1, ...>> by given KSerializer<T0>, KSerializer<T1>, ...
* via reflection (on JVM) or compiler+plugin intrinsic `SerializerFactory` (on Native)
diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt
index f01f952..8a9126d 100644
--- a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt
+++ b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt
@@ -8,7 +8,6 @@
import kotlinx.serialization.internal.*
import kotlin.js.*
import kotlin.jvm.*
-import kotlin.native.concurrent.*
import kotlin.reflect.*
/**
diff --git a/docs/basic-serialization.md b/docs/basic-serialization.md
index 3853376..96e7098 100644
--- a/docs/basic-serialization.md
+++ b/docs/basic-serialization.md
@@ -534,7 +534,7 @@
```text
Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 52: Expected string literal but 'null' literal was found at path: $.language
-Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls to default values.
+Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value.
```
<!--- TEST LINES_START -->
diff --git a/docs/json.md b/docs/json.md
index d764ce5..c637eb1 100644
--- a/docs/json.md
+++ b/docs/json.md
@@ -20,6 +20,7 @@
* [Allowing structured map keys](#allowing-structured-map-keys)
* [Allowing special floating-point values](#allowing-special-floating-point-values)
* [Class discriminator for polymorphism](#class-discriminator-for-polymorphism)
+ * [Class discriminator output mode](#class-discriminator-output-mode)
* [Decoding enums in a case-insensitive manner](#decoding-enums-in-a-case-insensitive-manner)
* [Global naming strategy](#global-naming-strategy)
* [Json elements](#json-elements)
@@ -94,7 +95,7 @@
### Lenient parsing
By default, [Json] parser enforces various JSON restrictions to be as specification-compliant as possible
-(see [RFC-4627]). Particularly, keys must be quoted, while literals must be unquoted. Those restrictions can be relaxed with
+(see [RFC-4627]). Particularly, keys and string literals must be quoted. Those restrictions can be relaxed with
the [isLenient][JsonBuilder.isLenient] property. With `isLenient = true`, you can parse quite freely-formatted data:
```kotlin
@@ -470,6 +471,45 @@
<!--- TEST -->
+### Class discriminator output mode
+
+Class discriminator provides information for serializing and deserializing [polymorphic class hierarchies](polymorphism.md#sealed-classes).
+As shown above, it is only added for polymorphic classes by default.
+In case you want to encode more or less information for various third party APIs about types in the output, it is possible to control
+addition of the class discriminator with the [JsonBuilder.classDiscriminatorMode] property.
+
+For example, [ClassDiscriminatorMode.NONE] does not add class discriminator at all, in case the receiving party is not interested in Kotlin types:
+
+```kotlin
+val format = Json { classDiscriminatorMode = ClassDiscriminatorMode.NONE }
+
+@Serializable
+sealed class Project {
+ abstract val name: String
+}
+
+@Serializable
+class OwnedProject(override val name: String, val owner: String) : Project()
+
+fun main() {
+ val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
+ println(format.encodeToString(data))
+}
+```
+
+> You can get the full code [here](../guide/example/example-json-12.kt).
+
+Note that it would be impossible to deserialize this output back with kotlinx.serialization.
+
+```text
+{"name":"kotlinx.coroutines","owner":"kotlin"}
+```
+
+Two other available values are [ClassDiscriminatorMode.POLYMORPHIC] (default behavior) and [ClassDiscriminatorMode.ALL_JSON_OBJECTS] (adds discriminator whenever possible).
+Consult their documentation for details.
+
+<!--- TEST -->
+
### Decoding enums in a case-insensitive manner
[Kotlin's naming policy recommends](https://kotlinlang.org/docs/coding-conventions.html#property-names) naming enum values
@@ -491,7 +531,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-12.kt).
+> You can get the full code [here](../guide/example/example-json-13.kt).
It affects serial names as well as alternative names specified with [JsonNames] annotation, so both values are successfully decoded:
@@ -523,7 +563,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-13.kt).
+> You can get the full code [here](../guide/example/example-json-14.kt).
As you can see, both serialization and deserialization work as if all serial names are transformed from camel case to snake case:
@@ -575,7 +615,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-14.kt).
+> You can get the full code [here](../guide/example/example-json-15.kt).
A `JsonElement` prints itself as a valid JSON:
@@ -618,7 +658,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-15.kt).
+> You can get the full code [here](../guide/example/example-json-16.kt).
The above example sums `votes` in all objects in the `forks` array, ignoring the objects that have no `votes`:
@@ -658,7 +698,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-16.kt).
+> You can get the full code [here](../guide/example/example-json-17.kt).
As a result, you get a proper JSON string:
@@ -687,7 +727,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-17.kt).
+> You can get the full code [here](../guide/example/example-json-18.kt).
The result is exactly what you would expect:
@@ -733,7 +773,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-18.kt).
+> You can get the full code [here](../guide/example/example-json-19.kt).
Even though `pi` was defined as a number with 30 decimal places, the resulting JSON does not reflect this.
The [Double] value is truncated to 15 decimal places, and the String is wrapped in quotes - which is not a JSON number.
@@ -773,7 +813,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-19.kt).
+> You can get the full code [here](../guide/example/example-json-20.kt).
`pi_literal` now accurately matches the value defined.
@@ -813,7 +853,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-20.kt).
+> You can get the full code [here](../guide/example/example-json-21.kt).
The exact value of `pi` is decoded, with all 30 decimal places of precision that were in the source JSON.
@@ -835,7 +875,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-21.kt).
+> You can get the full code [here](../guide/example/example-json-22.kt).
```text
Exception in thread "main" kotlinx.serialization.json.internal.JsonEncodingException: Creating a literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive
@@ -911,7 +951,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-22.kt).
+> You can get the full code [here](../guide/example/example-json-23.kt).
The output shows that both cases are correctly deserialized into a Kotlin [List].
@@ -963,7 +1003,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-23.kt).
+> You can get the full code [here](../guide/example/example-json-24.kt).
You end up with a single JSON object, not an array with one element:
@@ -1008,7 +1048,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-24.kt).
+> You can get the full code [here](../guide/example/example-json-25.kt).
See the effect of the custom serializer:
@@ -1081,7 +1121,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-25.kt).
+> You can get the full code [here](../guide/example/example-json-26.kt).
No class discriminator is added in the JSON output:
@@ -1177,7 +1217,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-26.kt).
+> You can get the full code [here](../guide/example/example-json-27.kt).
This gives you fine-grained control on the representation of the `Response` class in the JSON output:
@@ -1242,7 +1282,7 @@
}
```
-> You can get the full code [here](../guide/example/example-json-27.kt).
+> You can get the full code [here](../guide/example/example-json-28.kt).
```text
UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})
@@ -1296,6 +1336,10 @@
[JsonBuilder.allowSpecialFloatingPointValues]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/allow-special-floating-point-values.html
[JsonBuilder.classDiscriminator]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/class-discriminator.html
[JsonClassDiscriminator]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-class-discriminator/index.html
+[JsonBuilder.classDiscriminatorMode]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/class-discriminator-mode.html
+[ClassDiscriminatorMode.NONE]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-class-discriminator-mode/-n-o-n-e/index.html
+[ClassDiscriminatorMode.POLYMORPHIC]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-class-discriminator-mode/-p-o-l-y-m-o-r-p-h-i-c/index.html
+[ClassDiscriminatorMode.ALL_JSON_OBJECTS]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-class-discriminator-mode/-a-l-l_-j-s-o-n_-o-b-j-e-c-t-s/index.html
[JsonBuilder.decodeEnumsCaseInsensitive]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/decode-enums-case-insensitive.html
[JsonBuilder.namingStrategy]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/naming-strategy.html
[JsonNamingStrategy]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-naming-strategy/index.html
diff --git a/docs/polymorphism.md b/docs/polymorphism.md
index b7ea31f..2661e04 100644
--- a/docs/polymorphism.md
+++ b/docs/polymorphism.md
@@ -542,7 +542,7 @@
> You can get the full code [here](../guide/example/example-poly-13.kt).
-However, the `Any` is a class and it is not serializable:
+However, `Any` is a class and it is not serializable:
```text
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found.
diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md
index 68ede14..50cb130 100644
--- a/docs/serialization-guide.md
+++ b/docs/serialization-guide.md
@@ -120,6 +120,7 @@
* <a name='allowing-structured-map-keys'></a>[Allowing structured map keys](json.md#allowing-structured-map-keys)
* <a name='allowing-special-floating-point-values'></a>[Allowing special floating-point values](json.md#allowing-special-floating-point-values)
* <a name='class-discriminator-for-polymorphism'></a>[Class discriminator for polymorphism](json.md#class-discriminator-for-polymorphism)
+ * <a name='class-discriminator-output-mode'></a>[Class discriminator output mode](json.md#class-discriminator-output-mode)
* <a name='decoding-enums-in-a-case-insensitive-manner'></a>[Decoding enums in a case-insensitive manner](json.md#decoding-enums-in-a-case-insensitive-manner)
* <a name='global-naming-strategy'></a>[Global naming strategy](json.md#global-naming-strategy)
* <a name='json-elements'></a>[Json elements](json.md#json-elements)
diff --git a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt
index f2f2779..163e562 100644
--- a/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt
+++ b/formats/hocon/src/main/kotlin/kotlinx/serialization/hocon/Hocon.kt
@@ -109,11 +109,13 @@
private fun getTaggedNumber(tag: T) = validateAndCast<Number>(tag)
+ @Suppress("UNCHECKED_CAST")
+ protected fun <E> decodeDuration(tag: T): E =
+ getValueFromTaggedConfig(tag, ::decodeDurationImpl) as E
+
@SuppressAnimalSniffer
- protected fun <E> decodeDuration(tag: T): E {
- @Suppress("UNCHECKED_CAST")
- return getValueFromTaggedConfig(tag) { conf, path -> conf.decodeJavaDuration(path).toKotlinDuration() } as E
- }
+ private fun decodeDurationImpl(conf: Config, path: String): Duration =
+ conf.decodeJavaDuration(path).toKotlinDuration()
override fun decodeTaggedString(tag: T) = validateAndCast<String>(tag)
@@ -145,7 +147,7 @@
}
- private inner class ConfigReader(val conf: Config) : ConfigConverter<String>() {
+ private inner class ConfigReader(val conf: Config, private val isPolymorphic: Boolean = false) : ConfigConverter<String>() {
private var ind = -1
override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
@@ -161,8 +163,10 @@
private fun composeName(parentName: String, childName: String) =
if (parentName.isEmpty()) childName else "$parentName.$childName"
- override fun SerialDescriptor.getTag(index: Int): String =
- composeName(currentTagOrNull.orEmpty(), getConventionElementName(index, useConfigNamingConvention))
+ override fun SerialDescriptor.getTag(index: Int): String {
+ val conventionName = getConventionElementName(index, useConfigNamingConvention)
+ return if (!isPolymorphic) composeName(currentTagOrNull.orEmpty(), conventionName) else conventionName
+ }
override fun decodeNotNullMark(): Boolean {
// Tag might be null for top-level deserialization
@@ -206,6 +210,27 @@
}
}
+ private inner class PolymorphConfigReader(private val conf: Config) : ConfigConverter<String>() {
+ private var ind = -1
+
+ override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
+ when {
+ descriptor.kind.objLike -> ConfigReader(conf, isPolymorphic = true)
+ else -> this
+ }
+
+ override fun SerialDescriptor.getTag(index: Int): String = getElementName(index)
+
+ override fun decodeElementIndex(descriptor: SerialDescriptor): Int {
+ ind++
+ return if (ind >= descriptor.elementsCount) DECODE_DONE else ind
+ }
+
+ override fun <E> getValueFromTaggedConfig(tag: String, valueResolver: (Config, String) -> E): E {
+ return valueResolver(conf, tag)
+ }
+ }
+
private inner class ListConfigReader(private val list: ConfigList) : ConfigConverter<Int>() {
private var ind = -1
@@ -216,6 +241,7 @@
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
when {
+ descriptor.kind is PolymorphicKind -> PolymorphConfigReader((list[currentTag] as ConfigObject).toConfig())
descriptor.kind.listLike -> ListConfigReader(list[currentTag] as ConfigList)
descriptor.kind.objLike -> ConfigReader((list[currentTag] as ConfigObject).toConfig())
descriptor.kind == StructureKind.MAP -> MapConfigReader(list[currentTag] as ConfigObject)
@@ -256,6 +282,7 @@
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
when {
+ descriptor.kind is PolymorphicKind -> PolymorphConfigReader((values[currentTag / 2] as ConfigObject).toConfig())
descriptor.kind.listLike -> ListConfigReader(values[currentTag / 2] as ConfigList)
descriptor.kind.objLike -> ConfigReader((values[currentTag / 2] as ConfigObject).toConfig())
descriptor.kind == StructureKind.MAP -> MapConfigReader(values[currentTag / 2] as ConfigObject)
diff --git a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt
index db038e7..1dbc1f9 100644
--- a/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt
+++ b/formats/hocon/src/test/kotlin/kotlinx/serialization/hocon/HoconPolymorphismTest.kt
@@ -24,6 +24,12 @@
}
@Serializable
+ data class SealedCollectionContainer(val sealed: Collection<Sealed>)
+
+ @Serializable
+ data class SealedMapContainer(val sealed: Map<String, Sealed>)
+
+ @Serializable
data class CompositeClass(var sealed: Sealed)
@@ -102,4 +108,46 @@
serializer = Sealed.serializer(),
)
}
+
+ @Test
+ fun testCollectionContainer() {
+ objectHocon.assertStringFormAndRestored(
+ expected = """
+ sealed = [
+ { type = annotated_type_child, my_type = override, intField = 3 }
+ { type = object }
+ { type = data_class, name = testDataClass, intField = 1 }
+ ]
+ """.trimIndent(),
+ original = SealedCollectionContainer(
+ listOf(
+ Sealed.AnnotatedTypeChild(type = "override"),
+ Sealed.ObjectChild,
+ Sealed.DataClassChild(name = "testDataClass"),
+ )
+ ),
+ serializer = SealedCollectionContainer.serializer(),
+ )
+ }
+
+ @Test
+ fun testMapContainer() {
+ objectHocon.assertStringFormAndRestored(
+ expected = """
+ sealed = {
+ "annotated_type_child" = { type = annotated_type_child, my_type = override, intField = 3 }
+ "object" = { type = object }
+ "data_class" = { type = data_class, name = testDataClass, intField = 1 }
+ }
+ """.trimIndent(),
+ original = SealedMapContainer(
+ mapOf(
+ "annotated_type_child" to Sealed.AnnotatedTypeChild(type = "override"),
+ "object" to Sealed.ObjectChild,
+ "data_class" to Sealed.DataClassChild(name = "testDataClass"),
+ )
+ ),
+ serializer = SealedMapContainer.serializer(),
+ )
+ }
}
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt
index 28a4d12..9e2cf1d 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt
@@ -103,6 +103,52 @@
}
}
+ @Test
+ fun testKebabCaseStrategy() {
+ fun apply(name: String) =
+ JsonNamingStrategy.KebabCase.serialNameForJson(String.serializer().descriptor, 0, name)
+
+ val cases = mapOf<String, String>(
+ "" to "",
+ "_" to "_",
+ "-" to "-",
+ "___" to "___",
+ "---" to "---",
+ "a" to "a",
+ "A" to "a",
+ "-1" to "-1",
+ "-a" to "-a",
+ "-A" to "-a",
+ "property" to "property",
+ "twoWords" to "two-words",
+ "threeDistinctWords" to "three-distinct-words",
+ "ThreeDistinctWords" to "three-distinct-words",
+ "Oneword" to "oneword",
+ "camel-Case-WithDashes" to "camel-case-with-dashes",
+ "_many----dashes--" to "_many----dashes--",
+ "URLmapping" to "ur-lmapping",
+ "URLMapping" to "url-mapping",
+ "IOStream" to "io-stream",
+ "IOstream" to "i-ostream",
+ "myIo2Stream" to "my-io2-stream",
+ "myIO2Stream" to "my-io2-stream",
+ "myIO2stream" to "my-io2stream",
+ "myIO2streamMax" to "my-io2stream-max",
+ "InURLBetween" to "in-url-between",
+ "myHTTP2APIKey" to "my-http2-api-key",
+ "myHTTP2fastApiKey" to "my-http2fast-api-key",
+ "myHTTP23APIKey" to "my-http23-api-key",
+ "myHttp23ApiKey" to "my-http23-api-key",
+ "theWWW" to "the-www",
+ "theWWW-URL-xxx" to "the-www-url-xxx",
+ "hasDigit123AndPostfix" to "has-digit123-and-postfix"
+ )
+
+ cases.forEach { (input, expected) ->
+ assertEquals(expected, apply(input))
+ }
+ }
+
@Serializable
data class DontUseOriginal(val testCase: String)
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt
index ecb946c..3d7c332 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt
@@ -29,6 +29,16 @@
val enum: SampleEnum?
)
+ @Serializable
+ class Uncoercable(
+ val s: String
+ )
+
+ @Serializable
+ class UncoercableEnum(
+ val e: SampleEnum
+ )
+
val json = Json {
coerceInputValues = true
isLenient = true
@@ -112,4 +122,24 @@
decoded = decodeFromString<NullableEnumHolder>("""{"enum": OptionA}""")
assertEquals(SampleEnum.OptionA, decoded.enum)
}
+
+ @Test
+ fun propertiesWithoutDefaultValuesDoNotChangeErrorMsg() {
+ val json2 = Json(json) { coerceInputValues = false }
+ parametrizedTest { mode ->
+ val e1 = assertFailsWith<SerializationException>() { json.decodeFromString<Uncoercable>("""{"s":null}""", mode) }
+ val e2 = assertFailsWith<SerializationException>() { json2.decodeFromString<Uncoercable>("""{"s":null}""", mode) }
+ assertEquals(e2.message, e1.message)
+ }
+ }
+
+ @Test
+ fun propertiesWithoutDefaultValuesDoNotChangeErrorMsgEnum() {
+ val json2 = Json(json) { coerceInputValues = false }
+ parametrizedTest { mode ->
+ val e1 = assertFailsWith<SerializationException> { json.decodeFromString<UncoercableEnum>("""{"e":"UNEXPECTED"}""", mode) }
+ val e2 = assertFailsWith<SerializationException> { json2.decodeFromString<UncoercableEnum>("""{"e":"UNEXPECTED"}""", mode) }
+ assertEquals(e2.message, e1.message)
+ }
+ }
}
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonPrettyPrintTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonPrettyPrintTest.kt
new file mode 100644
index 0000000..8283e25
--- /dev/null
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonPrettyPrintTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.json
+
+import kotlinx.serialization.*
+import kotlin.test.*
+
+class JsonPrettyPrintTest : JsonTestBase() {
+ val fmt = Json(default) { prettyPrint = true; encodeDefaults = true }
+
+ @Serializable
+ class Empty
+
+ @Serializable
+ class A(val empty: Empty = Empty())
+
+ @Serializable
+ class B(val prefix: String = "a", val empty: Empty = Empty(), val postfix: String = "b")
+
+ @Serializable
+ class Recursive(val rec: Recursive?, val empty: Empty = Empty())
+
+ @Serializable
+ class WithListRec(val rec: WithListRec?, val l: List<Int> = listOf())
+
+ @Serializable
+ class WithDefaults(val x: String = "x", val y: Int = 0)
+
+ @Test
+ fun testTopLevel() = parametrizedTest { mode ->
+ assertEquals("{}", fmt.encodeToString(Empty(), mode))
+ }
+
+ @Test
+ fun testWithDefaults() = parametrizedTest { mode ->
+ val dropDefaults = Json(fmt) { encodeDefaults = false }
+ val s = "{\n \"boxed\": {}\n}"
+ assertEquals(s, dropDefaults.encodeToString(Box(WithDefaults()), mode))
+ }
+
+ @Test
+ fun testPlain() = parametrizedTest { mode ->
+ val s = """{
+ | "empty": {}
+ |}""".trimMargin()
+ assertEquals(s, fmt.encodeToString(A(), mode))
+ }
+
+ @Test
+ fun testInside() = parametrizedTest { mode ->
+ val s = """{
+ | "prefix": "a",
+ | "empty": {},
+ | "postfix": "b"
+ |}""".trimMargin()
+ assertEquals(s, fmt.encodeToString(B(), mode))
+ }
+
+ @Test
+ fun testRecursive() = parametrizedTest { mode ->
+ val obj = Recursive(Recursive(null))
+ val s = "{\n \"rec\": {\n \"rec\": null,\n \"empty\": {}\n },\n \"empty\": {}\n}"
+ assertEquals(s, fmt.encodeToString(obj, mode))
+ }
+
+ @Test
+ fun test() = parametrizedTest { mode ->
+ val obj = WithListRec(WithListRec(null), listOf(1, 2, 3))
+ val s =
+ "{\n \"rec\": {\n \"rec\": null,\n \"l\": []\n },\n \"l\": [\n 1,\n 2,\n 3\n ]\n}"
+ assertEquals(s, fmt.encodeToString(obj, mode))
+ }
+}
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeBaseTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeBaseTest.kt
new file mode 100644
index 0000000..8fcd549
--- /dev/null
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeBaseTest.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.json.polymorphic
+
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.*
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+import kotlinx.serialization.json.*
+import kotlinx.serialization.modules.*
+import kotlin.test.*
+
+abstract class JsonClassDiscriminatorModeBaseTest(
+ val discriminator: ClassDiscriminatorMode,
+ val deserializeBack: Boolean = true
+) : JsonTestBase() {
+
+ @Serializable
+ sealed class SealedBase
+
+ @Serializable
+ @SerialName("container")
+ data class SealedContainer(val i: Inner): SealedBase()
+
+ @Serializable
+ @SerialName("inner")
+ data class Inner(val x: String, val e: SampleEnum = SampleEnum.OptionB)
+
+ @Serializable
+ @SerialName("outer")
+ data class Outer(val inn: Inner, val lst: List<Inner>, val lss: List<String>)
+
+ data class ContextualType(val text: String)
+
+ object CtxSerializer : KSerializer<ContextualType> {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CtxSerializer") {
+ element("a", String.serializer().descriptor)
+ element("b", String.serializer().descriptor)
+ }
+
+ override fun serialize(encoder: Encoder, value: ContextualType) {
+ encoder.encodeStructure(descriptor) {
+ encodeStringElement(descriptor, 0, value.text.substringBefore("#"))
+ encodeStringElement(descriptor, 1, value.text.substringAfter("#"))
+ }
+ }
+
+ override fun deserialize(decoder: Decoder): ContextualType {
+ lateinit var a: String
+ lateinit var b: String
+ decoder.decodeStructure(descriptor) {
+ while (true) {
+ when (decodeElementIndex(descriptor)) {
+ 0 -> a = decodeStringElement(descriptor, 0)
+ 1 -> b = decodeStringElement(descriptor, 1)
+ else -> break
+ }
+ }
+ }
+ return ContextualType("$a#$b")
+ }
+ }
+
+ @Serializable
+ @SerialName("withContextual")
+ data class WithContextual(@Contextual val ctx: ContextualType, val i: Inner)
+
+ val ctxModule = serializersModuleOf(CtxSerializer)
+
+ val json = Json(default) {
+ ignoreUnknownKeys = true
+ serializersModule = polymorphicTestModule + ctxModule
+ encodeDefaults = true
+ classDiscriminatorMode = discriminator
+ }
+
+ @Serializable
+ @SerialName("mixed")
+ data class MixedPolyAndRegular(val sb: SealedBase, val sc: SealedContainer, val i: Inner)
+
+ private inline fun <reified T> doTest(expected: String, obj: T) {
+ parametrizedTest { mode ->
+ val serialized = json.encodeToString(serializer<T>(), obj, mode)
+ assertEquals(expected, serialized, "Failed with mode = $mode")
+ if (deserializeBack) {
+ val deserialized: T = json.decodeFromString(serializer(), serialized, mode)
+ assertEquals(obj, deserialized, "Failed with mode = $mode")
+ }
+ }
+ }
+
+ fun testMixed(expected: String) {
+ val i = Inner("in", SampleEnum.OptionC)
+ val o = MixedPolyAndRegular(SealedContainer(i), SealedContainer(i), i)
+ doTest(expected, o)
+ }
+
+ fun testIncludeNonPolymorphic(expected: String) {
+ val o = Outer(Inner("X"), listOf(Inner("a"), Inner("b")), listOf("foo"))
+ doTest(expected, o)
+ }
+
+ fun testIncludePolymorphic(expected: String) {
+ val o = OuterNullableBox(OuterNullableImpl(InnerImpl(42), null), InnerImpl2(239))
+ doTest(expected, o)
+ }
+
+ fun testIncludeSealed(expected: String) {
+ val b = Box<SealedBase>(SealedContainer(Inner("x", SampleEnum.OptionC)))
+ doTest(expected, b)
+ }
+
+ fun testContextual(expected: String) {
+ val c = WithContextual(ContextualType("c#d"), Inner("x"))
+ doTest(expected, c)
+ }
+
+ @Serializable
+ @JsonClassDiscriminator("message_type")
+ sealed class Base
+
+ @Serializable // Class discriminator is inherited from Base
+ sealed class ErrorClass : Base()
+
+ @Serializable
+ @SerialName("ErrorClassImpl")
+ data class ErrorClassImpl(val msg: String) : ErrorClass()
+
+ @Serializable
+ @SerialName("Cont")
+ data class Cont(val ec: ErrorClass, val eci: ErrorClassImpl)
+
+ fun testCustomDiscriminator(expected: String) {
+ val c = Cont(ErrorClassImpl("a"), ErrorClassImpl("b"))
+ doTest(expected, c)
+ }
+
+ fun testTopLevelPolyImpl(expectedOpen: String, expectedSealed: String) {
+ assertEquals(expectedOpen, json.encodeToString(InnerImpl(42)))
+ assertEquals(expectedSealed, json.encodeToString(SealedContainer(Inner("x"))))
+ }
+
+ @Serializable
+ @SerialName("NullableMixed")
+ data class NullableMixed(val sb: SealedBase?, val sc: SealedContainer?)
+
+ fun testNullable(expected: String) {
+ val nm = NullableMixed(null, null)
+ doTest(expected, nm)
+ }
+}
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeTest.kt
new file mode 100644
index 0000000..b2f4713
--- /dev/null
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.serialization.json.polymorphic
+
+import kotlinx.serialization.json.*
+import kotlin.test.*
+
+class ClassDiscriminatorModeAllObjectsTest :
+ JsonClassDiscriminatorModeBaseTest(ClassDiscriminatorMode.ALL_JSON_OBJECTS) {
+ @Test
+ fun testIncludeNonPolymorphic() = testIncludeNonPolymorphic("""{"type":"outer","inn":{"type":"inner","x":"X","e":"OptionB"},"lst":[{"type":"inner","x":"a","e":"OptionB"},{"type":"inner","x":"b","e":"OptionB"}],"lss":["foo"]}""")
+
+ @Test
+ fun testIncludePolymorphic() {
+ val s = """{"type":"kotlinx.serialization.json.polymorphic.OuterNullableBox","outerBase":{"type":"kotlinx.serialization.json.polymorphic.OuterNullableImpl","""+
+ """"base":{"type":"kotlinx.serialization.json.polymorphic.InnerImpl","field":42,"str":"default","nullable":null},"base2":null},"innerBase":{"type":"kotlinx.serialization.json.polymorphic.InnerImpl2","field":239}}"""
+ testIncludePolymorphic(s)
+ }
+
+ @Test
+ fun testIncludeSealed() {
+ testIncludeSealed("""{"type":"kotlinx.serialization.Box","boxed":{"type":"container","i":{"type":"inner","x":"x","e":"OptionC"}}}""")
+ }
+
+ @Test
+ fun testIncludeMixed() = testMixed("""{"type":"mixed","sb":{"type":"container","i":{"type":"inner","x":"in","e":"OptionC"}},"sc":{"type":"container","i":{"type":"inner","x":"in","e":"OptionC"}},"i":{"type":"inner","x":"in","e":"OptionC"}}""")
+
+ @Test
+ fun testIncludeCtx() =
+ testContextual("""{"type":"withContextual","ctx":{"type":"CtxSerializer","a":"c","b":"d"},"i":{"type":"inner","x":"x","e":"OptionB"}}""")
+
+ @Test
+ fun testIncludeCustomDiscriminator() =
+ testCustomDiscriminator("""{"type":"Cont","ec":{"message_type":"ErrorClassImpl","msg":"a"},"eci":{"message_type":"ErrorClassImpl","msg":"b"}}""")
+
+ @Test
+ fun testTopLevelPolyImpl() = testTopLevelPolyImpl(
+ """{"type":"kotlinx.serialization.json.polymorphic.InnerImpl","field":42,"str":"default","nullable":null}""",
+ """{"type":"container","i":{"type":"inner","x":"x","e":"OptionB"}}"""
+ )
+
+ @Test
+ fun testNullable() = testNullable("""{"type":"NullableMixed","sb":null,"sc":null}""")
+
+}
+
+class ClassDiscriminatorModeNoneTest :
+ JsonClassDiscriminatorModeBaseTest(ClassDiscriminatorMode.NONE, deserializeBack = false) {
+ @Test
+ fun testIncludeNonPolymorphic() = testIncludeNonPolymorphic("""{"inn":{"x":"X","e":"OptionB"},"lst":[{"x":"a","e":"OptionB"},{"x":"b","e":"OptionB"}],"lss":["foo"]}""")
+
+ @Test
+ fun testIncludePolymorphic() {
+ val s = """{"outerBase":{"base":{"field":42,"str":"default","nullable":null},"base2":null},"innerBase":{"field":239}}"""
+ testIncludePolymorphic(s)
+ }
+
+ @Test
+ fun testIncludeSealed() {
+ testIncludeSealed("""{"boxed":{"i":{"x":"x","e":"OptionC"}}}""")
+ }
+
+ @Test
+ fun testIncludeMixed() = testMixed("""{"sb":{"i":{"x":"in","e":"OptionC"}},"sc":{"i":{"x":"in","e":"OptionC"}},"i":{"x":"in","e":"OptionC"}}""")
+
+ @Test
+ fun testIncludeCtx() =
+ testContextual("""{"ctx":{"a":"c","b":"d"},"i":{"x":"x","e":"OptionB"}}""")
+
+ @Test
+ fun testIncludeCustomDiscriminator() = testCustomDiscriminator("""{"ec":{"msg":"a"},"eci":{"msg":"b"}}""")
+
+ @Test
+ fun testTopLevelPolyImpl() = testTopLevelPolyImpl(
+ """{"field":42,"str":"default","nullable":null}""",
+ """{"i":{"x":"x","e":"OptionB"}}"""
+ )
+
+ @Test
+ fun testNullable() = testNullable("""{"sb":null,"sc":null}""")
+}
+
diff --git a/formats/json/api/kotlinx-serialization-json.api b/formats/json/api/kotlinx-serialization-json.api
index 649cce0..6874d74 100644
--- a/formats/json/api/kotlinx-serialization-json.api
+++ b/formats/json/api/kotlinx-serialization-json.api
@@ -1,3 +1,12 @@
+public final class kotlinx/serialization/json/ClassDiscriminatorMode : java/lang/Enum {
+ public static final field ALL_JSON_OBJECTS Lkotlinx/serialization/json/ClassDiscriminatorMode;
+ public static final field NONE Lkotlinx/serialization/json/ClassDiscriminatorMode;
+ public static final field POLYMORPHIC Lkotlinx/serialization/json/ClassDiscriminatorMode;
+ public static fun getEntries ()Lkotlin/enums/EnumEntries;
+ public static fun valueOf (Ljava/lang/String;)Lkotlinx/serialization/json/ClassDiscriminatorMode;
+ public static fun values ()[Lkotlinx/serialization/json/ClassDiscriminatorMode;
+}
+
public final class kotlinx/serialization/json/DecodeSequenceMode : java/lang/Enum {
public static final field ARRAY_WRAPPED Lkotlinx/serialization/json/DecodeSequenceMode;
public static final field AUTO_DETECT Lkotlinx/serialization/json/DecodeSequenceMode;
@@ -89,6 +98,7 @@
public final fun getAllowStructuredMapKeys ()Z
public final fun getAllowTrailingComma ()Z
public final fun getClassDiscriminator ()Ljava/lang/String;
+ public final fun getClassDiscriminatorMode ()Lkotlinx/serialization/json/ClassDiscriminatorMode;
public final fun getCoerceInputValues ()Z
public final fun getDecodeEnumsCaseInsensitive ()Z
public final fun getEncodeDefaults ()Z
@@ -105,6 +115,7 @@
public final fun setAllowStructuredMapKeys (Z)V
public final fun setAllowTrailingComma (Z)V
public final fun setClassDiscriminator (Ljava/lang/String;)V
+ public final fun setClassDiscriminatorMode (Lkotlinx/serialization/json/ClassDiscriminatorMode;)V
public final fun setCoerceInputValues (Z)V
public final fun setDecodeEnumsCaseInsensitive (Z)V
public final fun setEncodeDefaults (Z)V
@@ -134,6 +145,7 @@
public final fun getAllowStructuredMapKeys ()Z
public final fun getAllowTrailingComma ()Z
public final fun getClassDiscriminator ()Ljava/lang/String;
+ public final fun getClassDiscriminatorMode ()Lkotlinx/serialization/json/ClassDiscriminatorMode;
public final fun getCoerceInputValues ()Z
public final fun getDecodeEnumsCaseInsensitive ()Z
public final fun getEncodeDefaults ()Z
@@ -145,6 +157,7 @@
public final fun getUseAlternativeNames ()Z
public final fun getUseArrayPolymorphism ()Z
public final fun isLenient ()Z
+ public final fun setClassDiscriminatorMode (Lkotlinx/serialization/json/ClassDiscriminatorMode;)V
public fun toString ()Ljava/lang/String;
}
@@ -266,6 +279,7 @@
}
public final class kotlinx/serialization/json/JsonNamingStrategy$Builtins {
+ public final fun getKebabCase ()Lkotlinx/serialization/json/JsonNamingStrategy;
public final fun getSnakeCase ()Lkotlinx/serialization/json/JsonNamingStrategy;
}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
index 26c376e..2a144fe 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
@@ -254,11 +254,10 @@
/**
* Removes JSON specification restriction (RFC-4627) and makes parser
- * more liberal to the malformed input. In lenient mode quoted boolean literals,
- * and unquoted string literals are allowed.
+ * more liberal to the malformed input. In lenient mode, unquoted JSON keys and string values are allowed.
*
* Its relaxations can be expanded in the future, so that lenient parser becomes even more
- * permissive to invalid value in the input, replacing them with defaults.
+ * permissive to invalid values in the input.
*
* `false` by default.
*/
@@ -287,7 +286,7 @@
public var prettyPrintIndent: String = json.configuration.prettyPrintIndent
/**
- * Enables coercing incorrect JSON values to the default property value in the following cases:
+ * Enables coercing incorrect JSON values to the default property value (if exists) in the following cases:
* 1. JSON value is `null` but the property type is non-nullable.
* 2. Property type is an enum type, but JSON value contains unknown enum member.
*
@@ -299,6 +298,8 @@
* Switches polymorphic serialization to the default array format.
* This is an option for legacy JSON format and should not be generally used.
* `false` by default.
+ *
+ * This option can only be used if [classDiscriminatorMode] in a default [ClassDiscriminatorMode.POLYMORPHIC] state.
*/
public var useArrayPolymorphism: Boolean = json.configuration.useArrayPolymorphism
@@ -308,6 +309,16 @@
*/
public var classDiscriminator: String = json.configuration.classDiscriminator
+
+ /**
+ * Defines which classes and objects should have class discriminator added to the output.
+ * [ClassDiscriminatorMode.POLYMORPHIC] by default.
+ *
+ * Other modes are generally intended to produce JSON for consumption by third-party libraries,
+ * therefore, this setting does not affect the deserialization process.
+ */
+ public var classDiscriminatorMode: ClassDiscriminatorMode = json.configuration.classDiscriminatorMode
+
/**
* Removes JSON specification restriction on
* special floating-point values such as `NaN` and `Infinity` and enables their serialization and deserialization.
@@ -385,8 +396,13 @@
@OptIn(ExperimentalSerializationApi::class)
internal fun build(): JsonConfiguration {
- if (useArrayPolymorphism) require(classDiscriminator == defaultDiscriminator) {
- "Class discriminator should not be specified when array polymorphism is specified"
+ if (useArrayPolymorphism) {
+ require(classDiscriminator == defaultDiscriminator) {
+ "Class discriminator should not be specified when array polymorphism is specified"
+ }
+ require(classDiscriminatorMode == ClassDiscriminatorMode.POLYMORPHIC) {
+ "useArrayPolymorphism option can only be used if classDiscriminatorMode in a default POLYMORPHIC state."
+ }
}
if (!prettyPrint) {
@@ -406,7 +422,7 @@
allowStructuredMapKeys, prettyPrint, explicitNulls, prettyPrintIndent,
coerceInputValues, useArrayPolymorphism,
classDiscriminator, allowSpecialFloatingPointValues, useAlternativeNames,
- namingStrategy, decodeEnumsCaseInsensitive, allowTrailingComma
+ namingStrategy, decodeEnumsCaseInsensitive, allowTrailingComma, classDiscriminatorMode
)
}
}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
index 053f4cd..1fa1644 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
@@ -1,6 +1,8 @@
package kotlinx.serialization.json
import kotlinx.serialization.*
+import kotlinx.serialization.modules.*
+import kotlinx.serialization.descriptors.*
/**
* Configuration of the current [Json] instance available through [Json.configuration]
@@ -35,6 +37,8 @@
public val decodeEnumsCaseInsensitive: Boolean = false,
@ExperimentalSerializationApi
public val allowTrailingComma: Boolean = false,
+ @ExperimentalSerializationApi
+ public var classDiscriminatorMode: ClassDiscriminatorMode = ClassDiscriminatorMode.POLYMORPHIC,
) {
/** @suppress Dokka **/
@@ -43,7 +47,88 @@
return "JsonConfiguration(encodeDefaults=$encodeDefaults, ignoreUnknownKeys=$ignoreUnknownKeys, isLenient=$isLenient, " +
"allowStructuredMapKeys=$allowStructuredMapKeys, prettyPrint=$prettyPrint, explicitNulls=$explicitNulls, " +
"prettyPrintIndent='$prettyPrintIndent', coerceInputValues=$coerceInputValues, useArrayPolymorphism=$useArrayPolymorphism, " +
- "classDiscriminator='$classDiscriminator', allowSpecialFloatingPointValues=$allowSpecialFloatingPointValues, useAlternativeNames=$useAlternativeNames, " +
- "namingStrategy=$namingStrategy, decodeEnumsCaseInsensitive=$decodeEnumsCaseInsensitive, allowTrailingComma=$allowTrailingComma)"
+ "classDiscriminator='$classDiscriminator', allowSpecialFloatingPointValues=$allowSpecialFloatingPointValues, " +
+ "useAlternativeNames=$useAlternativeNames, namingStrategy=$namingStrategy, decodeEnumsCaseInsensitive=$decodeEnumsCaseInsensitive, " +
+ "allowTrailingComma=$allowTrailingComma, classDiscriminatorMode=$classDiscriminatorMode)"
}
}
+
+/**
+ * Defines which classes and objects should have their serial name included in the json as so-called class discriminator.
+ *
+ * Class discriminator is a JSON field added by kotlinx.serialization that has [JsonBuilder.classDiscriminator] as a key (`type` by default),
+ * and class' serial name as a value (fully-qualified name by default, can be changed with [SerialName] annotation).
+ *
+ * Class discriminator is important for serializing and deserializing [polymorphic class hierarchies](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#sealed-classes).
+ * Default [ClassDiscriminatorMode.POLYMORPHIC] mode adds discriminator only to polymorphic classes.
+ * This behavior can be changed to match various JSON schemas.
+ *
+ * @see JsonBuilder.classDiscriminator
+ * @see JsonBuilder.classDiscriminatorMode
+ * @see Polymorphic
+ * @see PolymorphicSerializer
+ */
+public enum class ClassDiscriminatorMode {
+ /**
+ * Never include class discriminator in the output.
+ *
+ * This mode is generally intended to produce JSON for consumption by third-party libraries.
+ * kotlinx.serialization is unable to deserialize [polymorphic classes][POLYMORPHIC] without class discriminators,
+ * so it is impossible to deserialize JSON produced in this mode if a data model has polymorphic classes.
+ */
+ NONE,
+
+ /**
+ * Include class discriminators whenever possible.
+ *
+ * Given that class discriminator is added as a JSON field, adding class discriminator is possible
+ * when the resulting JSON is a json object — i.e., for Kotlin classes, `object`s, and interfaces.
+ * More specifically, discriminator is added to the output of serializers which descriptors
+ * have a [kind][SerialDescriptor.kind] of either [StructureKind.CLASS] or [StructureKind.OBJECT].
+ *
+ * This mode is generally intended to produce JSON for consumption by third-party libraries.
+ * Given that [JsonBuilder.classDiscriminatorMode] does not affect deserialization, kotlinx.serialization
+ * does not expect every object to have discriminator, which may trigger deserialization errors.
+ * If you experience such problems, refrain from using [ALL_JSON_OBJECTS] or use [JsonBuilder.ignoreUnknownKeys].
+ *
+ * In the example:
+ * ```
+ * @Serializable class Plain(val p: String)
+ * @Serializable sealed class Base
+ * @Serializable object Impl: Base()
+ *
+ * @Serializable class All(val p: Plain, val b: Base, val i: Impl)
+ * ```
+ * setting [JsonBuilder.classDiscriminatorMode] to [ClassDiscriminatorMode.ALL_JSON_OBJECTS] adds
+ * class discriminator to `All.p`, `All.b`, `All.i`, and to `All` object itself.
+ */
+ ALL_JSON_OBJECTS,
+
+ /**
+ * Include class discriminators for polymorphic classes.
+ *
+ * Sealed classes, abstract classes, and interfaces are polymorphic classes by definition.
+ * Open classes can be polymorphic if they are serializable with [PolymorphicSerializer]
+ * and properly registered in the [SerializersModule].
+ * See [kotlinx.serialization polymorphism guide](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#sealed-classes) for details.
+ *
+ * Note that implementations of polymorphic classes (e.g., sealed class inheritors) are not polymorphic classes from kotlinx.serialization standpoint.
+ * This means that this mode adds class discriminators only if a statically known type of the property is a base class or interface.
+ *
+ * In the example:
+ * ```
+ * @Serializable class Plain(val p: String)
+ * @Serializable sealed class Base
+ * @Serializable object Impl: Base()
+ *
+ * @Serializable class All(val p: Plain, val b: Base, val i: Impl)
+ * ```
+ * setting [JsonBuilder.classDiscriminatorMode] to [ClassDiscriminatorMode.POLYMORPHIC] adds
+ * class discriminator to `All.b`, but leaves `All.p` and `All.i` intact.
+ *
+ * @see SerializersModule
+ * @see SerializersModuleBuilder
+ * @see PolymorphicModuleBuilder
+ */
+ POLYMORPHIC,
+}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt
index 64b4e0b..b737fd6 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt
@@ -95,39 +95,83 @@
*/
@ExperimentalSerializationApi
public val SnakeCase: JsonNamingStrategy = object : JsonNamingStrategy {
- override fun serialNameForJson(descriptor: SerialDescriptor, elementIndex: Int, serialName: String): String =
- buildString(serialName.length * 2) {
- var bufferedChar: Char? = null
- var previousUpperCharsCount = 0
-
- serialName.forEach { c ->
- if (c.isUpperCase()) {
- if (previousUpperCharsCount == 0 && isNotEmpty() && last() != '_')
- append('_')
-
- bufferedChar?.let(::append)
-
- previousUpperCharsCount++
- bufferedChar = c.lowercaseChar()
- } else {
- if (bufferedChar != null) {
- if (previousUpperCharsCount > 1 && c.isLetter()) {
- append('_')
- }
- append(bufferedChar)
- previousUpperCharsCount = 0
- bufferedChar = null
- }
- append(c)
- }
- }
-
- if(bufferedChar != null) {
- append(bufferedChar)
- }
- }
+ override fun serialNameForJson(
+ descriptor: SerialDescriptor,
+ elementIndex: Int,
+ serialName: String
+ ): String = convertCamelCase(serialName, '_')
override fun toString(): String = "kotlinx.serialization.json.JsonNamingStrategy.SnakeCase"
}
+
+ /**
+ * A strategy that transforms serial names from camel case to kebab case — lowercase characters with words separated by dashes.
+ * The descriptor parameter is not used.
+ *
+ * **Transformation rules**
+ *
+ * Words' bounds are defined by uppercase characters. If there is a single uppercase char, it is transformed into lowercase one with a dash in front:
+ * `twoWords` -> `two-words`. No dash is added if it was a beginning of the name: `MyProperty` -> `my-property`. Also, no dash is added if it was already there:
+ * `camel-Case-WithDashes` -> `camel-case-with-dashes`.
+ *
+ * **Acronyms**
+ *
+ * Since acronym rules are quite complex, it is recommended to lowercase all acronyms in source code.
+ * If there is an uppercase acronym — a sequence of uppercase chars — they are considered as a whole word from the start to second-to-last character of the sequence:
+ * `URLMapping` -> `url-mapping`, `myHTTPAuth` -> `my-http-auth`. Non-letter characters allow the word to continue:
+ * `myHTTP2APIKey` -> `my-http2-api-key`, `myHTTP2fastApiKey` -> `my-http2fast-api-key`.
+ *
+ * **Note on cases**
+ *
+ * Whether a character is in upper case is determined by the result of [Char.isUpperCase] function.
+ * Lowercase transformation is performed by [Char.lowercaseChar], not by [Char.lowercase],
+ * and therefore does not support one-to-many and many-to-one character mappings.
+ * See the documentation of these functions for details.
+ */
+ @ExperimentalSerializationApi
+ public val KebabCase: JsonNamingStrategy = object : JsonNamingStrategy {
+ override fun serialNameForJson(
+ descriptor: SerialDescriptor,
+ elementIndex: Int,
+ serialName: String
+ ): String = convertCamelCase(serialName, '-')
+
+ override fun toString(): String = "kotlinx.serialization.json.JsonNamingStrategy.KebabCase"
+ }
+
+ private fun convertCamelCase(
+ serialName: String,
+ delimiter: Char
+ ) = buildString(serialName.length * 2) {
+ var bufferedChar: Char? = null
+ var previousUpperCharsCount = 0
+
+ serialName.forEach { c ->
+ if (c.isUpperCase()) {
+ if (previousUpperCharsCount == 0 && isNotEmpty() && last() != delimiter)
+ append(delimiter)
+
+ bufferedChar?.let(::append)
+
+ previousUpperCharsCount++
+ bufferedChar = c.lowercaseChar()
+ } else {
+ if (bufferedChar != null) {
+ if (previousUpperCharsCount > 1 && c.isLetter()) {
+ append(delimiter)
+ }
+ append(bufferedChar)
+ previousUpperCharsCount = 0
+ bufferedChar = null
+ }
+ append(c)
+ }
+ }
+
+ if (bufferedChar != null) {
+ append(bufferedChar)
+ }
+ }
+
}
}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt
index c1ed8cc..abdd1c4 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt
@@ -27,6 +27,10 @@
writingFirst = false
}
+ open fun nextItemIfNotFirst() {
+ writingFirst = false
+ }
+
open fun space() = Unit
fun print(v: Char) = writer.writeChar(v)
@@ -88,6 +92,11 @@
repeat(level) { print(json.configuration.prettyPrintIndent) }
}
+ override fun nextItemIfNotFirst() {
+ if (writingFirst) writingFirst = false
+ else nextItem()
+ }
+
override fun space() {
print(' ')
}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt
deleted file mode 100644
index e69de29..0000000
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt
+++ /dev/null
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt
index 8acd8fc..9128f3a 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt
@@ -110,11 +110,14 @@
@OptIn(ExperimentalSerializationApi::class)
internal inline fun Json.tryCoerceValue(
- elementDescriptor: SerialDescriptor,
+ descriptor: SerialDescriptor,
+ index: Int,
peekNull: (consume: Boolean) -> Boolean,
peekString: () -> String?,
onEnumCoercing: () -> Unit = {}
): Boolean {
+ if (!descriptor.isElementOptional(index)) return false
+ val elementDescriptor = descriptor.getElementDescriptor(index)
if (!elementDescriptor.isNullable && peekNull(true)) return true
if (elementDescriptor.kind == SerialKind.ENUM) {
if (elementDescriptor.isNullable && peekNull(false)) {
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt
index bd658fc..636f340 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt
@@ -9,6 +9,7 @@
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.internal.*
import kotlinx.serialization.json.*
+import kotlinx.serialization.modules.*
import kotlin.jvm.*
@Suppress("UNCHECKED_CAST")
@@ -17,22 +18,37 @@
value: T,
ifPolymorphic: (String) -> Unit
) {
- if (serializer !is AbstractPolymorphicSerializer<*> || json.configuration.useArrayPolymorphism) {
+ if (json.configuration.useArrayPolymorphism) {
serializer.serialize(this, value)
return
}
- val casted = serializer as AbstractPolymorphicSerializer<Any>
- val baseClassDiscriminator = serializer.descriptor.classDiscriminator(json)
- val actualSerializer = casted.findPolymorphicSerializer(this, value as Any)
- validateIfSealed(casted, actualSerializer, baseClassDiscriminator)
- checkKind(actualSerializer.descriptor.kind)
- ifPolymorphic(baseClassDiscriminator)
+ val isPolymorphicSerializer = serializer is AbstractPolymorphicSerializer<*>
+ val needDiscriminator =
+ if (isPolymorphicSerializer) {
+ json.configuration.classDiscriminatorMode != ClassDiscriminatorMode.NONE
+ } else {
+ when (json.configuration.classDiscriminatorMode) {
+ ClassDiscriminatorMode.NONE, ClassDiscriminatorMode.POLYMORPHIC /* already handled in isPolymorphicSerializer */ -> false
+ ClassDiscriminatorMode.ALL_JSON_OBJECTS -> serializer.descriptor.kind.let { it == StructureKind.CLASS || it == StructureKind.OBJECT }
+ }
+ }
+ val baseClassDiscriminator = if (needDiscriminator) serializer.descriptor.classDiscriminator(json) else null
+ val actualSerializer: SerializationStrategy<T> = if (isPolymorphicSerializer) {
+ val casted = serializer as AbstractPolymorphicSerializer<Any>
+ requireNotNull(value) { "Value for serializer ${serializer.descriptor} should always be non-null. Please report issue to the kotlinx.serialization tracker." }
+ val actual = casted.findPolymorphicSerializer(this, value)
+ if (baseClassDiscriminator != null) validateIfSealed(serializer, actual, baseClassDiscriminator)
+ checkKind(actual.descriptor.kind)
+ actual as SerializationStrategy<T>
+ } else serializer
+
+ if (baseClassDiscriminator != null) ifPolymorphic(baseClassDiscriminator)
actualSerializer.serialize(this, value)
}
private fun validateIfSealed(
serializer: SerializationStrategy<*>,
- actualSerializer: SerializationStrategy<Any>,
+ actualSerializer: SerializationStrategy<*>,
classDiscriminator: String
) {
if (serializer !is SealedClassSerializer<*>) return
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
index 0018fce..caa1f4a 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
@@ -213,7 +213,7 @@
* Checks whether JSON has `null` value for non-null property or unknown enum value for enum property
*/
private fun coerceInputValue(descriptor: SerialDescriptor, index: Int): Boolean = json.tryCoerceValue(
- descriptor.getElementDescriptor(index),
+ descriptor, index,
{ lexer.tryConsumeNull(it) },
{ lexer.peekString(configuration.isLenient) },
{ lexer.consumeString() /* skip unknown enum string*/ }
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt
index 4f7b1ec..cf562de 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt
@@ -96,7 +96,7 @@
override fun endStructure(descriptor: SerialDescriptor) {
if (mode.end != INVALID) {
composer.unIndent()
- composer.nextItem()
+ composer.nextItemIfNotFirst()
composer.print(mode.end)
}
}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt
index aedfb95..690b35e 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonDecoder.kt
@@ -190,7 +190,7 @@
*/
private fun coerceInputValue(descriptor: SerialDescriptor, index: Int, tag: String): Boolean =
json.tryCoerceValue(
- descriptor.getElementDescriptor(index),
+ descriptor, index,
{ currentElement(tag) is JsonNull },
{ (currentElement(tag) as? JsonPrimitive)?.contentOrNull }
)
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt
index c83bdef..f90ee1a 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt
@@ -11,7 +11,7 @@
import kotlin.math.*
internal const val lenientHint = "Use 'isLenient = true' in 'Json {}' builder to accept non-compliant JSON."
-internal const val coerceInputValuesHint = "Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls to default values."
+internal const val coerceInputValuesHint = "Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value."
internal const val specialFlowingValuesHint =
"It is possible to deserialize them using 'JsonBuilder.allowSpecialFloatingPointValues = true'"
internal const val ignoreUnknownKeysHint = "Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys."
diff --git a/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt b/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt
index 86c7a85..1ff1e40 100644
--- a/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt
+++ b/formats/json/jsMain/src/kotlinx/serialization/json/internal/DynamicDecoders.kt
@@ -73,7 +73,7 @@
private fun coerceInputValue(descriptor: SerialDescriptor, index: Int, tag: String): Boolean =
json.tryCoerceValue(
- descriptor.getElementDescriptor(index),
+ descriptor, index,
{ getByTag(tag) == null },
{ getByTag(tag) as? String }
)
diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/schema/ProtoBufSchemaGenerator.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/schema/ProtoBufSchemaGenerator.kt
index b22df62..4f4ca9c 100644
--- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/schema/ProtoBufSchemaGenerator.kt
+++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/schema/ProtoBufSchemaGenerator.kt
@@ -216,29 +216,34 @@
val messageDescriptor = messageType.descriptor
val fieldDescriptor = messageDescriptor.getElementDescriptor(index)
+ var unwrappedFieldDescriptor = fieldDescriptor
+ while (unwrappedFieldDescriptor.isInline) {
+ unwrappedFieldDescriptor = unwrappedFieldDescriptor.getElementDescriptor(0)
+ }
+
val nestedTypes: List<TypeDefinition>
val typeName: String = when {
messageDescriptor.isSealedPolymorphic && index == 1 -> {
appendLine(" // decoded as message with one of these types:")
- nestedTypes = fieldDescriptor.elementDescriptors.map { TypeDefinition(it) }.toList()
+ nestedTypes = unwrappedFieldDescriptor.elementDescriptors.map { TypeDefinition(it) }.toList()
nestedTypes.forEachIndexed { _, childType ->
append(" // message ").append(childType.descriptor.messageOrEnumName).append(", serial name '")
.append(removeLineBreaks(childType.descriptor.serialName)).appendLine('\'')
}
- fieldDescriptor.scalarTypeName()
+ unwrappedFieldDescriptor.scalarTypeName()
}
- fieldDescriptor.isProtobufScalar -> {
+ unwrappedFieldDescriptor.isProtobufScalar -> {
nestedTypes = emptyList()
- fieldDescriptor.scalarTypeName(messageDescriptor.getElementAnnotations(index))
+ unwrappedFieldDescriptor.scalarTypeName(messageDescriptor.getElementAnnotations(index))
}
- fieldDescriptor.isOpenPolymorphic -> {
+ unwrappedFieldDescriptor.isOpenPolymorphic -> {
nestedTypes = listOf(SyntheticPolymorphicType)
SyntheticPolymorphicType.descriptor.serialName
}
else -> {
// enum or regular message
- nestedTypes = listOf(TypeDefinition(fieldDescriptor))
- fieldDescriptor.messageOrEnumName
+ nestedTypes = listOf(TypeDefinition(unwrappedFieldDescriptor))
+ unwrappedFieldDescriptor.messageOrEnumName
}
}
diff --git a/formats/protobuf/jvmTest/resources/OptionalClass.proto b/formats/protobuf/jvmTest/resources/OptionalClass.proto
index 41fdba7..68fda00 100644
--- a/formats/protobuf/jvmTest/resources/OptionalClass.proto
+++ b/formats/protobuf/jvmTest/resources/OptionalClass.proto
@@ -5,9 +5,21 @@
// serial name 'kotlinx.serialization.protobuf.schema.GenerationTest.OptionalClass'
message OptionalClass {
required int32 requiredInt = 1;
+ required int32 requiredUInt = 2;
+ required int32 requiredWrappedUInt = 3;
// WARNING: a default value decoded when value is missing
- optional int32 optionalInt = 2;
- optional int32 nullableInt = 3;
+ optional int32 optionalInt = 4;
// WARNING: a default value decoded when value is missing
- optional int32 nullableOptionalInt = 4;
+ optional int32 optionalUInt = 5;
+ // WARNING: a default value decoded when value is missing
+ optional int32 optionalWrappedUInt = 6;
+ optional int32 nullableInt = 7;
+ optional int32 nullableUInt = 8;
+ optional int32 nullableWrappedUInt = 9;
+ // WARNING: a default value decoded when value is missing
+ optional int32 nullableOptionalInt = 10;
+ // WARNING: a default value decoded when value is missing
+ optional int32 nullableOptionalUInt = 11;
+ // WARNING: a default value decoded when value is missing
+ optional int32 nullableOptionalWrappedUInt = 12;
}
diff --git a/formats/protobuf/jvmTest/resources/common/schema.proto b/formats/protobuf/jvmTest/resources/common/schema.proto
index 79e2d79..44b5a18 100644
--- a/formats/protobuf/jvmTest/resources/common/schema.proto
+++ b/formats/protobuf/jvmTest/resources/common/schema.proto
@@ -63,11 +63,23 @@
// serial name 'kotlinx.serialization.protobuf.schema.GenerationTest.OptionalClass'
message OptionalClass {
required int32 requiredInt = 1;
+ required int32 requiredUInt = 2;
+ required int32 requiredWrappedUInt = 3;
// WARNING: a default value decoded when value is missing
- optional int32 optionalInt = 2;
- optional int32 nullableInt = 3;
+ optional int32 optionalInt = 4;
// WARNING: a default value decoded when value is missing
- optional int32 nullableOptionalInt = 4;
+ optional int32 optionalUInt = 5;
+ // WARNING: a default value decoded when value is missing
+ optional int32 optionalWrappedUInt = 6;
+ optional int32 nullableInt = 7;
+ optional int32 nullableUInt = 8;
+ optional int32 nullableWrappedUInt = 9;
+ // WARNING: a default value decoded when value is missing
+ optional int32 nullableOptionalInt = 10;
+ // WARNING: a default value decoded when value is missing
+ optional int32 nullableOptionalUInt = 11;
+ // WARNING: a default value decoded when value is missing
+ optional int32 nullableOptionalWrappedUInt = 12;
}
// serial name 'kotlinx.serialization.protobuf.schema.GenerationTest.ContextualHolder'
diff --git a/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/schema/GenerationTest.kt b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/schema/GenerationTest.kt
index 61a2dce..f2a4423 100644
--- a/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/schema/GenerationTest.kt
+++ b/formats/protobuf/jvmTest/src/kotlinx/serialization/protobuf/schema/GenerationTest.kt
@@ -61,7 +61,7 @@
@ProtoNumber(5)
val b: Int,
@ProtoNumber(3)
- val c: Int
+ val c: UInt,
)
@Serializable
@@ -84,6 +84,10 @@
@Serializable
data class OptionsClass(val i: Int)
+ @JvmInline
+ @Serializable
+ value class WrappedUInt(val i : UInt)
+
@Serializable
class ListClass(
val intList: List<Int>,
@@ -113,9 +117,17 @@
@Serializable
data class OptionalClass(
val requiredInt: Int,
+ val requiredUInt: UInt,
+ val requiredWrappedUInt: WrappedUInt,
val optionalInt: Int = 5,
+ val optionalUInt: UInt = 5U,
+ val optionalWrappedUInt: WrappedUInt = WrappedUInt(5U),
val nullableInt: Int?,
- val nullableOptionalInt: Int? = 10
+ val nullableUInt: UInt?,
+ val nullableWrappedUInt: WrappedUInt?,
+ val nullableOptionalInt: Int? = 10,
+ val nullableOptionalUInt: UInt? = 10U,
+ val nullableOptionalWrappedUInt: WrappedUInt? = WrappedUInt(10U),
)
@Serializable
diff --git a/gradle/configure-source-sets.gradle b/gradle/configure-source-sets.gradle
index 740a475..f744b17 100644
--- a/gradle/configure-source-sets.gradle
+++ b/gradle/configure-source-sets.gradle
@@ -42,7 +42,6 @@
kotlinOptions {
sourceMap = true
moduleKind = "umd"
- metaInfo = true
}
}
}
diff --git a/gradle/native-targets.gradle b/gradle/native-targets.gradle
index 373aeba..8ef7f48 100644
--- a/gradle/native-targets.gradle
+++ b/gradle/native-targets.gradle
@@ -11,13 +11,14 @@
// According to https://kotlinlang.org/docs/native-target-support.html
// Tier 1
- linuxX64()
macosX64()
macosArm64()
iosSimulatorArm64()
iosX64()
// Tier 2
+ linuxX64()
+ linuxArm64()
watchosSimulatorArm64()
watchosX64()
watchosArm32()
@@ -26,7 +27,6 @@
tvosX64()
tvosArm64()
iosArm64()
- linuxArm64()
// Tier 3
mingwX64()
diff --git a/guide/example/example-json-12.kt b/guide/example/example-json-12.kt
index 1a37516..99a872b 100644
--- a/guide/example/example-json-12.kt
+++ b/guide/example/example-json-12.kt
@@ -4,13 +4,17 @@
import kotlinx.serialization.*
import kotlinx.serialization.json.*
-val format = Json { decodeEnumsCaseInsensitive = true }
-
-enum class Cases { VALUE_A, @JsonNames("Alternative") VALUE_B }
+val format = Json { classDiscriminatorMode = ClassDiscriminatorMode.NONE }
@Serializable
-data class CasesList(val cases: List<Cases>)
+sealed class Project {
+ abstract val name: String
+}
+
+@Serializable
+class OwnedProject(override val name: String, val owner: String) : Project()
fun main() {
- println(format.decodeFromString<CasesList>("""{"cases":["value_A", "alternative"]}"""))
+ val data: Project = OwnedProject("kotlinx.coroutines", "kotlin")
+ println(format.encodeToString(data))
}
diff --git a/guide/example/example-json-13.kt b/guide/example/example-json-13.kt
index cd7cf7f..e20afe2 100644
--- a/guide/example/example-json-13.kt
+++ b/guide/example/example-json-13.kt
@@ -4,12 +4,13 @@
import kotlinx.serialization.*
import kotlinx.serialization.json.*
-@Serializable
-data class Project(val projectName: String, val projectOwner: String)
+val format = Json { decodeEnumsCaseInsensitive = true }
-val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }
+enum class Cases { VALUE_A, @JsonNames("Alternative") VALUE_B }
+
+@Serializable
+data class CasesList(val cases: List<Cases>)
fun main() {
- val project = format.decodeFromString<Project>("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""")
- println(format.encodeToString(project.copy(projectName = "kotlinx.serialization")))
+ println(format.decodeFromString<CasesList>("""{"cases":["value_A", "alternative"]}"""))
}
diff --git a/guide/example/example-json-14.kt b/guide/example/example-json-14.kt
index 98464dc..50de55f 100644
--- a/guide/example/example-json-14.kt
+++ b/guide/example/example-json-14.kt
@@ -4,9 +4,12 @@
import kotlinx.serialization.*
import kotlinx.serialization.json.*
+@Serializable
+data class Project(val projectName: String, val projectOwner: String)
+
+val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }
+
fun main() {
- val element = Json.parseToJsonElement("""
- {"name":"kotlinx.serialization","language":"Kotlin"}
- """)
- println(element)
+ val project = format.decodeFromString<Project>("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""")
+ println(format.encodeToString(project.copy(projectName = "kotlinx.serialization")))
}
diff --git a/guide/example/example-json-15.kt b/guide/example/example-json-15.kt
index 72fd23e..384ae41 100644
--- a/guide/example/example-json-15.kt
+++ b/guide/example/example-json-15.kt
@@ -6,13 +6,7 @@
fun main() {
val element = Json.parseToJsonElement("""
- {
- "name": "kotlinx.serialization",
- "forks": [{"votes": 42}, {"votes": 9000}, {}]
- }
+ {"name":"kotlinx.serialization","language":"Kotlin"}
""")
- val sum = element
- .jsonObject["forks"]!!
- .jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 }
- println(sum)
+ println(element)
}
diff --git a/guide/example/example-json-16.kt b/guide/example/example-json-16.kt
index cff8ec7..fff287a 100644
--- a/guide/example/example-json-16.kt
+++ b/guide/example/example-json-16.kt
@@ -5,19 +5,14 @@
import kotlinx.serialization.json.*
fun main() {
- val element = buildJsonObject {
- put("name", "kotlinx.serialization")
- putJsonObject("owner") {
- put("name", "kotlin")
+ val element = Json.parseToJsonElement("""
+ {
+ "name": "kotlinx.serialization",
+ "forks": [{"votes": 42}, {"votes": 9000}, {}]
}
- putJsonArray("forks") {
- addJsonObject {
- put("votes", 42)
- }
- addJsonObject {
- put("votes", 9000)
- }
- }
- }
- println(element)
+ """)
+ val sum = element
+ .jsonObject["forks"]!!
+ .jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 }
+ println(sum)
}
diff --git a/guide/example/example-json-17.kt b/guide/example/example-json-17.kt
index 25be758..72a696a 100644
--- a/guide/example/example-json-17.kt
+++ b/guide/example/example-json-17.kt
@@ -4,14 +4,20 @@
import kotlinx.serialization.*
import kotlinx.serialization.json.*
-@Serializable
-data class Project(val name: String, val language: String)
-
fun main() {
val element = buildJsonObject {
put("name", "kotlinx.serialization")
- put("language", "Kotlin")
+ putJsonObject("owner") {
+ put("name", "kotlin")
+ }
+ putJsonArray("forks") {
+ addJsonObject {
+ put("votes", 42)
+ }
+ addJsonObject {
+ put("votes", 9000)
+ }
+ }
}
- val data = Json.decodeFromJsonElement<Project>(element)
- println(data)
+ println(element)
}
diff --git a/guide/example/example-json-18.kt b/guide/example/example-json-18.kt
index 2a1add4..1b655bf 100644
--- a/guide/example/example-json-18.kt
+++ b/guide/example/example-json-18.kt
@@ -4,20 +4,14 @@
import kotlinx.serialization.*
import kotlinx.serialization.json.*
-import java.math.BigDecimal
-
-val format = Json { prettyPrint = true }
+@Serializable
+data class Project(val name: String, val language: String)
fun main() {
- val pi = BigDecimal("3.141592653589793238462643383279")
-
- val piJsonDouble = JsonPrimitive(pi.toDouble())
- val piJsonString = JsonPrimitive(pi.toString())
-
- val piObject = buildJsonObject {
- put("pi_double", piJsonDouble)
- put("pi_string", piJsonString)
+ val element = buildJsonObject {
+ put("name", "kotlinx.serialization")
+ put("language", "Kotlin")
}
-
- println(format.encodeToString(piObject))
+ val data = Json.decodeFromJsonElement<Project>(element)
+ println(data)
}
diff --git a/guide/example/example-json-19.kt b/guide/example/example-json-19.kt
index d59bf26..b001c55 100644
--- a/guide/example/example-json-19.kt
+++ b/guide/example/example-json-19.kt
@@ -10,15 +10,11 @@
fun main() {
val pi = BigDecimal("3.141592653589793238462643383279")
-
- // use JsonUnquotedLiteral to encode raw JSON content
- val piJsonLiteral = JsonUnquotedLiteral(pi.toString())
-
+
val piJsonDouble = JsonPrimitive(pi.toDouble())
val piJsonString = JsonPrimitive(pi.toString())
val piObject = buildJsonObject {
- put("pi_literal", piJsonLiteral)
put("pi_double", piJsonDouble)
put("pi_string", piJsonString)
}
diff --git a/guide/example/example-json-20.kt b/guide/example/example-json-20.kt
index 2f481da..f522b3f 100644
--- a/guide/example/example-json-20.kt
+++ b/guide/example/example-json-20.kt
@@ -6,18 +6,22 @@
import java.math.BigDecimal
+val format = Json { prettyPrint = true }
+
fun main() {
- val piObjectJson = """
- {
- "pi_literal": 3.141592653589793238462643383279
- }
- """.trimIndent()
-
- val piObject: JsonObject = Json.decodeFromString(piObjectJson)
-
- val piJsonLiteral = piObject["pi_literal"]!!.jsonPrimitive.content
-
- val pi = BigDecimal(piJsonLiteral)
-
- println(pi)
+ val pi = BigDecimal("3.141592653589793238462643383279")
+
+ // use JsonUnquotedLiteral to encode raw JSON content
+ val piJsonLiteral = JsonUnquotedLiteral(pi.toString())
+
+ val piJsonDouble = JsonPrimitive(pi.toDouble())
+ val piJsonString = JsonPrimitive(pi.toString())
+
+ val piObject = buildJsonObject {
+ put("pi_literal", piJsonLiteral)
+ put("pi_double", piJsonDouble)
+ put("pi_string", piJsonString)
+ }
+
+ println(format.encodeToString(piObject))
}
diff --git a/guide/example/example-json-21.kt b/guide/example/example-json-21.kt
index 86a4b73..efd6071 100644
--- a/guide/example/example-json-21.kt
+++ b/guide/example/example-json-21.kt
@@ -4,7 +4,20 @@
import kotlinx.serialization.*
import kotlinx.serialization.json.*
+import java.math.BigDecimal
+
fun main() {
- // caution: creating null with JsonUnquotedLiteral will cause an exception!
- JsonUnquotedLiteral("null")
+ val piObjectJson = """
+ {
+ "pi_literal": 3.141592653589793238462643383279
+ }
+ """.trimIndent()
+
+ val piObject: JsonObject = Json.decodeFromString(piObjectJson)
+
+ val piJsonLiteral = piObject["pi_literal"]!!.jsonPrimitive.content
+
+ val pi = BigDecimal(piJsonLiteral)
+
+ println(pi)
}
diff --git a/guide/example/example-json-22.kt b/guide/example/example-json-22.kt
index 84bd0d8..e64ab06 100644
--- a/guide/example/example-json-22.kt
+++ b/guide/example/example-json-22.kt
@@ -4,29 +4,7 @@
import kotlinx.serialization.*
import kotlinx.serialization.json.*
-import kotlinx.serialization.builtins.*
-
-@Serializable
-data class Project(
- val name: String,
- @Serializable(with = UserListSerializer::class)
- val users: List<User>
-)
-
-@Serializable
-data class User(val name: String)
-
-object UserListSerializer : JsonTransformingSerializer<List<User>>(ListSerializer(User.serializer())) {
- // If response is not an array, then it is a single object that should be wrapped into the array
- override fun transformDeserialize(element: JsonElement): JsonElement =
- if (element !is JsonArray) JsonArray(listOf(element)) else element
-}
-
fun main() {
- println(Json.decodeFromString<Project>("""
- {"name":"kotlinx.serialization","users":{"name":"kotlin"}}
- """))
- println(Json.decodeFromString<Project>("""
- {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]}
- """))
+ // caution: creating null with JsonUnquotedLiteral will cause an exception!
+ JsonUnquotedLiteral("null")
}
diff --git a/guide/example/example-json-23.kt b/guide/example/example-json-23.kt
index bb23f52..ffa9f7d 100644
--- a/guide/example/example-json-23.kt
+++ b/guide/example/example-json-23.kt
@@ -17,14 +17,16 @@
data class User(val name: String)
object UserListSerializer : JsonTransformingSerializer<List<User>>(ListSerializer(User.serializer())) {
-
- override fun transformSerialize(element: JsonElement): JsonElement {
- require(element is JsonArray) // this serializer is used only with lists
- return element.singleOrNull() ?: element
- }
+ // If response is not an array, then it is a single object that should be wrapped into the array
+ override fun transformDeserialize(element: JsonElement): JsonElement =
+ if (element !is JsonArray) JsonArray(listOf(element)) else element
}
fun main() {
- val data = Project("kotlinx.serialization", listOf(User("kotlin")))
- println(Json.encodeToString(data))
+ println(Json.decodeFromString<Project>("""
+ {"name":"kotlinx.serialization","users":{"name":"kotlin"}}
+ """))
+ println(Json.decodeFromString<Project>("""
+ {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]}
+ """))
}
diff --git a/guide/example/example-json-24.kt b/guide/example/example-json-24.kt
index def90f2..010bd27 100644
--- a/guide/example/example-json-24.kt
+++ b/guide/example/example-json-24.kt
@@ -4,19 +4,27 @@
import kotlinx.serialization.*
import kotlinx.serialization.json.*
-@Serializable
-class Project(val name: String, val language: String)
+import kotlinx.serialization.builtins.*
-object ProjectSerializer : JsonTransformingSerializer<Project>(Project.serializer()) {
- override fun transformSerialize(element: JsonElement): JsonElement =
- // Filter out top-level key value pair with the key "language" and the value "Kotlin"
- JsonObject(element.jsonObject.filterNot {
- (k, v) -> k == "language" && v.jsonPrimitive.content == "Kotlin"
- })
+@Serializable
+data class Project(
+ val name: String,
+ @Serializable(with = UserListSerializer::class)
+ val users: List<User>
+)
+
+@Serializable
+data class User(val name: String)
+
+object UserListSerializer : JsonTransformingSerializer<List<User>>(ListSerializer(User.serializer())) {
+
+ override fun transformSerialize(element: JsonElement): JsonElement {
+ require(element is JsonArray) // this serializer is used only with lists
+ return element.singleOrNull() ?: element
+ }
}
fun main() {
- val data = Project("kotlinx.serialization", "Kotlin")
- println(Json.encodeToString(data)) // using plugin-generated serializer
- println(Json.encodeToString(ProjectSerializer, data)) // using custom serializer
+ val data = Project("kotlinx.serialization", listOf(User("kotlin")))
+ println(Json.encodeToString(data))
}
diff --git a/guide/example/example-json-25.kt b/guide/example/example-json-25.kt
index 6f6d67a..a7d19a7 100644
--- a/guide/example/example-json-25.kt
+++ b/guide/example/example-json-25.kt
@@ -4,33 +4,19 @@
import kotlinx.serialization.*
import kotlinx.serialization.json.*
-import kotlinx.serialization.builtins.*
-
@Serializable
-abstract class Project {
- abstract val name: String
-}
+class Project(val name: String, val language: String)
-@Serializable
-data class BasicProject(override val name: String): Project()
-
-
-@Serializable
-data class OwnedProject(override val name: String, val owner: String) : Project()
-
-object ProjectSerializer : JsonContentPolymorphicSerializer<Project>(Project::class) {
- override fun selectDeserializer(element: JsonElement) = when {
- "owner" in element.jsonObject -> OwnedProject.serializer()
- else -> BasicProject.serializer()
- }
+object ProjectSerializer : JsonTransformingSerializer<Project>(Project.serializer()) {
+ override fun transformSerialize(element: JsonElement): JsonElement =
+ // Filter out top-level key value pair with the key "language" and the value "Kotlin"
+ JsonObject(element.jsonObject.filterNot {
+ (k, v) -> k == "language" && v.jsonPrimitive.content == "Kotlin"
+ })
}
fun main() {
- val data = listOf(
- OwnedProject("kotlinx.serialization", "kotlin"),
- BasicProject("example")
- )
- val string = Json.encodeToString(ListSerializer(ProjectSerializer), data)
- println(string)
- println(Json.decodeFromString(ListSerializer(ProjectSerializer), string))
+ val data = Project("kotlinx.serialization", "Kotlin")
+ println(Json.encodeToString(data)) // using plugin-generated serializer
+ println(Json.encodeToString(ProjectSerializer, data)) // using custom serializer
}
diff --git a/guide/example/example-json-26.kt b/guide/example/example-json-26.kt
index c308b63..b1b9299 100644
--- a/guide/example/example-json-26.kt
+++ b/guide/example/example-json-26.kt
@@ -4,56 +4,33 @@
import kotlinx.serialization.*
import kotlinx.serialization.json.*
-import kotlinx.serialization.descriptors.*
-import kotlinx.serialization.encoding.*
+import kotlinx.serialization.builtins.*
-@Serializable(with = ResponseSerializer::class)
-sealed class Response<out T> {
- data class Ok<out T>(val data: T) : Response<T>()
- data class Error(val message: String) : Response<Nothing>()
-}
-
-class ResponseSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Response<T>> {
- override val descriptor: SerialDescriptor = buildSerialDescriptor("Response", PolymorphicKind.SEALED) {
- element("Ok", dataSerializer.descriptor)
- element("Error", buildClassSerialDescriptor("Error") {
- element<String>("message")
- })
- }
-
- override fun deserialize(decoder: Decoder): Response<T> {
- // Decoder -> JsonDecoder
- require(decoder is JsonDecoder) // this class can be decoded only by Json
- // JsonDecoder -> JsonElement
- val element = decoder.decodeJsonElement()
- // JsonElement -> value
- if (element is JsonObject && "error" in element)
- return Response.Error(element["error"]!!.jsonPrimitive.content)
- return Response.Ok(decoder.json.decodeFromJsonElement(dataSerializer, element))
- }
-
- override fun serialize(encoder: Encoder, value: Response<T>) {
- // Encoder -> JsonEncoder
- require(encoder is JsonEncoder) // This class can be encoded only by Json
- // value -> JsonElement
- val element = when (value) {
- is Response.Ok -> encoder.json.encodeToJsonElement(dataSerializer, value.data)
- is Response.Error -> buildJsonObject { put("error", value.message) }
- }
- // JsonElement -> JsonEncoder
- encoder.encodeJsonElement(element)
- }
+@Serializable
+abstract class Project {
+ abstract val name: String
}
@Serializable
-data class Project(val name: String)
+data class BasicProject(override val name: String): Project()
+
+
+@Serializable
+data class OwnedProject(override val name: String, val owner: String) : Project()
+
+object ProjectSerializer : JsonContentPolymorphicSerializer<Project>(Project::class) {
+ override fun selectDeserializer(element: JsonElement) = when {
+ "owner" in element.jsonObject -> OwnedProject.serializer()
+ else -> BasicProject.serializer()
+ }
+}
fun main() {
- val responses = listOf(
- Response.Ok(Project("kotlinx.serialization")),
- Response.Error("Not found")
+ val data = listOf(
+ OwnedProject("kotlinx.serialization", "kotlin"),
+ BasicProject("example")
)
- val string = Json.encodeToString(responses)
+ val string = Json.encodeToString(ListSerializer(ProjectSerializer), data)
println(string)
- println(Json.decodeFromString<List<Response<Project>>>(string))
+ println(Json.decodeFromString(ListSerializer(ProjectSerializer), string))
}
diff --git a/guide/example/example-json-27.kt b/guide/example/example-json-27.kt
index 219de6e..5905733 100644
--- a/guide/example/example-json-27.kt
+++ b/guide/example/example-json-27.kt
@@ -7,31 +7,53 @@
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
-data class UnknownProject(val name: String, val details: JsonObject)
+@Serializable(with = ResponseSerializer::class)
+sealed class Response<out T> {
+ data class Ok<out T>(val data: T) : Response<T>()
+ data class Error(val message: String) : Response<Nothing>()
+}
-object UnknownProjectSerializer : KSerializer<UnknownProject> {
- override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") {
- element<String>("name")
- element<JsonElement>("details")
+class ResponseSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Response<T>> {
+ override val descriptor: SerialDescriptor = buildSerialDescriptor("Response", PolymorphicKind.SEALED) {
+ element("Ok", dataSerializer.descriptor)
+ element("Error", buildClassSerialDescriptor("Error") {
+ element<String>("message")
+ })
}
- override fun deserialize(decoder: Decoder): UnknownProject {
- // Cast to JSON-specific interface
- val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON")
- // Read the whole content as JSON
- val json = jsonInput.decodeJsonElement().jsonObject
- // Extract and remove name property
- val name = json.getValue("name").jsonPrimitive.content
- val details = json.toMutableMap()
- details.remove("name")
- return UnknownProject(name, JsonObject(details))
+ override fun deserialize(decoder: Decoder): Response<T> {
+ // Decoder -> JsonDecoder
+ require(decoder is JsonDecoder) // this class can be decoded only by Json
+ // JsonDecoder -> JsonElement
+ val element = decoder.decodeJsonElement()
+ // JsonElement -> value
+ if (element is JsonObject && "error" in element)
+ return Response.Error(element["error"]!!.jsonPrimitive.content)
+ return Response.Ok(decoder.json.decodeFromJsonElement(dataSerializer, element))
}
- override fun serialize(encoder: Encoder, value: UnknownProject) {
- error("Serialization is not supported")
+ override fun serialize(encoder: Encoder, value: Response<T>) {
+ // Encoder -> JsonEncoder
+ require(encoder is JsonEncoder) // This class can be encoded only by Json
+ // value -> JsonElement
+ val element = when (value) {
+ is Response.Ok -> encoder.json.encodeToJsonElement(dataSerializer, value.data)
+ is Response.Error -> buildJsonObject { put("error", value.message) }
+ }
+ // JsonElement -> JsonEncoder
+ encoder.encodeJsonElement(element)
}
}
+@Serializable
+data class Project(val name: String)
+
fun main() {
- println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}"""))
+ val responses = listOf(
+ Response.Ok(Project("kotlinx.serialization")),
+ Response.Error("Not found")
+ )
+ val string = Json.encodeToString(responses)
+ println(string)
+ println(Json.decodeFromString<List<Response<Project>>>(string))
}
diff --git a/guide/example/example-json-28.kt b/guide/example/example-json-28.kt
new file mode 100644
index 0000000..a3fab61
--- /dev/null
+++ b/guide/example/example-json-28.kt
@@ -0,0 +1,37 @@
+// This file was automatically generated from json.md by Knit tool. Do not edit.
+package example.exampleJson28
+
+import kotlinx.serialization.*
+import kotlinx.serialization.json.*
+
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+
+data class UnknownProject(val name: String, val details: JsonObject)
+
+object UnknownProjectSerializer : KSerializer<UnknownProject> {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") {
+ element<String>("name")
+ element<JsonElement>("details")
+ }
+
+ override fun deserialize(decoder: Decoder): UnknownProject {
+ // Cast to JSON-specific interface
+ val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON")
+ // Read the whole content as JSON
+ val json = jsonInput.decodeJsonElement().jsonObject
+ // Extract and remove name property
+ val name = json.getValue("name").jsonPrimitive.content
+ val details = json.toMutableMap()
+ details.remove("name")
+ return UnknownProject(name, JsonObject(details))
+ }
+
+ override fun serialize(encoder: Encoder, value: UnknownProject) {
+ error("Serialization is not supported")
+ }
+}
+
+fun main() {
+ println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}"""))
+}
diff --git a/guide/test/BasicSerializationTest.kt b/guide/test/BasicSerializationTest.kt
index dc89feb..11f9e9f 100644
--- a/guide/test/BasicSerializationTest.kt
+++ b/guide/test/BasicSerializationTest.kt
@@ -110,7 +110,7 @@
fun testExampleClasses12() {
captureOutput("ExampleClasses12") { example.exampleClasses12.main() }.verifyOutputLinesStart(
"Exception in thread \"main\" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 52: Expected string literal but 'null' literal was found at path: $.language",
- "Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls to default values."
+ "Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value."
)
}
diff --git a/guide/test/JsonTest.kt b/guide/test/JsonTest.kt
index 115ef77..0c5ed85 100644
--- a/guide/test/JsonTest.kt
+++ b/guide/test/JsonTest.kt
@@ -90,52 +90,49 @@
@Test
fun testExampleJson12() {
captureOutput("ExampleJson12") { example.exampleJson12.main() }.verifyOutputLines(
- "CasesList(cases=[VALUE_A, VALUE_B])"
+ "{\"name\":\"kotlinx.coroutines\",\"owner\":\"kotlin\"}"
)
}
@Test
fun testExampleJson13() {
captureOutput("ExampleJson13") { example.exampleJson13.main() }.verifyOutputLines(
- "{\"project_name\":\"kotlinx.serialization\",\"project_owner\":\"Kotlin\"}"
+ "CasesList(cases=[VALUE_A, VALUE_B])"
)
}
@Test
fun testExampleJson14() {
captureOutput("ExampleJson14") { example.exampleJson14.main() }.verifyOutputLines(
- "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}"
+ "{\"project_name\":\"kotlinx.serialization\",\"project_owner\":\"Kotlin\"}"
)
}
@Test
fun testExampleJson15() {
captureOutput("ExampleJson15") { example.exampleJson15.main() }.verifyOutputLines(
- "9042"
+ "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}"
)
}
@Test
fun testExampleJson16() {
captureOutput("ExampleJson16") { example.exampleJson16.main() }.verifyOutputLines(
- "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}"
+ "9042"
)
}
@Test
fun testExampleJson17() {
captureOutput("ExampleJson17") { example.exampleJson17.main() }.verifyOutputLines(
- "Project(name=kotlinx.serialization, language=Kotlin)"
+ "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}"
)
}
@Test
fun testExampleJson18() {
captureOutput("ExampleJson18") { example.exampleJson18.main() }.verifyOutputLines(
- "{",
- " \"pi_double\": 3.141592653589793,",
- " \"pi_string\": \"3.141592653589793238462643383279\"",
- "}"
+ "Project(name=kotlinx.serialization, language=Kotlin)"
)
}
@@ -143,7 +140,6 @@
fun testExampleJson19() {
captureOutput("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLines(
"{",
- " \"pi_literal\": 3.141592653589793238462643383279,",
" \"pi_double\": 3.141592653589793,",
" \"pi_string\": \"3.141592653589793238462643383279\"",
"}"
@@ -153,59 +149,70 @@
@Test
fun testExampleJson20() {
captureOutput("ExampleJson20") { example.exampleJson20.main() }.verifyOutputLines(
- "3.141592653589793238462643383279"
+ "{",
+ " \"pi_literal\": 3.141592653589793238462643383279,",
+ " \"pi_double\": 3.141592653589793,",
+ " \"pi_string\": \"3.141592653589793238462643383279\"",
+ "}"
)
}
@Test
fun testExampleJson21() {
- captureOutput("ExampleJson21") { example.exampleJson21.main() }.verifyOutputLinesStart(
- "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonEncodingException: Creating a literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive"
+ captureOutput("ExampleJson21") { example.exampleJson21.main() }.verifyOutputLines(
+ "3.141592653589793238462643383279"
)
}
@Test
fun testExampleJson22() {
- captureOutput("ExampleJson22") { example.exampleJson22.main() }.verifyOutputLines(
- "Project(name=kotlinx.serialization, users=[User(name=kotlin)])",
- "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])"
+ captureOutput("ExampleJson22") { example.exampleJson22.main() }.verifyOutputLinesStart(
+ "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonEncodingException: Creating a literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive"
)
}
@Test
fun testExampleJson23() {
captureOutput("ExampleJson23") { example.exampleJson23.main() }.verifyOutputLines(
- "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}"
+ "Project(name=kotlinx.serialization, users=[User(name=kotlin)])",
+ "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])"
)
}
@Test
fun testExampleJson24() {
captureOutput("ExampleJson24") { example.exampleJson24.main() }.verifyOutputLines(
- "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}",
- "{\"name\":\"kotlinx.serialization\"}"
+ "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}"
)
}
@Test
fun testExampleJson25() {
captureOutput("ExampleJson25") { example.exampleJson25.main() }.verifyOutputLines(
- "[{\"name\":\"kotlinx.serialization\",\"owner\":\"kotlin\"},{\"name\":\"example\"}]",
- "[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]"
+ "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}",
+ "{\"name\":\"kotlinx.serialization\"}"
)
}
@Test
fun testExampleJson26() {
captureOutput("ExampleJson26") { example.exampleJson26.main() }.verifyOutputLines(
- "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]",
- "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]"
+ "[{\"name\":\"kotlinx.serialization\",\"owner\":\"kotlin\"},{\"name\":\"example\"}]",
+ "[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]"
)
}
@Test
fun testExampleJson27() {
captureOutput("ExampleJson27") { example.exampleJson27.main() }.verifyOutputLines(
+ "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]",
+ "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]"
+ )
+ }
+
+ @Test
+ fun testExampleJson28() {
+ captureOutput("ExampleJson28") { example.exampleJson28.main() }.verifyOutputLines(
"UnknownProject(name=example, details={\"type\":\"unknown\",\"maintainer\":\"Unknown\",\"license\":\"Apache 2.0\"})"
)
}
diff --git a/integration-test/build.gradle b/integration-test/build.gradle
index 00237a7..6c4e700 100644
--- a/integration-test/build.gradle
+++ b/integration-test/build.gradle
@@ -45,7 +45,6 @@
kotlinOptions {
sourceMap = true
moduleKind = "umd"
- metaInfo = true
}
}
}