From fdbf1804d280979ef22529a640abf9e1d6848b18 Mon Sep 17 00:00:00 2001 From: Leonid Startsev Date: Thu, 2 May 2024 19:14:47 +0200 Subject: [PATCH] Stabilize explicitNulls feature: - Bring back its interaction with coerceInputValues flag - Enhance documentation and add more samples to it - Remove @ExperimentalSerializationApi Fixes #2636 Fixes #2586 --- docs/json.md | 181 +++++++++++------- docs/serialization-guide.md | 2 +- .../json/AbstractJsonImplicitNullsTest.kt | 7 +- .../json/JsonCoerceInputValuesTest.kt | 29 +++ .../src/kotlinx/serialization/json/Json.kt | 55 +++++- .../serialization/json/JsonConfiguration.kt | 1 - .../json/internal/JsonNamesMap.kt | 7 +- guide/example/example-json-05.kt | 14 +- guide/example/example-json-06.kt | 16 +- guide/example/example-json-07.kt | 18 +- guide/example/example-json-08.kt | 16 +- guide/example/example-json-09.kt | 13 +- guide/example/example-json-10.kt | 14 +- guide/example/example-json-11.kt | 24 +-- guide/example/example-json-12.kt | 23 ++- guide/example/example-json-13.kt | 12 +- guide/example/example-json-14.kt | 11 +- guide/example/example-json-15.kt | 50 +---- guide/example/example-json-16.kt | 53 ++++- guide/example/example-json-17.kt | 10 +- guide/example/example-json-18.kt | 23 +-- guide/example/example-json-19.kt | 18 +- guide/example/example-json-20.kt | 20 +- guide/example/example-json-21.kt | 6 +- guide/example/example-json-22.kt | 30 +-- guide/example/example-json-23.kt | 17 +- guide/example/example-json-24.kt | 26 +-- guide/example/example-json-25.kt | 16 +- guide/example/example-json-26.kt | 28 ++- guide/example/example-json-27.kt | 36 ++-- guide/example/example-json-28.kt | 59 ++---- guide/example/example-json-29.kt | 58 ++++-- guide/example/example-json-30.kt | 37 ++++ guide/test/JsonTest.kt | 77 ++++---- 34 files changed, 577 insertions(+), 430 deletions(-) create mode 100644 guide/example/example-json-30.kt diff --git a/docs/json.md b/docs/json.md index 7ce7a918bb..8d4fe0650a 100644 --- a/docs/json.md +++ b/docs/json.md @@ -14,9 +14,9 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json * [Lenient parsing](#lenient-parsing) * [Ignoring unknown keys](#ignoring-unknown-keys) * [Alternative Json names](#alternative-json-names) - * [Coercing input values](#coercing-input-values) * [Encoding defaults](#encoding-defaults) * [Explicit nulls](#explicit-nulls) + * [Coercing input values](#coercing-input-values) * [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) @@ -195,51 +195,6 @@ unless you want to do some fine-tuning. -### Coercing input values - -JSON formats that from third parties can evolve, sometimes changing the field types. -This can lead to exceptions during decoding when the actual values do not match the expected values. -The default [Json] implementation is strict with respect to input types as was demonstrated in -the [Type safety is enforced](basic-serialization.md#type-safety-is-enforced) section. You can relax this restriction -using the [coerceInputValues][JsonBuilder.coerceInputValues] property. - -This property only affects decoding. It treats a limited subset of invalid input values as if the -corresponding property was missing and uses the default value of the corresponding property instead. -The current list of supported invalid values is: - -* `null` inputs for non-nullable types -* unknown values for enums - -> This list may be expanded in the future, so that [Json] instance configured with this property becomes even more -> permissive to invalid value in the input, replacing them with defaults. - -See the example from the [Type safety is enforced](basic-serialization.md#type-safety-is-enforced) section: - -```kotlin -val format = Json { coerceInputValues = true } - -@Serializable -data class Project(val name: String, val language: String = "Kotlin") - -fun main() { - val data = format.decodeFromString(""" - {"name":"kotlinx.serialization","language":null} - """) - println(data) -} -``` - -> You can get the full code [here](../guide/example/example-json-05.kt). - -The invalid `null` value for the `language` property was coerced into the default value: - -```text -Project(name=kotlinx.serialization, language=Kotlin) -``` - - - - ### Encoding defaults Default values of properties are not encoded by default because they will be assigned to missing fields during decoding anyway. @@ -263,7 +218,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-06.kt). +> You can get the full code [here](../guide/example/example-json-05.kt). It produces the following output which encodes all the property values including the default ones: @@ -302,7 +257,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-07.kt). +> You can get the full code [here](../guide/example/example-json-06.kt). As you can see, `version`, `website` and `description` fields are not present in output JSON on the first line. After decoding, the missing nullable property `website` without a default values has received a `null` value, @@ -313,10 +268,94 @@ while nullable properties `version` and `description` are filled with their defa Project(name=kotlinx.serialization, language=Kotlin, version=1.2.2, website=null, description=null) ``` +> Pay attention to the fact that `version` was `null` before encoding and because `1.2.2` after decoding. +> Encoding/decoding of proeprties like it — nullable with non-null default — becomes asymmetrical if `explicitNulls` is set to `false`. + +It is possible to make decoder treat some invalid input data as the missing field to enhance the functionality of this flag. +See [coerceInputValues](#coercing-input-values) below for details. + `explicitNulls` is `true` by default as it is the default behavior across different versions of the library. +### Coercing input values + +JSON formats that from third parties can evolve, sometimes changing the field types. +This can lead to exceptions during decoding when the actual values do not match the expected values. +The default [Json] implementation is strict with respect to input types as was demonstrated in +the [Type safety is enforced](basic-serialization.md#type-safety-is-enforced) section. You can relax this restriction +using the [coerceInputValues][JsonBuilder.coerceInputValues] property. + +This property only affects decoding. It treats a limited subset of invalid input values as if the +corresponding property was missing. +The current list of supported invalid values is: + +* `null` inputs for non-nullable types +* unknown values for enums + +If value is missing, it is replaced either with a default property value if it exists, +or with a `null` if [explicitNulls](#explicit-nulls) flag is set to `false` and a property is nullable. + +> This list may be expanded in the future, so that [Json] instance configured with this property becomes even more +> permissive to invalid value in the input, replacing them with defaults or nulls. + +See the example from the [Type safety is enforced](basic-serialization.md#type-safety-is-enforced) section: + +```kotlin +val format = Json { coerceInputValues = true } + +@Serializable +data class Project(val name: String, val language: String = "Kotlin") + +fun main() { + val data = format.decodeFromString(""" + {"name":"kotlinx.serialization","language":null} + """) + println(data) +} +``` + +> You can get the full code [here](../guide/example/example-json-07.kt). + +The invalid `null` value for the `language` property was coerced into the default value: + +```text +Project(name=kotlinx.serialization, language=Kotlin) +``` + + + +Example of using this flag together with [explicitNulls](#explicit-nulls) to coerce invalid enum values: + +```kotlin +enum class Color { BLACK, WHITE } + +@Serializable +data class Brush(val foreground: Color = Color.BLACK, val background: Color?) + +val json = Json { + coerceInputValues = true + explicitNulls = false +} + +fun main() { + val brush = json.decodeFromString("""{"foreground":"pink", "background":"purple"}""") + println(brush) +} +``` + +> You can get the full code [here](../guide/example/example-json-08.kt). + +Despite that we do not have `Color.pink` and `Color.purple` colors, `decodeFromString` function returns successfully: + +```text +Brush(foreground=BLACK, background=null) +``` + +`foreground` property received its default value, and `background` property received `null` because of `explicitNulls = false` setting. + + + ### Allowing structured map keys JSON format does not natively support the concept of a map with structured keys. Keys in JSON objects @@ -341,7 +380,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-08.kt). +> You can get the full code [here](../guide/example/example-json-09.kt). The map with structured keys gets represented as JSON array with the following items: `[key1, value1, key2, value2,...]`. @@ -372,7 +411,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-09.kt). +> You can get the full code [here](../guide/example/example-json-10.kt). This example produces the following non-stardard JSON output, yet it is a widely used encoding for special values in JVM world: @@ -406,7 +445,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-10.kt). +> You can get the full code [here](../guide/example/example-json-11.kt). In combination with an explicitly specified [SerialName] of the class it provides full control over the resulting JSON object: @@ -462,7 +501,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-11.kt). +> You can get the full code [here](../guide/example/example-json-12.kt). As you can see, discriminator from the `Base` class is used: @@ -498,7 +537,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). Note that it would be impossible to deserialize this output back with kotlinx.serialization. @@ -532,7 +571,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). It affects serial names as well as alternative names specified with [JsonNames] annotation, so both values are successfully decoded: @@ -564,7 +603,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). As you can see, both serialization and deserialization work as if all serial names are transformed from camel case to snake case: @@ -662,7 +701,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) ```text {"base64Input":"Zm9vIHN0cmluZw=="} @@ -704,7 +743,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). A `JsonElement` prints itself as a valid JSON: @@ -747,7 +786,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 above example sums `votes` in all objects in the `forks` array, ignoring the objects that have no `votes`: @@ -787,7 +826,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). As a result, you get a proper JSON string: @@ -816,7 +855,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). The result is exactly what you would expect: @@ -862,7 +901,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). 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. @@ -902,7 +941,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). `pi_literal` now accurately matches the value defined. @@ -942,7 +981,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 exact value of `pi` is decoded, with all 30 decimal places of precision that were in the source JSON. @@ -964,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). ```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 @@ -1040,7 +1079,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). The output shows that both cases are correctly deserialized into a Kotlin [List]. @@ -1092,7 +1131,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). You end up with a single JSON object, not an array with one element: @@ -1137,7 +1176,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). See the effect of the custom serializer: @@ -1210,7 +1249,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). No class discriminator is added in the JSON output: @@ -1306,7 +1345,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-28.kt). +> You can get the full code [here](../guide/example/example-json-29.kt). This gives you fine-grained control on the representation of the `Response` class in the JSON output: @@ -1371,7 +1410,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-29.kt). +> You can get the full code [here](../guide/example/example-json-30.kt). ```text UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"}) @@ -1418,9 +1457,9 @@ The next chapter covers [Alternative and custom formats (experimental)](formats. [JsonBuilder.ignoreUnknownKeys]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/ignore-unknown-keys.html [JsonNames]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-names/index.html [JsonBuilder.useAlternativeNames]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/use-alternative-names.html -[JsonBuilder.coerceInputValues]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/coerce-input-values.html [JsonBuilder.encodeDefaults]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/encode-defaults.html [JsonBuilder.explicitNulls]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/explicit-nulls.html +[JsonBuilder.coerceInputValues]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/coerce-input-values.html [JsonBuilder.allowStructuredMapKeys]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/allow-structured-map-keys.html [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 diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index acd654b351..65ff69c269 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -114,9 +114,9 @@ Once the project is set up, we can start serializing some classes. * [Lenient parsing](json.md#lenient-parsing) * [Ignoring unknown keys](json.md#ignoring-unknown-keys) * [Alternative Json names](json.md#alternative-json-names) - * [Coercing input values](json.md#coercing-input-values) * [Encoding defaults](json.md#encoding-defaults) * [Explicit nulls](json.md#explicit-nulls) + * [Coercing input values](json.md#coercing-input-values) * [Allowing structured map keys](json.md#allowing-structured-map-keys) * [Allowing special floating-point values](json.md#allowing-special-floating-point-values) * [Class discriminator for polymorphism](json.md#class-discriminator-for-polymorphism) diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/AbstractJsonImplicitNullsTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/AbstractJsonImplicitNullsTest.kt index a2f4a9dfba..7b5b327d55 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/AbstractJsonImplicitNullsTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/AbstractJsonImplicitNullsTest.kt @@ -84,8 +84,13 @@ abstract class AbstractJsonImplicitNullsTest { } @Test - fun testDecodeOptional() { + fun testOptional() { + val encoded = format.encode(WithOptional(null), WithOptional.serializer()) val json = """{}""" + assertEquals(json, encoded) + // Same result when `null` is used instead of `1`: + val encodedWithNullInsteadOfDefault = format.encode(WithOptional(null, null), WithOptional.serializer()) + assertEquals(json, encodedWithNullInsteadOfDefault) val decoded = format.decode(json, WithOptional.serializer()) assertEquals(WithOptional(null), decoded) 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 3d7c332271..9607b6a65a 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonCoerceInputValuesTest.kt @@ -39,6 +39,16 @@ class JsonCoerceInputValuesTest : JsonTestBase() { val e: SampleEnum ) + @Serializable + data class NullableEnumWithoutDefault( + val e: SampleEnum? + ) + + @Serializable + data class NullableEnumWithDefault( + val e: SampleEnum? = SampleEnum.OptionC + ) + val json = Json { coerceInputValues = true isLenient = true @@ -142,4 +152,23 @@ class JsonCoerceInputValuesTest : JsonTestBase() { assertEquals(e2.message, e1.message) } } + + @Test + fun testNullableEnumWithoutDefault() { + val j = Json(json) { explicitNulls = false } + parametrizedTest { mode -> + assertEquals(NullableEnumWithoutDefault(null), j.decodeFromString("{}")) + assertEquals(NullableEnumWithoutDefault(null), j.decodeFromString("""{"e":"incorrect"}""")) + } + } + + @Test + fun testNullableEnumWithDefault() { + val j = Json(json) { explicitNulls = false } + parametrizedTest { mode -> + assertEquals(NullableEnumWithDefault(), j.decodeFromString("{}")) + assertEquals(NullableEnumWithDefault(), j.decodeFromString("""{"e":"incorrect"}""")) + assertEquals(NullableEnumWithDefault(null), j.decodeFromString("""{"e":null}""")) + } + } } diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt index f0702064c8..41d1e8a2b0 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt @@ -346,10 +346,14 @@ public class JsonBuilder internal constructor(json: Json) { * Specifies whether `null` values should be encoded for nullable properties and must be present in JSON object * during decoding. * - * When this flag is disabled properties with `null` values without default are not encoded; + * When this flag is disabled properties with `null` values are not encoded; * during decoding, the absence of a field value is treated as `null` for nullable properties without a default value. + * * `true` by default. * + * It is possible to make decoder treat some invalid input data as the missing field to enhance the functionality of this flag. + * See [coerceInputValues] documentation for details. + * * Example of usage: * ``` * @Serializable @@ -368,8 +372,28 @@ public class JsonBuilder internal constructor(json: Json) { * // Fails with "MissingFieldException: Field 'description' is required" * Json.decodeFromString("""{"name":"unknown"}""") * ``` + * + * Exercise extra caution if you want to use this flag and have non-typical classes with properties + * that are nullable, but have default value that is not `null`. In that case, encoding and decoding will not + * be symmetrical if `null` is omitted from the output. + * Example of such a pitfall: + * + * ``` + * @Serializable + * data class Example(val nullable: String? = "non-null default") + * + * val json = Json { explicitNulls = false } + * + * val original = Example(null) + * val s = json.encodeToString(original) + * // prints "{}" because of explicitNulls flag + * println(s) + * val decoded = json.decodeFromString(s) + * // Prints "non-null default" because default value is inserted since `nullable` field is missing in the input + * println(decoded.nullable) + * println(decoded != original) // true + * ``` */ - @ExperimentalSerializationApi public var explicitNulls: Boolean = json.configuration.explicitNulls /** @@ -434,9 +458,32 @@ public class JsonBuilder internal constructor(json: Json) { public var prettyPrintIndent: String = json.configuration.prettyPrintIndent /** - * Enables coercing incorrect JSON values to the default property value (if exists) in the following cases: + * Enables coercing incorrect JSON values 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. + * 2. Property type is an enum type, but JSON value contains an unknown enum member. + * + * Coerced values are treated as missing; they are replaced either with a default property value if it exists, or with a `null` if [explicitNulls] flag + * is set to `false` and a property is nullable. + * + * Example of usage: + * ``` + * enum class Choice { A, B, C } + * + * @Serializable + * data class Example1(val a: String = "default", b: Choice = Choice.A, c: Choice? = null) + * + * val coercingJson = Json { coerceInputValues = true } + * // Decodes Example1("default", Choice.A, null) instance + * coercingJson.decodeFromString("""{"a": null, "b": "unknown", "c": "unknown"}""") + * + * @Serializable + * data class Example2(val c: Choice?) + * + * val coercingImplicitJson = Json(coercingJson) { explicitNulls = false } + * // Decodes Example2(null) instance. + * coercingImplicitJson.decodeFromString("""{"c": "unknown"}""") + * ``` * * `false` by default. */ diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt index dba87b1010..dc69644e4b 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt @@ -22,7 +22,6 @@ public class JsonConfiguration @OptIn(ExperimentalSerializationApi::class) inter public val isLenient: Boolean = false, public val allowStructuredMapKeys: Boolean = false, public val prettyPrint: Boolean = false, - @ExperimentalSerializationApi public val explicitNulls: Boolean = true, @ExperimentalSerializationApi public val prettyPrintIndent: String = " ", 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 9128f3a276..2a9a971bf6 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt @@ -116,9 +116,9 @@ internal inline fun Json.tryCoerceValue( peekString: () -> String?, onEnumCoercing: () -> Unit = {} ): Boolean { - if (!descriptor.isElementOptional(index)) return false + val isOptional = descriptor.isElementOptional(index) val elementDescriptor = descriptor.getElementDescriptor(index) - if (!elementDescriptor.isNullable && peekNull(true)) return true + if (isOptional && !elementDescriptor.isNullable && peekNull(true)) return true if (elementDescriptor.kind == SerialKind.ENUM) { if (elementDescriptor.isNullable && peekNull(false)) { return false @@ -127,7 +127,8 @@ internal inline fun Json.tryCoerceValue( val enumValue = peekString() ?: return false // if value is not a string, decodeEnum() will throw correct exception val enumIndex = elementDescriptor.getJsonNameIndex(this, enumValue) - if (enumIndex == CompositeDecoder.UNKNOWN_NAME) { + val coerceToNull = !configuration.explicitNulls && elementDescriptor.isNullable + if (enumIndex == CompositeDecoder.UNKNOWN_NAME && (isOptional || coerceToNull)) { onEnumCoercing() return true } diff --git a/guide/example/example-json-05.kt b/guide/example/example-json-05.kt index e1b54225ef..809cc9ed61 100644 --- a/guide/example/example-json-05.kt +++ b/guide/example/example-json-05.kt @@ -4,14 +4,16 @@ package example.exampleJson05 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { coerceInputValues = true } +val format = Json { encodeDefaults = true } @Serializable -data class Project(val name: String, val language: String = "Kotlin") +class Project( + val name: String, + val language: String = "Kotlin", + val website: String? = null +) fun main() { - val data = format.decodeFromString(""" - {"name":"kotlinx.serialization","language":null} - """) - println(data) + val data = Project("kotlinx.serialization") + println(format.encodeToString(data)) } diff --git a/guide/example/example-json-06.kt b/guide/example/example-json-06.kt index 605b48847e..776e3ec43e 100644 --- a/guide/example/example-json-06.kt +++ b/guide/example/example-json-06.kt @@ -4,16 +4,20 @@ package example.exampleJson06 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { encodeDefaults = true } +val format = Json { explicitNulls = false } @Serializable -class Project( +data class Project( val name: String, - val language: String = "Kotlin", - val website: String? = null + val language: String, + val version: String? = "1.2.2", + val website: String?, + val description: String? = null ) fun main() { - val data = Project("kotlinx.serialization") - println(format.encodeToString(data)) + val data = Project("kotlinx.serialization", "Kotlin", null, null, null) + val json = format.encodeToString(data) + println(json) + println(format.decodeFromString(json)) } diff --git a/guide/example/example-json-07.kt b/guide/example/example-json-07.kt index 60aa2b28ff..4d9ad2c0a8 100644 --- a/guide/example/example-json-07.kt +++ b/guide/example/example-json-07.kt @@ -4,20 +4,14 @@ package example.exampleJson07 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { explicitNulls = false } +val format = Json { coerceInputValues = true } @Serializable -data class Project( - val name: String, - val language: String, - val version: String? = "1.2.2", - val website: String?, - val description: String? = null -) +data class Project(val name: String, val language: String = "Kotlin") fun main() { - val data = Project("kotlinx.serialization", "Kotlin", null, null, null) - val json = format.encodeToString(data) - println(json) - println(format.decodeFromString(json)) + val data = format.decodeFromString(""" + {"name":"kotlinx.serialization","language":null} + """) + println(data) } diff --git a/guide/example/example-json-08.kt b/guide/example/example-json-08.kt index 86e6298f0c..501a38eb0d 100644 --- a/guide/example/example-json-08.kt +++ b/guide/example/example-json-08.kt @@ -4,15 +4,17 @@ package example.exampleJson08 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { allowStructuredMapKeys = true } +enum class Color { BLACK, WHITE } @Serializable -data class Project(val name: String) +data class Brush(val foreground: Color = Color.BLACK, val background: Color?) + +val json = Json { + coerceInputValues = true + explicitNulls = false +} fun main() { - val map = mapOf( - Project("kotlinx.serialization") to "Serialization", - Project("kotlinx.coroutines") to "Coroutines" - ) - println(format.encodeToString(map)) + val brush = json.decodeFromString("""{"foreground":"pink", "background":"purple"}""") + println(brush) } diff --git a/guide/example/example-json-09.kt b/guide/example/example-json-09.kt index 1303fdd727..a0ed6329ce 100644 --- a/guide/example/example-json-09.kt +++ b/guide/example/example-json-09.kt @@ -4,14 +4,15 @@ package example.exampleJson09 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { allowSpecialFloatingPointValues = true } +val format = Json { allowStructuredMapKeys = true } @Serializable -class Data( - val value: Double -) +data class Project(val name: String) fun main() { - val data = Data(Double.NaN) - println(format.encodeToString(data)) + val map = mapOf( + Project("kotlinx.serialization") to "Serialization", + Project("kotlinx.coroutines") to "Coroutines" + ) + println(format.encodeToString(map)) } diff --git a/guide/example/example-json-10.kt b/guide/example/example-json-10.kt index 49df395e58..dc528bb64a 100644 --- a/guide/example/example-json-10.kt +++ b/guide/example/example-json-10.kt @@ -4,18 +4,14 @@ package example.exampleJson10 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { classDiscriminator = "#class" } +val format = Json { allowSpecialFloatingPointValues = true } @Serializable -sealed class Project { - abstract val name: String -} - -@Serializable -@SerialName("owned") -class OwnedProject(override val name: String, val owner: String) : Project() +class Data( + val value: Double +) fun main() { - val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") + val data = Data(Double.NaN) println(format.encodeToString(data)) } diff --git a/guide/example/example-json-11.kt b/guide/example/example-json-11.kt index 57e350ade5..31f873151b 100644 --- a/guide/example/example-json-11.kt +++ b/guide/example/example-json-11.kt @@ -4,28 +4,18 @@ package example.exampleJson11 import kotlinx.serialization.* import kotlinx.serialization.json.* -@Serializable -@JsonClassDiscriminator("message_type") -sealed class Base - -@Serializable // Class discriminator is inherited from Base -sealed class ErrorClass: Base() - -@Serializable -data class Message(val message: Base, val error: ErrorClass?) +val format = Json { classDiscriminator = "#class" } @Serializable -@SerialName("my.app.BaseMessage") -data class BaseMessage(val message: String) : Base() +sealed class Project { + abstract val name: String +} @Serializable -@SerialName("my.app.GenericError") -data class GenericError(@SerialName("error_code") val errorCode: Int) : ErrorClass() - - -val format = Json { classDiscriminator = "#class" } +@SerialName("owned") +class OwnedProject(override val name: String, val owner: String) : Project() fun main() { - val data = Message(BaseMessage("not found"), GenericError(404)) + val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") println(format.encodeToString(data)) } diff --git a/guide/example/example-json-12.kt b/guide/example/example-json-12.kt index 99a872b72c..da76d4c39c 100644 --- a/guide/example/example-json-12.kt +++ b/guide/example/example-json-12.kt @@ -4,17 +4,28 @@ package example.exampleJson12 import kotlinx.serialization.* import kotlinx.serialization.json.* -val format = Json { classDiscriminatorMode = ClassDiscriminatorMode.NONE } +@Serializable +@JsonClassDiscriminator("message_type") +sealed class Base + +@Serializable // Class discriminator is inherited from Base +sealed class ErrorClass: Base() @Serializable -sealed class Project { - abstract val name: String -} +data class Message(val message: Base, val error: ErrorClass?) @Serializable -class OwnedProject(override val name: String, val owner: String) : Project() +@SerialName("my.app.BaseMessage") +data class BaseMessage(val message: String) : Base() + +@Serializable +@SerialName("my.app.GenericError") +data class GenericError(@SerialName("error_code") val errorCode: Int) : ErrorClass() + + +val format = Json { classDiscriminator = "#class" } fun main() { - val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") + val data = Message(BaseMessage("not found"), GenericError(404)) println(format.encodeToString(data)) } diff --git a/guide/example/example-json-13.kt b/guide/example/example-json-13.kt index e20afe286a..5da592e745 100644 --- a/guide/example/example-json-13.kt +++ b/guide/example/example-json-13.kt @@ -4,13 +4,17 @@ package example.exampleJson13 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) +class OwnedProject(override val name: String, val owner: String) : Project() fun main() { - println(format.decodeFromString("""{"cases":["value_A", "alternative"]}""")) + val data: Project = OwnedProject("kotlinx.coroutines", "kotlin") + println(format.encodeToString(data)) } diff --git a/guide/example/example-json-14.kt b/guide/example/example-json-14.kt index 50de55fdc7..67d2e2d32e 100644 --- a/guide/example/example-json-14.kt +++ b/guide/example/example-json-14.kt @@ -4,12 +4,13 @@ package example.exampleJson14 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) fun main() { - val project = format.decodeFromString("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""") - println(format.encodeToString(project.copy(projectName = "kotlinx.serialization"))) + println(format.decodeFromString("""{"cases":["value_A", "alternative"]}""")) } diff --git a/guide/example/example-json-15.kt b/guide/example/example-json-15.kt index de612b8223..db504cbff0 100644 --- a/guide/example/example-json-15.kt +++ b/guide/example/example-json-15.kt @@ -4,54 +4,12 @@ package example.exampleJson15 import kotlinx.serialization.* import kotlinx.serialization.json.* -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.descriptors.* -import kotlin.io.encoding.* - -@OptIn(ExperimentalEncodingApi::class) -object ByteArrayAsBase64Serializer : KSerializer { - private val base64 = Base64.Default - - override val descriptor: SerialDescriptor - get() = PrimitiveSerialDescriptor( - "ByteArrayAsBase64Serializer", - PrimitiveKind.STRING - ) - - override fun serialize(encoder: Encoder, value: ByteArray) { - val base64Encoded = base64.encode(value) - encoder.encodeString(base64Encoded) - } - - override fun deserialize(decoder: Decoder): ByteArray { - val base64Decoded = decoder.decodeString() - return base64.decode(base64Decoded) - } -} - @Serializable -data class Value( - @Serializable(with = ByteArrayAsBase64Serializer::class) - val base64Input: ByteArray -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Value - return base64Input.contentEquals(other.base64Input) - } +data class Project(val projectName: String, val projectOwner: String) - override fun hashCode(): Int { - return base64Input.contentHashCode() - } -} +val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase } fun main() { - val string = "foo string" - val value = Value(string.toByteArray()) - val encoded = Json.encodeToString(value) - println(encoded) - val decoded = Json.decodeFromString(encoded) - println(decoded.base64Input.decodeToString()) + val project = format.decodeFromString("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""") + println(format.encodeToString(project.copy(projectName = "kotlinx.serialization"))) } diff --git a/guide/example/example-json-16.kt b/guide/example/example-json-16.kt index acd69fe809..eaa7c90a1c 100644 --- a/guide/example/example-json-16.kt +++ b/guide/example/example-json-16.kt @@ -4,9 +4,54 @@ package example.exampleJson16 import kotlinx.serialization.* import kotlinx.serialization.json.* +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.descriptors.* +import kotlin.io.encoding.* + +@OptIn(ExperimentalEncodingApi::class) +object ByteArrayAsBase64Serializer : KSerializer { + private val base64 = Base64.Default + + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor( + "ByteArrayAsBase64Serializer", + PrimitiveKind.STRING + ) + + override fun serialize(encoder: Encoder, value: ByteArray) { + val base64Encoded = base64.encode(value) + encoder.encodeString(base64Encoded) + } + + override fun deserialize(decoder: Decoder): ByteArray { + val base64Decoded = decoder.decodeString() + return base64.decode(base64Decoded) + } +} + +@Serializable +data class Value( + @Serializable(with = ByteArrayAsBase64Serializer::class) + val base64Input: ByteArray +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Value + return base64Input.contentEquals(other.base64Input) + } + + override fun hashCode(): Int { + return base64Input.contentHashCode() + } +} + fun main() { - val element = Json.parseToJsonElement(""" - {"name":"kotlinx.serialization","language":"Kotlin"} - """) - println(element) + val string = "foo string" + val value = Value(string.toByteArray()) + val encoded = Json.encodeToString(value) + println(encoded) + val decoded = Json.decodeFromString(encoded) + println(decoded.base64Input.decodeToString()) } diff --git a/guide/example/example-json-17.kt b/guide/example/example-json-17.kt index 7e11ee20df..ba7177d6c1 100644 --- a/guide/example/example-json-17.kt +++ b/guide/example/example-json-17.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-18.kt b/guide/example/example-json-18.kt index 061845a196..f378615572 100644 --- a/guide/example/example-json-18.kt +++ b/guide/example/example-json-18.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-19.kt b/guide/example/example-json-19.kt index 500cea261f..66ce99b7ac 100644 --- a/guide/example/example-json-19.kt +++ b/guide/example/example-json-19.kt @@ -4,14 +4,20 @@ package example.exampleJson19 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(element) - println(data) + println(element) } diff --git a/guide/example/example-json-20.kt b/guide/example/example-json-20.kt index f80fba28cb..8f1c786eee 100644 --- a/guide/example/example-json-20.kt +++ b/guide/example/example-json-20.kt @@ -4,20 +4,14 @@ package example.exampleJson20 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(element) + println(data) } diff --git a/guide/example/example-json-21.kt b/guide/example/example-json-21.kt index 4d43d4d9a4..2b1d1109cf 100644 --- a/guide/example/example-json-21.kt +++ b/guide/example/example-json-21.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-22.kt b/guide/example/example-json-22.kt index 27982dbf1c..952ca30cf3 100644 --- a/guide/example/example-json-22.kt +++ b/guide/example/example-json-22.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-23.kt b/guide/example/example-json-23.kt index 1501f0f61d..14f70e2330 100644 --- a/guide/example/example-json-23.kt +++ b/guide/example/example-json-23.kt @@ -4,7 +4,20 @@ package example.exampleJson23 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-24.kt b/guide/example/example-json-24.kt index 5b54222aec..c9138d243e 100644 --- a/guide/example/example-json-24.kt +++ b/guide/example/example-json-24.kt @@ -4,29 +4,7 @@ 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 -) - -@Serializable -data class User(val name: String) - -object UserListSerializer : JsonTransformingSerializer>(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(""" - {"name":"kotlinx.serialization","users":{"name":"kotlin"}} - """)) - println(Json.decodeFromString(""" - {"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-25.kt b/guide/example/example-json-25.kt index 337eaa4d9a..67c3bf5a83 100644 --- a/guide/example/example-json-25.kt +++ b/guide/example/example-json-25.kt @@ -17,14 +17,16 @@ data class Project( data class User(val name: String) object UserListSerializer : JsonTransformingSerializer>(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(""" + {"name":"kotlinx.serialization","users":{"name":"kotlin"}} + """)) + println(Json.decodeFromString(""" + {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]} + """)) } diff --git a/guide/example/example-json-26.kt b/guide/example/example-json-26.kt index 7939b7ff96..812e49679a 100644 --- a/guide/example/example-json-26.kt +++ b/guide/example/example-json-26.kt @@ -4,19 +4,27 @@ package example.exampleJson26 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 +) + @Serializable -class Project(val name: String, val language: String) +data class User(val name: String) + +object UserListSerializer : JsonTransformingSerializer>(ListSerializer(User.serializer())) { -object ProjectSerializer : JsonTransformingSerializer(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-27.kt b/guide/example/example-json-27.kt index 5b316034f2..e28b50ad6f 100644 --- a/guide/example/example-json-27.kt +++ b/guide/example/example-json-27.kt @@ -4,33 +4,19 @@ package example.exampleJson27 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::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.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-28.kt b/guide/example/example-json-28.kt index 1006df2c9e..52ca872c39 100644 --- a/guide/example/example-json-28.kt +++ b/guide/example/example-json-28.kt @@ -4,56 +4,33 @@ package example.exampleJson28 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 { - data class Ok(val data: T) : Response() - data class Error(val message: String) : Response() +@Serializable +abstract class Project { + abstract val name: String } -class ResponseSerializer(private val dataSerializer: KSerializer) : KSerializer> { - override val descriptor: SerialDescriptor = buildSerialDescriptor("Response", PolymorphicKind.SEALED) { - element("Ok", dataSerializer.descriptor) - element("Error", buildClassSerialDescriptor("Error") { - element("message") - }) - } +@Serializable +data class BasicProject(override val name: String): Project() - override fun deserialize(decoder: Decoder): Response { - // 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) { - // 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::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>>(string)) + println(Json.decodeFromString(ListSerializer(ProjectSerializer), string)) } diff --git a/guide/example/example-json-29.kt b/guide/example/example-json-29.kt index 312b71e755..db5f0b9677 100644 --- a/guide/example/example-json-29.kt +++ b/guide/example/example-json-29.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 { + data class Ok(val data: T) : Response() + data class Error(val message: String) : Response() +} -object UnknownProjectSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") { - element("name") - element("details") +class ResponseSerializer(private val dataSerializer: KSerializer) : KSerializer> { + override val descriptor: SerialDescriptor = buildSerialDescriptor("Response", PolymorphicKind.SEALED) { + element("Ok", dataSerializer.descriptor) + element("Error", buildClassSerialDescriptor("Error") { + element("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 { + // 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) { + // 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>>(string)) } diff --git a/guide/example/example-json-30.kt b/guide/example/example-json-30.kt new file mode 100644 index 0000000000..3aaa045029 --- /dev/null +++ b/guide/example/example-json-30.kt @@ -0,0 +1,37 @@ +// This file was automatically generated from json.md by Knit tool. Do not edit. +package example.exampleJson30 + +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 { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") { + element("name") + element("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 8c0a156799..70aea3b60e 100644 --- a/guide/test/JsonTest.kt +++ b/guide/test/JsonTest.kt @@ -40,117 +40,114 @@ class JsonTest { @Test fun testExampleJson05() { captureOutput("ExampleJson05") { example.exampleJson05.main() }.verifyOutputLines( - "Project(name=kotlinx.serialization, language=Kotlin)" + "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\",\"website\":null}" ) } @Test fun testExampleJson06() { captureOutput("ExampleJson06") { example.exampleJson06.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\",\"website\":null}" + "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}", + "Project(name=kotlinx.serialization, language=Kotlin, version=1.2.2, website=null, description=null)" ) } @Test fun testExampleJson07() { captureOutput("ExampleJson07") { example.exampleJson07.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}", - "Project(name=kotlinx.serialization, language=Kotlin, version=1.2.2, website=null, description=null)" + "Project(name=kotlinx.serialization, language=Kotlin)" ) } @Test fun testExampleJson08() { captureOutput("ExampleJson08") { example.exampleJson08.main() }.verifyOutputLines( - "[{\"name\":\"kotlinx.serialization\"},\"Serialization\",{\"name\":\"kotlinx.coroutines\"},\"Coroutines\"]" + "Brush(foreground=BLACK, background=null)" ) } @Test fun testExampleJson09() { captureOutput("ExampleJson09") { example.exampleJson09.main() }.verifyOutputLines( - "{\"value\":NaN}" + "[{\"name\":\"kotlinx.serialization\"},\"Serialization\",{\"name\":\"kotlinx.coroutines\"},\"Coroutines\"]" ) } @Test fun testExampleJson10() { captureOutput("ExampleJson10") { example.exampleJson10.main() }.verifyOutputLines( - "{\"#class\":\"owned\",\"name\":\"kotlinx.coroutines\",\"owner\":\"kotlin\"}" + "{\"value\":NaN}" ) } @Test fun testExampleJson11() { captureOutput("ExampleJson11") { example.exampleJson11.main() }.verifyOutputLines( - "{\"message\":{\"message_type\":\"my.app.BaseMessage\",\"message\":\"not found\"},\"error\":{\"message_type\":\"my.app.GenericError\",\"error_code\":404}}" + "{\"#class\":\"owned\",\"name\":\"kotlinx.coroutines\",\"owner\":\"kotlin\"}" ) } @Test fun testExampleJson12() { captureOutput("ExampleJson12") { example.exampleJson12.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.coroutines\",\"owner\":\"kotlin\"}" + "{\"message\":{\"message_type\":\"my.app.BaseMessage\",\"message\":\"not found\"},\"error\":{\"message_type\":\"my.app.GenericError\",\"error_code\":404}}" ) } @Test fun testExampleJson13() { captureOutput("ExampleJson13") { example.exampleJson13.main() }.verifyOutputLines( - "CasesList(cases=[VALUE_A, VALUE_B])" + "{\"name\":\"kotlinx.coroutines\",\"owner\":\"kotlin\"}" ) } @Test fun testExampleJson14() { captureOutput("ExampleJson14") { example.exampleJson14.main() }.verifyOutputLines( - "{\"project_name\":\"kotlinx.serialization\",\"project_owner\":\"Kotlin\"}" + "CasesList(cases=[VALUE_A, VALUE_B])" ) } @Test fun testExampleJson15() { captureOutput("ExampleJson15") { example.exampleJson15.main() }.verifyOutputLines( - "{\"base64Input\":\"Zm9vIHN0cmluZw==\"}", - "foo string" + "{\"project_name\":\"kotlinx.serialization\",\"project_owner\":\"Kotlin\"}" ) } @Test fun testExampleJson16() { captureOutput("ExampleJson16") { example.exampleJson16.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}" + "{\"base64Input\":\"Zm9vIHN0cmluZw==\"}", + "foo string" ) } @Test fun testExampleJson17() { captureOutput("ExampleJson17") { example.exampleJson17.main() }.verifyOutputLines( - "9042" + "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}" ) } @Test fun testExampleJson18() { captureOutput("ExampleJson18") { example.exampleJson18.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}" + "9042" ) } @Test fun testExampleJson19() { captureOutput("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLines( - "Project(name=kotlinx.serialization, language=Kotlin)" + "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}" ) } @Test fun testExampleJson20() { captureOutput("ExampleJson20") { example.exampleJson20.main() }.verifyOutputLines( - "{", - " \"pi_double\": 3.141592653589793,", - " \"pi_string\": \"3.141592653589793238462643383279\"", - "}" + "Project(name=kotlinx.serialization, language=Kotlin)" ) } @@ -158,7 +155,6 @@ class JsonTest { fun testExampleJson21() { captureOutput("ExampleJson21") { example.exampleJson21.main() }.verifyOutputLines( "{", - " \"pi_literal\": 3.141592653589793238462643383279,", " \"pi_double\": 3.141592653589793,", " \"pi_string\": \"3.141592653589793238462643383279\"", "}" @@ -168,59 +164,70 @@ class JsonTest { @Test fun testExampleJson22() { captureOutput("ExampleJson22") { example.exampleJson22.main() }.verifyOutputLines( - "3.141592653589793238462643383279" + "{", + " \"pi_literal\": 3.141592653589793238462643383279,", + " \"pi_double\": 3.141592653589793,", + " \"pi_string\": \"3.141592653589793238462643383279\"", + "}" ) } @Test fun testExampleJson23() { - captureOutput("ExampleJson23") { example.exampleJson23.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("ExampleJson23") { example.exampleJson23.main() }.verifyOutputLines( + "3.141592653589793238462643383279" ) } @Test fun testExampleJson24() { - captureOutput("ExampleJson24") { example.exampleJson24.main() }.verifyOutputLines( - "Project(name=kotlinx.serialization, users=[User(name=kotlin)])", - "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])" + captureOutput("ExampleJson24") { example.exampleJson24.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 testExampleJson25() { captureOutput("ExampleJson25") { example.exampleJson25.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 testExampleJson26() { captureOutput("ExampleJson26") { example.exampleJson26.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}", - "{\"name\":\"kotlinx.serialization\"}" + "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}" ) } @Test fun testExampleJson27() { captureOutput("ExampleJson27") { example.exampleJson27.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 testExampleJson28() { captureOutput("ExampleJson28") { example.exampleJson28.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 testExampleJson29() { captureOutput("ExampleJson29") { example.exampleJson29.main() }.verifyOutputLines( + "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]", + "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]" + ) + } + + @Test + fun testExampleJson30() { + captureOutput("ExampleJson30") { example.exampleJson30.main() }.verifyOutputLines( "UnknownProject(name=example, details={\"type\":\"unknown\",\"maintainer\":\"Unknown\",\"license\":\"Apache 2.0\"})" ) }