summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLeonid Startsev <sandwwraith@users.noreply.github.com>2023-12-19 17:03:39 +0100
committerGitHub <noreply@github.com>2023-12-19 17:03:39 +0100
commitcd9f8b0094c5e88ae30fc7baba3478e83aef8ebd (patch)
tree4cdd708154fd6890c3be3cac83a6cbbd161d9001
parentad9ddd10e9fea9660fe7b024cd6e6399d4922db5 (diff)
downloadkotlinx.serialization-cd9f8b0094c5e88ae30fc7baba3478e83aef8ebd.tar.gz
Implement ClassDiscriminatorMode.ALL, .NONE, and .POLYMORPHIC (#2532)
Implement ClassDiscriminatorMode.ALL, .NONE, and .POLYMORPHIC As a part of the solution for #1247
-rw-r--r--core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt1
-rw-r--r--docs/json.md76
-rw-r--r--docs/serialization-guide.md1
-rw-r--r--formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeBaseTest.kt153
-rw-r--r--formats/json-tests/commonTest/src/kotlinx/serialization/json/polymorphic/JsonClassDiscriminatorModeTest.kt84
-rw-r--r--formats/json/api/kotlinx-serialization-json.api13
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/Json.kt23
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt89
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt0
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/Polymorphic.kt32
-rw-r--r--guide/example/example-json-12.kt12
-rw-r--r--guide/example/example-json-13.kt11
-rw-r--r--guide/example/example-json-14.kt11
-rw-r--r--guide/example/example-json-15.kt10
-rw-r--r--guide/example/example-json-16.kt23
-rw-r--r--guide/example/example-json-17.kt18
-rw-r--r--guide/example/example-json-18.kt20
-rw-r--r--guide/example/example-json-19.kt6
-rw-r--r--guide/example/example-json-20.kt30
-rw-r--r--guide/example/example-json-21.kt17
-rw-r--r--guide/example/example-json-22.kt26
-rw-r--r--guide/example/example-json-23.kt16
-rw-r--r--guide/example/example-json-24.kt28
-rw-r--r--guide/example/example-json-25.kt36
-rw-r--r--guide/example/example-json-26.kt59
-rw-r--r--guide/example/example-json-27.kt58
-rw-r--r--guide/example/example-json-28.kt37
-rw-r--r--guide/test/JsonTest.kt55
28 files changed, 692 insertions, 253 deletions
diff --git a/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt b/core/commonMain/src/kotlinx/serialization/modules/SerializersModule.kt
index f01f952a..8a9126d7 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.*
import kotlinx.serialization.internal.*
import kotlin.js.*
import kotlin.jvm.*
-import kotlin.native.concurrent.*
import kotlin.reflect.*
/**
diff --git a/docs/json.md b/docs/json.md
index d764ce5f..eaa2b797 100644
--- a/docs/json.md
+++ b/docs/json.md
@@ -20,6 +20,7 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json
* [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)
@@ -470,6 +471,45 @@ As you can see, discriminator from the `Base` class is used:
<!--- 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ fun main() {
}
```
-> 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 @@ The next chapter covers [Alternative and custom formats (experimental)](formats.
[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/serialization-guide.md b/docs/serialization-guide.md
index 68ede144..50cb1306 100644
--- a/docs/serialization-guide.md
+++ b/docs/serialization-guide.md
@@ -120,6 +120,7 @@ Once the project is set up, we can start serializing some classes.
* <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/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 00000000..8fcd5499
--- /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 00000000..b2f47137
--- /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 649cce0e..88dd29c2 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 class kotlinx/serialization/json/JsonBuilder {
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 class kotlinx/serialization/json/JsonBuilder {
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 class kotlinx/serialization/json/JsonConfiguration {
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 class kotlinx/serialization/json/JsonConfiguration {
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;
}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
index a510e8a3..e09b9edc 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
@@ -299,6 +299,8 @@ public class JsonBuilder internal constructor(json: Json) {
* 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 +310,16 @@ public class JsonBuilder internal constructor(json: Json) {
*/
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 +397,13 @@ public class JsonBuilder internal constructor(json: Json) {
@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 +423,7 @@ public class JsonBuilder internal constructor(json: Json) {
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 053f4cd6..1fa1644e 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 class JsonConfiguration @OptIn(ExperimentalSerializationApi::class) inter
public val decodeEnumsCaseInsensitive: Boolean = false,
@ExperimentalSerializationApi
public val allowTrailingComma: Boolean = false,
+ @ExperimentalSerializationApi
+ public var classDiscriminatorMode: ClassDiscriminatorMode = ClassDiscriminatorMode.POLYMORPHIC,
) {
/** @suppress Dokka **/
@@ -43,7 +47,88 @@ public class JsonConfiguration @OptIn(ExperimentalSerializationApi::class) inter
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/internal/JsonConfiguration.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt
deleted file mode 100644
index e69de29b..00000000
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonConfiguration.kt
+++ /dev/null
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 bd658fc1..636f340d 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.*
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 @@ internal inline fun <T> JsonEncoder.encodePolymorphically(
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/guide/example/example-json-12.kt b/guide/example/example-json-12.kt
index 1a37516b..99a872b7 100644
--- a/guide/example/example-json-12.kt
+++ b/guide/example/example-json-12.kt
@@ -4,13 +4,17 @@ package example.exampleJson12
import kotlinx.serialization.*
import kotlinx.serialization.json.*
-val format = Json { decodeEnumsCaseInsensitive = true }
+val format = Json { classDiscriminatorMode = ClassDiscriminatorMode.NONE }
-enum class Cases { VALUE_A, @JsonNames("Alternative") VALUE_B }
+@Serializable
+sealed class Project {
+ abstract val name: String
+}
@Serializable
-data class CasesList(val cases: List<Cases>)
+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 cd7cf7f2..e20afe28 100644
--- a/guide/example/example-json-13.kt
+++ b/guide/example/example-json-13.kt
@@ -4,12 +4,13 @@ package example.exampleJson13
import kotlinx.serialization.*
import kotlinx.serialization.json.*
-@Serializable
-data class Project(val projectName: String, val projectOwner: String)
+val format = Json { decodeEnumsCaseInsensitive = true }
+
+enum class Cases { VALUE_A, @JsonNames("Alternative") VALUE_B }
-val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }
+@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 98464dcd..50de55fd 100644
--- a/guide/example/example-json-14.kt
+++ b/guide/example/example-json-14.kt
@@ -4,9 +4,12 @@ package example.exampleJson14
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 72fd23eb..384ae416 100644
--- a/guide/example/example-json-15.kt
+++ b/guide/example/example-json-15.kt
@@ -6,13 +6,7 @@ import kotlinx.serialization.json.*
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 cff8ec7d..fff287ae 100644
--- a/guide/example/example-json-16.kt
+++ b/guide/example/example-json-16.kt
@@ -5,19 +5,14 @@ import kotlinx.serialization.*
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 25be7584..72a696a2 100644
--- a/guide/example/example-json-17.kt
+++ b/guide/example/example-json-17.kt
@@ -4,14 +4,20 @@ package example.exampleJson17
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 2a1add45..1b655bfe 100644
--- a/guide/example/example-json-18.kt
+++ b/guide/example/example-json-18.kt
@@ -4,20 +4,14 @@ package example.exampleJson18
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 d59bf26b..b001c55a 100644
--- a/guide/example/example-json-19.kt
+++ b/guide/example/example-json-19.kt
@@ -10,15 +10,11 @@ val format = Json { prettyPrint = true }
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 2f481daa..f522b3fa 100644
--- a/guide/example/example-json-20.kt
+++ b/guide/example/example-json-20.kt
@@ -6,18 +6,22 @@ import kotlinx.serialization.json.*
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 86a4b734..efd60710 100644
--- a/guide/example/example-json-21.kt
+++ b/guide/example/example-json-21.kt
@@ -4,7 +4,20 @@ package example.exampleJson21
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 84bd0d8a..e64ab06f 100644
--- a/guide/example/example-json-22.kt
+++ b/guide/example/example-json-22.kt
@@ -4,29 +4,7 @@ package example.exampleJson22
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 bb23f527..ffa9f7d7 100644
--- a/guide/example/example-json-23.kt
+++ b/guide/example/example-json-23.kt
@@ -17,14 +17,16 @@ data class Project(
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 def90f20..010bd27d 100644
--- a/guide/example/example-json-24.kt
+++ b/guide/example/example-json-24.kt
@@ -4,19 +4,27 @@ package example.exampleJson24
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
-class Project(val name: String, val language: String)
+data class User(val name: String)
+
+object UserListSerializer : JsonTransformingSerializer<List<User>>(ListSerializer(User.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"
- })
+ 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 6f6d67a0..a7d19a7f 100644
--- a/guide/example/example-json-25.kt
+++ b/guide/example/example-json-25.kt
@@ -4,33 +4,19 @@ package example.exampleJson25
import kotlinx.serialization.*
import kotlinx.serialization.json.*
-import kotlinx.serialization.builtins.*
-
-@Serializable
-abstract class Project {
- abstract val name: 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()
- }
+class Project(val name: String, val language: String)
+
+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 c308b634..b1b92999 100644
--- a/guide/example/example-json-26.kt
+++ b/guide/example/example-json-26.kt
@@ -4,56 +4,33 @@ package example.exampleJson26
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>()
+@Serializable
+abstract class Project {
+ abstract val name: String
}
-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")
- })
- }
+@Serializable
+data class BasicProject(override val name: String): Project()
- 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
+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()
}
}
-@Serializable
-data class Project(val name: String)
-
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 219de6ef..5905733a 100644
--- a/guide/example/example-json-27.kt
+++ b/guide/example/example-json-27.kt
@@ -7,31 +7,53 @@ import kotlinx.serialization.json.*
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 00000000..a3fab617
--- /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/JsonTest.kt b/guide/test/JsonTest.kt
index 115ef778..0c5ed85e 100644
--- a/guide/test/JsonTest.kt
+++ b/guide/test/JsonTest.kt
@@ -90,52 +90,49 @@ class JsonTest {
@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 @@ class JsonTest {
fun testExampleJson19() {
captureOutput("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLines(
"{",
- " \"pi_literal\": 3.141592653589793238462643383279,",
" \"pi_double\": 3.141592653589793,",
" \"pi_string\": \"3.141592653589793238462643383279\"",
"}"
@@ -153,59 +149,70 @@ class JsonTest {
@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\"})"
)
}