diff --git a/docs/json.md b/docs/json.md index e432b14e41..9fd8cac4dc 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) + * [Global naming strategy](#global-naming-strategy) * [Json elements](#json-elements) * [Parsing to Json element](#parsing-to-json-element) * [Types of Json elements](#types-of-json-elements) @@ -468,6 +469,53 @@ As you can see, discriminator from the `Base` class is used: +### Global naming strategy + +If properties' names in Json input are different from Kotlin ones, it is recommended to specify the name +for each property explicitly using [`@SerialName` annotation](basic-serialization.md#serial-field-names). +However, there are certain situations where transformation should be applied to every serial name — such as migration +from other frameworks or legacy codebase. For that cases, it is possible to specify a [namingStrategy][JsonBuilder.namingStrategy] +for a [Json] instance. `kotlinx.serialization` provides one strategy implementation out of the box, the [JsonNamingStrategy.SnakeCase](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-naming-strategy/-builtins/-snake-case.html): + +```kotlin +@Serializable +data class Project(val projectName: String, val projectOwner: String) + +val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase } + +fun main() { + val project = format.decodeFromString("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""") + println(format.encodeToString(project.copy(projectName = "kotlinx.serialization"))) +} +``` + +> You can get the full code [here](../guide/example/example-json-12.kt). + +As you can see, both serialization and deserialization work as if all serial names are transformed from camel case to snake case: + +```text +{"project_name":"kotlinx.serialization","project_owner":"Kotlin"} +``` + +There are some caveats one should remember while dealing with a [JsonNamingStrategy]: + +* Due to the nature of the `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. To be able to deserialize +non-transformed names, [JsonNames] annotation can be used instead. + +* 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. + +* Global naming strategies are very implicit: by looking only at the definition of the class, +it is impossible to determine which names it will have in the serialized form. +As a consequence, naming strategies are not friendly to actions like Find Usages/Rename in IDE, full-text search by grep, etc. +For them, the original name and the transformed are two different things; +changing one without the other may introduce bugs in many unexpected ways and lead to greater maintenance efforts for code with global naming strategies. + +Therefore, one should carefully weigh the pros and cons before considering adding global naming strategies to an application. + + ## Json elements @@ -493,7 +541,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). A `JsonElement` prints itself as a valid JSON: @@ -536,7 +584,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). The above example sums `votes` in all objects in the `forks` array, ignoring the objects that have no `votes`: @@ -576,7 +624,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 a result, you get a proper JSON string: @@ -605,7 +653,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-15.kt). +> You can get the full code [here](../guide/example/example-json-16.kt). The result is exactly what you would expect: @@ -651,7 +699,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). 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. @@ -691,7 +739,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). `pi_literal` now accurately matches the value defined. @@ -731,7 +779,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). The exact value of `pi` is decoded, with all 30 decimal places of precision that were in the source JSON. @@ -753,7 +801,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). ```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 @@ -829,7 +877,7 @@ fun main() { } ``` -> You can get the full code [here](../guide/example/example-json-20.kt). +> You can get the full code [here](../guide/example/example-json-21.kt). The output shows that both cases are correctly deserialized into a Kotlin [List]. @@ -881,7 +929,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). You end up with a single JSON object, not an array with one element: @@ -926,7 +974,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). See the effect of the custom serializer: @@ -999,7 +1047,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). No class discriminator is added in the JSON output: @@ -1095,7 +1143,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). This gives you fine-grained control on the representation of the `Response` class in the JSON output: @@ -1160,7 +1208,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). ```text UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"}) @@ -1214,6 +1262,8 @@ 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.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 [Json.parseToJsonElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/parse-to-json-element.html [JsonPrimitive]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-primitive/index.html diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index a597c74856..7048206a6e 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. * [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) + * [Global naming strategy](json.md#global-naming-strategy) * [Json elements](json.md#json-elements) * [Parsing to Json element](json.md#parsing-to-json-element) * [Types of Json elements](json.md#types-of-json-elements) diff --git a/guide/example/example-json-12.kt b/guide/example/example-json-12.kt index cc98bf5cae..5a2cbbe496 100644 --- a/guide/example/example-json-12.kt +++ b/guide/example/example-json-12.kt @@ -4,9 +4,12 @@ package example.exampleJson12 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_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""") + println(format.encodeToString(project.copy(projectName = "kotlinx.serialization"))) } diff --git a/guide/example/example-json-13.kt b/guide/example/example-json-13.kt index 97188ff59a..86a57c82b9 100644 --- a/guide/example/example-json-13.kt +++ b/guide/example/example-json-13.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-14.kt b/guide/example/example-json-14.kt index 0e5ba36221..8025e58b32 100644 --- a/guide/example/example-json-14.kt +++ b/guide/example/example-json-14.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-15.kt b/guide/example/example-json-15.kt index 0aa317f4da..2f2b33a428 100644 --- a/guide/example/example-json-15.kt +++ b/guide/example/example-json-15.kt @@ -4,14 +4,20 @@ package example.exampleJson15 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-16.kt b/guide/example/example-json-16.kt index 46c8b3f5ee..d502c86489 100644 --- a/guide/example/example-json-16.kt +++ b/guide/example/example-json-16.kt @@ -4,20 +4,14 @@ package example.exampleJson16 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-17.kt b/guide/example/example-json-17.kt index c41bf1e9c7..b51b48c5f5 100644 --- a/guide/example/example-json-17.kt +++ b/guide/example/example-json-17.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-18.kt b/guide/example/example-json-18.kt index 471d320933..fbbbffe35f 100644 --- a/guide/example/example-json-18.kt +++ b/guide/example/example-json-18.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-19.kt b/guide/example/example-json-19.kt index 4fd0e2924b..2ed79cb2ce 100644 --- a/guide/example/example-json-19.kt +++ b/guide/example/example-json-19.kt @@ -4,7 +4,20 @@ package example.exampleJson19 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-20.kt b/guide/example/example-json-20.kt index 949a25811d..638ccde9c6 100644 --- a/guide/example/example-json-20.kt +++ b/guide/example/example-json-20.kt @@ -4,29 +4,7 @@ package example.exampleJson20 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-21.kt b/guide/example/example-json-21.kt index 1d25360073..3f1b2477bc 100644 --- a/guide/example/example-json-21.kt +++ b/guide/example/example-json-21.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-22.kt b/guide/example/example-json-22.kt index b987c27309..58b7b26109 100644 --- a/guide/example/example-json-22.kt +++ b/guide/example/example-json-22.kt @@ -4,19 +4,27 @@ package example.exampleJson22 import kotlinx.serialization.* import kotlinx.serialization.json.* +import kotlinx.serialization.builtins.* + +@Serializable +data class Project( + val name: String, + @Serializable(with = UserListSerializer::class) + val users: List +) + @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-23.kt b/guide/example/example-json-23.kt index 06570cafec..3b553e25bd 100644 --- a/guide/example/example-json-23.kt +++ b/guide/example/example-json-23.kt @@ -4,33 +4,19 @@ package example.exampleJson23 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-24.kt b/guide/example/example-json-24.kt index 02a05ee6c1..19fbbea6fe 100644 --- a/guide/example/example-json-24.kt +++ b/guide/example/example-json-24.kt @@ -4,56 +4,33 @@ package example.exampleJson24 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", buildClassSerialDescriptor("Ok") { - element("message") - }) - element("Error", dataSerializer.descriptor) - } +@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-25.kt b/guide/example/example-json-25.kt index 2b078bfe11..94c9deeed2 100644 --- a/guide/example/example-json-25.kt +++ b/guide/example/example-json-25.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", buildClassSerialDescriptor("Ok") { + element("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 { + // 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-26.kt b/guide/example/example-json-26.kt new file mode 100644 index 0000000000..9e4b585710 --- /dev/null +++ b/guide/example/example-json-26.kt @@ -0,0 +1,37 @@ +// This file was automatically generated from json.md by Knit tool. Do not edit. +package example.exampleJson26 + +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 a38539e8d3..8a081f01ea 100644 --- a/guide/test/JsonTest.kt +++ b/guide/test/JsonTest.kt @@ -90,38 +90,35 @@ class JsonTest { @Test fun testExampleJson12() { captureOutput("ExampleJson12") { example.exampleJson12.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}" + "{\"project_name\":\"kotlinx.serialization\",\"project_owner\":\"Kotlin\"}" ) } @Test fun testExampleJson13() { captureOutput("ExampleJson13") { example.exampleJson13.main() }.verifyOutputLines( - "9042" + "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}" ) } @Test fun testExampleJson14() { captureOutput("ExampleJson14") { example.exampleJson14.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}" + "9042" ) } @Test fun testExampleJson15() { captureOutput("ExampleJson15") { example.exampleJson15.main() }.verifyOutputLines( - "Project(name=kotlinx.serialization, language=Kotlin)" + "{\"name\":\"kotlinx.serialization\",\"owner\":{\"name\":\"kotlin\"},\"forks\":[{\"votes\":42},{\"votes\":9000}]}" ) } @Test fun testExampleJson16() { captureOutput("ExampleJson16") { example.exampleJson16.main() }.verifyOutputLines( - "{", - " \"pi_double\": 3.141592653589793,", - " \"pi_string\": \"3.141592653589793238462643383279\"", - "}" + "Project(name=kotlinx.serialization, language=Kotlin)" ) } @@ -129,7 +126,6 @@ class JsonTest { fun testExampleJson17() { captureOutput("ExampleJson17") { example.exampleJson17.main() }.verifyOutputLines( "{", - " \"pi_literal\": 3.141592653589793238462643383279,", " \"pi_double\": 3.141592653589793,", " \"pi_string\": \"3.141592653589793238462643383279\"", "}" @@ -139,59 +135,70 @@ class JsonTest { @Test fun testExampleJson18() { captureOutput("ExampleJson18") { example.exampleJson18.main() }.verifyOutputLines( - "3.141592653589793238462643383279" + "{", + " \"pi_literal\": 3.141592653589793238462643383279,", + " \"pi_double\": 3.141592653589793,", + " \"pi_string\": \"3.141592653589793238462643383279\"", + "}" ) } @Test fun testExampleJson19() { - captureOutput("ExampleJson19") { example.exampleJson19.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("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLines( + "3.141592653589793238462643383279" ) } @Test fun testExampleJson20() { - captureOutput("ExampleJson20") { example.exampleJson20.main() }.verifyOutputLines( - "Project(name=kotlinx.serialization, users=[User(name=kotlin)])", - "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])" + 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" ) } @Test fun testExampleJson21() { captureOutput("ExampleJson21") { example.exampleJson21.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 testExampleJson22() { captureOutput("ExampleJson22") { example.exampleJson22.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}", - "{\"name\":\"kotlinx.serialization\"}" + "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}" ) } @Test fun testExampleJson23() { captureOutput("ExampleJson23") { example.exampleJson23.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 testExampleJson24() { captureOutput("ExampleJson24") { example.exampleJson24.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 testExampleJson25() { captureOutput("ExampleJson25") { example.exampleJson25.main() }.verifyOutputLines( + "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]", + "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]" + ) + } + + @Test + fun testExampleJson26() { + captureOutput("ExampleJson26") { example.exampleJson26.main() }.verifyOutputLines( "UnknownProject(name=example, details={\"type\":\"unknown\",\"maintainer\":\"Unknown\",\"license\":\"Apache 2.0\"})" ) }