summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLeonid Startsev <sandwwraith@users.noreply.github.com>2023-07-05 19:18:17 +0200
committerGitHub <noreply@github.com>2023-07-05 19:18:17 +0200
commit782b9f3be9970e0fd36215a86bf7fdba9f2bfe83 (patch)
treedcec941bc829929cd66d004825c5eb728e5d8d23
parenta87b0f1d89f927896bbeeefa4377bfdeafbf7cb0 (diff)
downloadkotlinx.serialization-782b9f3be9970e0fd36215a86bf7fdba9f2bfe83.tar.gz
Introduce 'decodeEnumsCaseInsensitive' feature to Json. (#2345)
It allows decoding enum values in a case-insensitive manner. It does not affect CLASS kinds or encoding. It is one of the most-voted feature requests. Also enhance JsonNamingStrategy documentation. Fixes #209
-rw-r--r--docs/json.md65
-rw-r--r--docs/serialization-guide.md1
-rw-r--r--formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonEnumsCaseInsensitiveTest.kt171
-rw-r--r--formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt1
-rw-r--r--formats/json-tests/commonTest/src/kotlinx/serialization/test/TestingFramework.kt4
-rw-r--r--formats/json/api/kotlinx-serialization-json.api3
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/Json.kt33
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt6
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt13
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt35
-rw-r--r--guide/example/example-json-12.kt11
-rw-r--r--guide/example/example-json-13.kt11
-rw-r--r--guide/example/example-json-14.kt10
-rw-r--r--guide/example/example-json-15.kt23
-rw-r--r--guide/example/example-json-16.kt18
-rw-r--r--guide/example/example-json-17.kt20
-rw-r--r--guide/example/example-json-18.kt6
-rw-r--r--guide/example/example-json-19.kt30
-rw-r--r--guide/example/example-json-20.kt17
-rw-r--r--guide/example/example-json-21.kt26
-rw-r--r--guide/example/example-json-22.kt16
-rw-r--r--guide/example/example-json-23.kt28
-rw-r--r--guide/example/example-json-24.kt36
-rw-r--r--guide/example/example-json-25.kt59
-rw-r--r--guide/example/example-json-26.kt58
-rw-r--r--guide/example/example-json-27.kt37
-rw-r--r--guide/test/JsonTest.kt53
27 files changed, 536 insertions, 255 deletions
diff --git a/docs/json.md b/docs/json.md
index 9fd8cac4..07476f97 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)
+ * [Decoding enums in a case-insensitive manner](#decoding-enums-in-a-case-insensitive-manner)
* [Global naming strategy](#global-naming-strategy)
* [Json elements](#json-elements)
* [Parsing to Json element](#parsing-to-json-element)
@@ -469,6 +470,39 @@ As you can see, discriminator from the `Base` class is used:
<!--- 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
+using either uppercase underscore-separated names or upper camel case names.
+[Json] uses exact Kotlin enum values names for decoding by default.
+However, sometimes third-party JSONs have such values named in lowercase or some mixed case.
+In this case, it is possible to decode enum values in a case-insensitive manner using [JsonBuilder.decodeEnumsCaseInsensitive] property:
+
+```kotlin
+val format = Json { decodeEnumsCaseInsensitive = true }
+
+enum class Cases { VALUE_A, @JsonNames("Alternative") VALUE_B }
+
+@Serializable
+data class CasesList(val cases: List<Cases>)
+
+fun main() {
+ println(format.decodeFromString<CasesList>("""{"cases":["value_A", "alternative"]}"""))
+}
+```
+
+> You can get the full code [here](../guide/example/example-json-12.kt).
+
+It affects serial names as well as alternative names specified with [JsonNames] annotation, so both values are successfully decoded:
+
+```text
+CasesList(cases=[VALUE_A, VALUE_B])
+```
+
+This property does not affect encoding in any way.
+
+<!--- TEST -->
+
### Global naming strategy
If properties' names in Json input are different from Kotlin ones, it is recommended to specify the name
@@ -489,7 +523,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).
As you can see, both serialization and deserialization work as if all serial names are transformed from camel case to snake case:
@@ -541,7 +575,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).
A `JsonElement` prints itself as a valid JSON:
@@ -584,7 +618,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).
The above example sums `votes` in all objects in the `forks` array, ignoring the objects that have no `votes`:
@@ -624,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).
As a result, you get a proper JSON string:
@@ -653,7 +687,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).
The result is exactly what you would expect:
@@ -699,7 +733,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).
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.
@@ -739,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).
`pi_literal` now accurately matches the value defined.
@@ -779,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).
The exact value of `pi` is decoded, with all 30 decimal places of precision that were in the source JSON.
@@ -801,7 +835,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).
```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
@@ -877,7 +911,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).
The output shows that both cases are correctly deserialized into a Kotlin [List].
@@ -929,7 +963,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).
You end up with a single JSON object, not an array with one element:
@@ -974,7 +1008,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).
See the effect of the custom serializer:
@@ -1047,7 +1081,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).
No class discriminator is added in the JSON output:
@@ -1143,7 +1177,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).
This gives you fine-grained control on the representation of the `Response` class in the JSON output:
@@ -1208,7 +1242,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).
```text
UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})
@@ -1262,6 +1296,7 @@ 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.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
[JsonElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-element/index.html
diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md
index 7048206a..445d32e3 100644
--- a/docs/serialization-guide.md
+++ b/docs/serialization-guide.md
@@ -119,6 +119,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='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)
* <a name='parsing-to-json-element'></a>[Parsing to Json element](json.md#parsing-to-json-element)
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonEnumsCaseInsensitiveTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonEnumsCaseInsensitiveTest.kt
new file mode 100644
index 00000000..ce80a346
--- /dev/null
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonEnumsCaseInsensitiveTest.kt
@@ -0,0 +1,171 @@
+package kotlinx.serialization.features
+
+import kotlinx.serialization.*
+import kotlinx.serialization.json.*
+import kotlinx.serialization.test.*
+import kotlin.test.*
+
+@Suppress("EnumEntryName")
+class JsonEnumsCaseInsensitiveTest: JsonTestBase() {
+ @Serializable
+ data class Foo(
+ val one: Bar = Bar.BAZ,
+ val two: Bar = Bar.QUX,
+ val three: Bar = Bar.QUX
+ )
+
+ enum class Bar { BAZ, QUX }
+
+ // It seems that we no longer report a warning that @Serializable is required for enums with @SerialName.
+ // It is still required for them to work at top-level.
+ @Serializable
+ enum class Cases {
+ ALL_CAPS,
+ MiXed,
+ all_lower,
+
+ @JsonNames("AltName")
+ hasAltNames,
+
+ @SerialName("SERIAL_NAME")
+ hasSerialName
+ }
+
+ @Serializable
+ data class EnumCases(val cases: List<Cases>)
+
+ val json = Json(default) { decodeEnumsCaseInsensitive = true }
+
+ @Test
+ fun testCases() = noLegacyJs { parametrizedTest { mode ->
+ val input =
+ """{"cases":["ALL_CAPS","all_caps","mixed","MIXED","miXed","all_lower","ALL_LOWER","all_Lower","hasAltNames","HASALTNAMES","altname","ALTNAME","AltName","SERIAL_NAME","serial_name"]}"""
+ val target = listOf(
+ Cases.ALL_CAPS,
+ Cases.ALL_CAPS,
+ Cases.MiXed,
+ Cases.MiXed,
+ Cases.MiXed,
+ Cases.all_lower,
+ Cases.all_lower,
+ Cases.all_lower,
+ Cases.hasAltNames,
+ Cases.hasAltNames,
+ Cases.hasAltNames,
+ Cases.hasAltNames,
+ Cases.hasAltNames,
+ Cases.hasSerialName,
+ Cases.hasSerialName
+ )
+ val decoded = json.decodeFromString<EnumCases>(input, mode)
+ assertEquals(EnumCases(target), decoded)
+ val encoded = json.encodeToString(decoded, mode)
+ assertEquals(
+ """{"cases":["ALL_CAPS","ALL_CAPS","MiXed","MiXed","MiXed","all_lower","all_lower","all_lower","hasAltNames","hasAltNames","hasAltNames","hasAltNames","hasAltNames","SERIAL_NAME","SERIAL_NAME"]}""",
+ encoded
+ )
+ }}
+
+ @Test
+ fun testTopLevelList() = noLegacyJs { parametrizedTest { mode ->
+ val input = """["all_caps","serial_name"]"""
+ val decoded = json.decodeFromString<List<Cases>>(input, mode)
+ assertEquals(listOf(Cases.ALL_CAPS, Cases.hasSerialName), decoded)
+ assertEquals("""["ALL_CAPS","SERIAL_NAME"]""", json.encodeToString(decoded, mode))
+ }}
+
+ @Test
+ fun testTopLevelEnum() = noLegacyJs { parametrizedTest { mode ->
+ val input = """"altName""""
+ val decoded = json.decodeFromString<Cases>(input, mode)
+ assertEquals(Cases.hasAltNames, decoded)
+ assertEquals(""""hasAltNames"""", json.encodeToString(decoded, mode))
+ }}
+
+ @Test
+ fun testSimpleCase() = parametrizedTest { mode ->
+ val input = """{"one":"baz","two":"Qux","three":"QUX"}"""
+ val decoded = json.decodeFromString<Foo>(input, mode)
+ assertEquals(Foo(), decoded)
+ assertEquals("""{"one":"BAZ","two":"QUX","three":"QUX"}""", json.encodeToString(decoded, mode))
+ }
+
+ enum class E { VALUE_A, @JsonNames("ALTERNATIVE") VALUE_B }
+
+ @Test
+ fun testDocSample() = noLegacyJs {
+
+ val j = Json { decodeEnumsCaseInsensitive = true }
+ @Serializable
+ data class Outer(val enums: List<E>)
+
+ println(j.decodeFromString<Outer>("""{"enums":["value_A", "alternative"]}""").enums)
+ }
+
+ @Test
+ fun testCoercingStillWorks() = parametrizedTest { mode ->
+ val withCoercing = Json(json) { coerceInputValues = true }
+ val input = """{"one":"baz","two":"unknown","three":"Que"}"""
+ assertEquals(Foo(), withCoercing.decodeFromString<Foo>(input, mode))
+ }
+
+ @Test
+ fun testCaseInsensitivePriorityOverCoercing() = parametrizedTest { mode ->
+ val withCoercing = Json(json) { coerceInputValues = true }
+ val input = """{"one":"QuX","two":"Baz","three":"Que"}"""
+ assertEquals(Foo(Bar.QUX, Bar.BAZ, Bar.QUX), withCoercing.decodeFromString<Foo>(input, mode))
+ }
+
+ @Test
+ fun testCoercingStillWorksWithNulls() = parametrizedTest { mode ->
+ val withCoercing = Json(json) { coerceInputValues = true }
+ val input = """{"one":"baz","two":"null","three":null}"""
+ assertEquals(Foo(), withCoercing.decodeFromString<Foo>(input, mode))
+ }
+
+ @Test
+ fun testFeatureDisablesProperly() = parametrizedTest { mode ->
+ val disabled = Json(json) {
+ coerceInputValues = true
+ decodeEnumsCaseInsensitive = false
+ }
+ val input = """{"one":"BAZ","two":"BAz","three":"baz"}""" // two and three should be coerced to QUX
+ assertEquals(Foo(), disabled.decodeFromString<Foo>(input, mode))
+ }
+
+ @Test
+ fun testFeatureDisabledThrowsWithoutCoercing() = parametrizedTest { mode ->
+ val disabled = Json(json) {
+ coerceInputValues = false
+ decodeEnumsCaseInsensitive = false
+ }
+ val input = """{"one":"BAZ","two":"BAz","three":"baz"}"""
+ assertFailsWithMessage<SerializationException>("does not contain element with name 'BAz'") {
+ disabled.decodeFromString<Foo>(input, mode)
+ }
+ }
+
+ @Serializable enum class BadEnum { Bad, BAD }
+
+ @Serializable data class ListBadEnum(val l: List<BadEnum>)
+
+ @Test
+ fun testLowercaseClashThrowsException() = parametrizedTest { mode ->
+ assertFailsWithMessage<SerializationException>("""The suggested name 'bad' for enum value BAD is already one of the names for enum value Bad""") {
+ // an explicit serializer is required for JSLegacy
+ json.decodeFromString(Box.serializer(BadEnum.serializer()),"""{"boxed":"bad"}""", mode)
+ }
+ assertFailsWithMessage<SerializationException>("""The suggested name 'bad' for enum value BAD is already one of the names for enum value Bad""") {
+ json.decodeFromString(Box.serializer(BadEnum.serializer()),"""{"boxed":"unrelated"}""", mode)
+ }
+ }
+
+ @Test
+ fun testLowercaseClashHandledWithoutFeature() = parametrizedTest { mode ->
+ val disabled = Json(json) {
+ coerceInputValues = false
+ decodeEnumsCaseInsensitive = false
+ }
+ assertEquals(ListBadEnum(listOf(BadEnum.Bad, BadEnum.BAD)), disabled.decodeFromString("""{"l":["Bad","BAD"]}"""))
+ }
+}
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt
index 330d5d2b..68f36def 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/features/JsonNamingStrategyTest.kt
@@ -24,6 +24,7 @@ class JsonNamingStrategyTest : JsonTestBase() {
val jsonWithNaming = Json(default) {
namingStrategy = JsonNamingStrategy.SnakeCase
+ decodeEnumsCaseInsensitive = true // check that related feature does not break anything
}
@Test
diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/test/TestingFramework.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/test/TestingFramework.kt
index b46afe69..e941f047 100644
--- a/formats/json-tests/commonTest/src/kotlinx/serialization/test/TestingFramework.kt
+++ b/formats/json-tests/commonTest/src/kotlinx/serialization/test/TestingFramework.kt
@@ -78,7 +78,7 @@ inline fun assertFailsWithSerialMessage(
)
assertTrue(
exception.message!!.contains(message),
- "expected:<${exception.message}> but was:<$message>"
+ "expected:<$message> but was:<${exception.message}>"
)
}
inline fun <reified T : Throwable> assertFailsWithMessage(
@@ -89,6 +89,6 @@ inline fun <reified T : Throwable> assertFailsWithMessage(
val exception = assertFailsWith(T::class, assertionMessage, block)
assertTrue(
exception.message!!.contains(message),
- "expected:<${exception.message}> but was:<$message>"
+ "expected:<$message> but was:<${exception.message}>"
)
}
diff --git a/formats/json/api/kotlinx-serialization-json.api b/formats/json/api/kotlinx-serialization-json.api
index 663bd997..ec79e13d 100644
--- a/formats/json/api/kotlinx-serialization-json.api
+++ b/formats/json/api/kotlinx-serialization-json.api
@@ -88,6 +88,7 @@ public final class kotlinx/serialization/json/JsonBuilder {
public final fun getAllowStructuredMapKeys ()Z
public final fun getClassDiscriminator ()Ljava/lang/String;
public final fun getCoerceInputValues ()Z
+ public final fun getDecodeEnumsCaseInsensitive ()Z
public final fun getEncodeDefaults ()Z
public final fun getExplicitNulls ()Z
public final fun getIgnoreUnknownKeys ()Z
@@ -102,6 +103,7 @@ public final class kotlinx/serialization/json/JsonBuilder {
public final fun setAllowStructuredMapKeys (Z)V
public final fun setClassDiscriminator (Ljava/lang/String;)V
public final fun setCoerceInputValues (Z)V
+ public final fun setDecodeEnumsCaseInsensitive (Z)V
public final fun setEncodeDefaults (Z)V
public final fun setExplicitNulls (Z)V
public final fun setIgnoreUnknownKeys (Z)V
@@ -129,6 +131,7 @@ public final class kotlinx/serialization/json/JsonConfiguration {
public final fun getAllowStructuredMapKeys ()Z
public final fun getClassDiscriminator ()Ljava/lang/String;
public final fun getCoerceInputValues ()Z
+ public final fun getDecodeEnumsCaseInsensitive ()Z
public final fun getEncodeDefaults ()Z
public final fun getExplicitNulls ()Z
public final fun getIgnoreUnknownKeys ()Z
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
index 443f1dc3..40dcc23e 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
@@ -288,7 +288,7 @@ public class JsonBuilder internal constructor(json: Json) {
/**
* Enables coercing incorrect JSON values to the default property value in the following cases:
- * 1. JSON value is `null` but property type is non-nullable.
+ * 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.
*
* `false` by default.
@@ -336,6 +336,35 @@ public class JsonBuilder internal constructor(json: Json) {
public var namingStrategy: JsonNamingStrategy? = json.configuration.namingStrategy
/**
+ * Enables decoding enum values in a case-insensitive manner.
+ * Encoding is not affected.
+ *
+ * This affects both enum serial names and alternative names (specified with the [JsonNames] annotation).
+ * In the following example, string `[VALUE_A, VALUE_B]` will be printed:
+ * ```
+ * enum class E { VALUE_A, @JsonNames("ALTERNATIVE") VALUE_B }
+ *
+ * @Serializable
+ * data class Outer(val enums: List<E>)
+ *
+ * val j = Json { decodeEnumsCaseInsensitive = true }
+ * println(j.decodeFromString<Outer>("""{"enums":["value_A", "alternative"]}""").enums)
+ * ```
+ *
+ * If this feature is enabled,
+ * it is no longer possible to decode enum values that have the same name in a lowercase form.
+ * The following code will throw a serialization exception:
+ *
+ * ```
+ * enum class BadEnum { Bad, BAD }
+ * val j = Json { decodeEnumsCaseInsensitive = true }
+ * j.decodeFromString<Box<BadEnum>>("""{"boxed":"bad"}""")
+ * ```
+ */
+ @ExperimentalSerializationApi
+ public var decodeEnumsCaseInsensitive: Boolean = json.configuration.decodeEnumsCaseInsensitive
+
+ /**
* Module with contextual and polymorphic serializers to be used in the resulting [Json] instance.
*
* @see SerializersModule
@@ -367,7 +396,7 @@ public class JsonBuilder internal constructor(json: Json) {
allowStructuredMapKeys, prettyPrint, explicitNulls, prettyPrintIndent,
coerceInputValues, useArrayPolymorphism,
classDiscriminator, allowSpecialFloatingPointValues, useAlternativeNames,
- namingStrategy
+ namingStrategy, decodeEnumsCaseInsensitive
)
}
}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
index d17d0fcc..ea653a64 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonConfiguration.kt
@@ -9,7 +9,7 @@ import kotlinx.serialization.*
* Can be used for debug purposes and for custom Json-specific serializers
* via [JsonEncoder] and [JsonDecoder].
*
- * Standalone configuration object is meaningless and can nor be used outside of the
+ * Standalone configuration object is meaningless and can nor be used outside the
* [Json], neither new [Json] instance can be created from it.
*
* Detailed description of each property is available in [JsonBuilder] class.
@@ -31,6 +31,8 @@ public class JsonConfiguration @OptIn(ExperimentalSerializationApi::class) inter
public val useAlternativeNames: Boolean = true,
@ExperimentalSerializationApi
public val namingStrategy: JsonNamingStrategy? = null,
+ @ExperimentalSerializationApi
+ public val decodeEnumsCaseInsensitive: Boolean = false
) {
/** @suppress Dokka **/
@@ -40,6 +42,6 @@ public class JsonConfiguration @OptIn(ExperimentalSerializationApi::class) inter
"allowStructuredMapKeys=$allowStructuredMapKeys, prettyPrint=$prettyPrint, explicitNulls=$explicitNulls, " +
"prettyPrintIndent='$prettyPrintIndent', coerceInputValues=$coerceInputValues, useArrayPolymorphism=$useArrayPolymorphism, " +
"classDiscriminator='$classDiscriminator', allowSpecialFloatingPointValues=$allowSpecialFloatingPointValues, useAlternativeNames=$useAlternativeNames, " +
- "namingStrategy=$namingStrategy)"
+ "namingStrategy=$namingStrategy, decodeEnumsCaseInsensitive=$decodeEnumsCaseInsensitive)"
}
}
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt
index 060572af..64b4e0b7 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt
@@ -7,10 +7,11 @@ import kotlinx.serialization.descriptors.*
/**
* Represents naming strategy — a transformer for serial names in a [Json] format.
* Transformed serial names are used for both serialization and deserialization.
- * Actual transformation happens in the [serialNameForJson] function.
* A naming strategy is always applied globally in the Json configuration builder
* (see [JsonBuilder.namingStrategy]).
- * However, it is possible to apply additional filtering inside the transformer using the `descriptor` parameter in [serialNameForJson].
+ *
+ * Actual transformation happens in the [serialNameForJson] function.
+ * It is possible to apply additional filtering inside the transformer using the `descriptor` parameter in [serialNameForJson].
*
* Original serial names are never used after transformation, so they are ignored in a Json input.
* If the original serial name is present in the Json input but transformed is not,
@@ -21,7 +22,7 @@ import kotlinx.serialization.descriptors.*
*
* * Due to the nature of kotlinx.serialization framework, naming strategy transformation is applied to all properties regardless
* of whether their serial name was taken from the property name or provided by @[SerialName] annotation.
- * Effectively it means one cannot avoid transformation by explicitly specifying the serial name.
+ * Effectively, it means one cannot avoid transformation by explicitly specifying the serial name.
*
* * Collision of the transformed name with any other (transformed) properties serial names or any alternative names
* specified with [JsonNames] will lead to a deserialization exception.
@@ -40,7 +41,7 @@ import kotlinx.serialization.descriptors.*
* changing one without the other may introduce bugs in many unexpected ways.
* The lack of a single place of definition, the inability to use automated tools, and more error-prone code lead
* to greater maintenance efforts for code with global naming strategies.
- * However, there are cases where usage of naming strategies is inevitable, such as interop with existing API or migrating a large codebase.
+ * However, there are cases where usage of naming strategies is inevitable, such as interop with an existing API or migrating a large codebase.
* Therefore, one should carefully weigh the pros and cons before considering adding global naming strategies to an application.
*/
@ExperimentalSerializationApi
@@ -56,7 +57,7 @@ public fun interface JsonNamingStrategy {
* annotations (see [SerialDescriptor.getElementAnnotations]) or element optionality (see [SerialDescriptor.isElementOptional]).
*
* Note that invocations of this function are cached for performance reasons.
- * Caching strategy is an implementation detail and shouldn't be assumed as a part of the public API contract, as it may be changed in future releases.
+ * Caching strategy is an implementation detail and should not be assumed as a part of the public API contract, as it may be changed in future releases.
* Therefore, it is essential for this function to be pure: it should not have any side effects, and it should
* return the same String for a given [descriptor], [elementIndex], and [serialName], regardless of the number of invocations.
*/
@@ -74,7 +75,7 @@ public fun interface JsonNamingStrategy {
*
* **Transformation rules**
*
- * Words bounds are defined by uppercase characters. If there is a single uppercase char, it is transformed into lowercase one with underscore in front:
+ * Words' bounds are defined by uppercase characters. If there is a single uppercase char, it is transformed into lowercase one with underscore in front:
* `twoWords` -> `two_words`. No underscore is added if it was a beginning of the name: `MyProperty` -> `my_property`. Also, no underscore is added if it was already there:
* `camel_Case_Underscores` -> `camel_case_underscores`.
*
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 fc9cc19b..8acd8fc4 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonNamesMap.kt
@@ -17,9 +17,10 @@ internal val JsonSerializationNamesKey = DescriptorSchemaCache.Key<Array<String>
private fun SerialDescriptor.buildDeserializationNamesMap(json: Json): Map<String, Int> {
fun MutableMap<String, Int>.putOrThrow(name: String, index: Int) {
+ val entity = if (kind == SerialKind.ENUM) "enum value" else "property"
if (name in this) {
throw JsonException(
- "The suggested name '$name' for property ${getElementName(index)} is already one of the names for property " +
+ "The suggested name '$name' for $entity ${getElementName(index)} is already one of the names for $entity " +
"${getElementName(getValue(name))} in ${this@buildDeserializationNamesMap}"
)
}
@@ -28,12 +29,19 @@ private fun SerialDescriptor.buildDeserializationNamesMap(json: Json): Map<Strin
val builder: MutableMap<String, Int> =
mutableMapOf() // can be not concurrent because it is only read after creation and safely published to concurrent map
- val strategy = namingStrategy(json)
+ val useLowercaseEnums = json.decodeCaseInsensitive(this)
+ val strategyForClasses = namingStrategy(json)
for (i in 0 until elementsCount) {
getElementAnnotations(i).filterIsInstance<JsonNames>().singleOrNull()?.names?.forEach { name ->
- builder.putOrThrow(name, i)
+ builder.putOrThrow(if (useLowercaseEnums) name.lowercase() else name, i)
+ }
+ val nameToPut = when {
+ // the branches do not intersect because useLowercase = true for enums only, and strategy != null for classes only.
+ useLowercaseEnums -> getElementName(i).lowercase()
+ strategyForClasses != null -> strategyForClasses.serialNameForJson(this, i, getElementName(i))
+ else -> null
}
- strategy?.let { builder.putOrThrow(it.serialNameForJson(this, i, getElementName(i)), i) }
+ nameToPut?.let { builder.putOrThrow(it, i) }
}
return builder.ifEmpty { emptyMap() }
}
@@ -61,17 +69,24 @@ internal fun SerialDescriptor.getJsonElementName(json: Json, index: Int): String
internal fun SerialDescriptor.namingStrategy(json: Json) =
if (kind == StructureKind.CLASS) json.configuration.namingStrategy else null
+private fun SerialDescriptor.getJsonNameIndexSlowPath(json: Json, name: String): Int =
+ json.deserializationNamesMap(this)[name] ?: CompositeDecoder.UNKNOWN_NAME
+
+private fun Json.decodeCaseInsensitive(descriptor: SerialDescriptor) =
+ configuration.decodeEnumsCaseInsensitive && descriptor.kind == SerialKind.ENUM
+
/**
- * Serves same purpose as [SerialDescriptor.getElementIndex] but respects
- * [JsonNames] annotation and [JsonConfiguration.useAlternativeNames] state.
+ * Serves same purpose as [SerialDescriptor.getElementIndex] but respects [JsonNames] annotation
+ * and [JsonConfiguration] settings.
*/
@OptIn(ExperimentalSerializationApi::class)
internal fun SerialDescriptor.getJsonNameIndex(json: Json, name: String): Int {
- fun getJsonNameIndexSlowPath(): Int =
- json.deserializationNamesMap(this)[name] ?: CompositeDecoder.UNKNOWN_NAME
+ if (json.decodeCaseInsensitive(this)) {
+ return getJsonNameIndexSlowPath(json, name.lowercase())
+ }
val strategy = namingStrategy(json)
- if (strategy != null) return getJsonNameIndexSlowPath()
+ if (strategy != null) return getJsonNameIndexSlowPath(json, name)
val index = getElementIndex(name)
// Fast path, do not go through ConcurrentHashMap.get
// Note, it blocks ability to detect collisions between the primary name and alternate,
@@ -79,7 +94,7 @@ internal fun SerialDescriptor.getJsonNameIndex(json: Json, name: String): Int {
if (index != CompositeDecoder.UNKNOWN_NAME) return index
if (!json.configuration.useAlternativeNames) return index
// Slow path
- return getJsonNameIndexSlowPath()
+ return getJsonNameIndexSlowPath(json, name)
}
/**
diff --git a/guide/example/example-json-12.kt b/guide/example/example-json-12.kt
index 5a2cbbe4..1a37516b 100644
--- a/guide/example/example-json-12.kt
+++ b/guide/example/example-json-12.kt
@@ -4,12 +4,13 @@ package example.exampleJson12
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-13.kt b/guide/example/example-json-13.kt
index 86a57c82..cd7cf7f2 100644
--- a/guide/example/example-json-13.kt
+++ b/guide/example/example-json-13.kt
@@ -4,9 +4,12 @@ package example.exampleJson13
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-14.kt b/guide/example/example-json-14.kt
index 8025e58b..98464dcd 100644
--- a/guide/example/example-json-14.kt
+++ b/guide/example/example-json-14.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-15.kt b/guide/example/example-json-15.kt
index 2f2b33a4..72fd23eb 100644
--- a/guide/example/example-json-15.kt
+++ b/guide/example/example-json-15.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-16.kt b/guide/example/example-json-16.kt
index d502c864..cff8ec7d 100644
--- a/guide/example/example-json-16.kt
+++ b/guide/example/example-json-16.kt
@@ -4,14 +4,20 @@ package example.exampleJson16
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-17.kt b/guide/example/example-json-17.kt
index b51b48c5..25be7584 100644
--- a/guide/example/example-json-17.kt
+++ b/guide/example/example-json-17.kt
@@ -4,20 +4,14 @@ package example.exampleJson17
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-18.kt b/guide/example/example-json-18.kt
index fbbbffe3..2a1add45 100644
--- a/guide/example/example-json-18.kt
+++ b/guide/example/example-json-18.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-19.kt b/guide/example/example-json-19.kt
index 2ed79cb2..d59bf26b 100644
--- a/guide/example/example-json-19.kt
+++ b/guide/example/example-json-19.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-20.kt b/guide/example/example-json-20.kt
index 638ccde9..2f481daa 100644
--- a/guide/example/example-json-20.kt
+++ b/guide/example/example-json-20.kt
@@ -4,7 +4,20 @@ package example.exampleJson20
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-21.kt b/guide/example/example-json-21.kt
index 3f1b2477..86a4b734 100644
--- a/guide/example/example-json-21.kt
+++ b/guide/example/example-json-21.kt
@@ -4,29 +4,7 @@ package example.exampleJson21
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-22.kt b/guide/example/example-json-22.kt
index 58b7b261..84bd0d8a 100644
--- a/guide/example/example-json-22.kt
+++ b/guide/example/example-json-22.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-23.kt b/guide/example/example-json-23.kt
index 3b553e25..bb23f527 100644
--- a/guide/example/example-json-23.kt
+++ b/guide/example/example-json-23.kt
@@ -4,19 +4,27 @@ package example.exampleJson23
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-24.kt b/guide/example/example-json-24.kt
index 19fbbea6..def90f20 100644
--- a/guide/example/example-json-24.kt
+++ b/guide/example/example-json-24.kt
@@ -4,33 +4,19 @@ package example.exampleJson24
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-25.kt b/guide/example/example-json-25.kt
index 94c9deee..6f6d67a0 100644
--- a/guide/example/example-json-25.kt
+++ b/guide/example/example-json-25.kt
@@ -4,56 +4,33 @@ package example.exampleJson25
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", buildClassSerialDescriptor("Ok") {
- element<String>("message")
- })
- element("Error", dataSerializer.descriptor)
- }
+@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-26.kt b/guide/example/example-json-26.kt
index 9e4b5857..32b2fb65 100644
--- a/guide/example/example-json-26.kt
+++ b/guide/example/example-json-26.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", buildClassSerialDescriptor("Ok") {
+ element<String>("message")
+ })
+ element("Error", dataSerializer.descriptor)
}
- 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-27.kt b/guide/example/example-json-27.kt
new file mode 100644
index 00000000..219de6ef
--- /dev/null
+++ b/guide/example/example-json-27.kt
@@ -0,0 +1,37 @@
+// This file was automatically generated from json.md by Knit tool. Do not edit.
+package example.exampleJson27
+
+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 8a081f01..115ef778 100644
--- a/guide/test/JsonTest.kt
+++ b/guide/test/JsonTest.kt
@@ -90,45 +90,42 @@ class JsonTest {
@Test
fun testExampleJson12() {
captureOutput("ExampleJson12") { example.exampleJson12.main() }.verifyOutputLines(
- "{\"project_name\":\"kotlinx.serialization\",\"project_owner\":\"Kotlin\"}"
+ "CasesList(cases=[VALUE_A, VALUE_B])"
)
}
@Test
fun testExampleJson13() {
captureOutput("ExampleJson13") { example.exampleJson13.main() }.verifyOutputLines(
- "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}"
+ "{\"project_name\":\"kotlinx.serialization\",\"project_owner\":\"Kotlin\"}"
)
}
@Test
fun testExampleJson14() {
captureOutput("ExampleJson14") { example.exampleJson14.main() }.verifyOutputLines(
- "9042"
+ "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}"
)
}
@Test
fun testExampleJson15() {
captureOutput("ExampleJson15") { example.exampleJson15.main() }.verifyOutputLines(
- "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}"
+ "9042"
)
}
@Test
fun testExampleJson16() {
captureOutput("ExampleJson16") { example.exampleJson16.main() }.verifyOutputLines(
- "Project(name=kotlinx.serialization, language=Kotlin)"
+ "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}"
)
}
@Test
fun testExampleJson17() {
captureOutput("ExampleJson17") { example.exampleJson17.main() }.verifyOutputLines(
- "{",
- " \"pi_double\": 3.141592653589793,",
- " \"pi_string\": \"3.141592653589793238462643383279\"",
- "}"
+ "Project(name=kotlinx.serialization, language=Kotlin)"
)
}
@@ -136,7 +133,6 @@ class JsonTest {
fun testExampleJson18() {
captureOutput("ExampleJson18") { example.exampleJson18.main() }.verifyOutputLines(
"{",
- " \"pi_literal\": 3.141592653589793238462643383279,",
" \"pi_double\": 3.141592653589793,",
" \"pi_string\": \"3.141592653589793238462643383279\"",
"}"
@@ -146,59 +142,70 @@ class JsonTest {
@Test
fun testExampleJson19() {
captureOutput("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLines(
- "3.141592653589793238462643383279"
+ "{",
+ " \"pi_literal\": 3.141592653589793238462643383279,",
+ " \"pi_double\": 3.141592653589793,",
+ " \"pi_string\": \"3.141592653589793238462643383279\"",
+ "}"
)
}
@Test
fun testExampleJson20() {
- captureOutput("ExampleJson20") { example.exampleJson20.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("ExampleJson20") { example.exampleJson20.main() }.verifyOutputLines(
+ "3.141592653589793238462643383279"
)
}
@Test
fun testExampleJson21() {
- captureOutput("ExampleJson21") { example.exampleJson21.main() }.verifyOutputLines(
- "Project(name=kotlinx.serialization, users=[User(name=kotlin)])",
- "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])"
+ 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"
)
}
@Test
fun testExampleJson22() {
captureOutput("ExampleJson22") { example.exampleJson22.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 testExampleJson23() {
captureOutput("ExampleJson23") { example.exampleJson23.main() }.verifyOutputLines(
- "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}",
- "{\"name\":\"kotlinx.serialization\"}"
+ "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}"
)
}
@Test
fun testExampleJson24() {
captureOutput("ExampleJson24") { example.exampleJson24.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 testExampleJson25() {
captureOutput("ExampleJson25") { example.exampleJson25.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 testExampleJson26() {
captureOutput("ExampleJson26") { example.exampleJson26.main() }.verifyOutputLines(
+ "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]",
+ "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]"
+ )
+ }
+
+ @Test
+ fun testExampleJson27() {
+ captureOutput("ExampleJson27") { example.exampleJson27.main() }.verifyOutputLines(
"UnknownProject(name=example, details={\"type\":\"unknown\",\"maintainer\":\"Unknown\",\"license\":\"Apache 2.0\"})"
)
}