diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index 3bf89f7fe2..df682c243a 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -774,6 +774,10 @@ public final class kotlinx/serialization/internal/InlineClassDescriptor : kotlin public fun isInline ()Z } +public final class kotlinx/serialization/internal/InlineClassDescriptorKt { + public static final fun InlinePrimitiveDescriptor (Ljava/lang/String;Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/descriptors/SerialDescriptor; +} + public final class kotlinx/serialization/internal/IntArrayBuilder : kotlinx/serialization/internal/PrimitiveArrayBuilder { public synthetic fun build$kotlinx_serialization_core ()Ljava/lang/Object; } diff --git a/core/commonMain/src/kotlinx/serialization/internal/InlineClassDescriptor.kt b/core/commonMain/src/kotlinx/serialization/internal/InlineClassDescriptor.kt index b5068477ad..ec9edc9664 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/InlineClassDescriptor.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/InlineClassDescriptor.kt @@ -25,7 +25,8 @@ internal class InlineClassDescriptor( } } -internal fun InlinePrimitiveDescriptor(name: String, primitiveSerializer: KSerializer): SerialDescriptor = +@InternalSerializationApi +public fun InlinePrimitiveDescriptor(name: String, primitiveSerializer: KSerializer): SerialDescriptor = InlineClassDescriptor(name, object : GeneratedSerializer { // object needed only to pass childSerializers() override fun childSerializers(): Array> = arrayOf(primitiveSerializer) diff --git a/docs/json.md b/docs/json.md index 59e52d3e20..e2006a66fe 100644 --- a/docs/json.md +++ b/docs/json.md @@ -25,6 +25,9 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json * [Types of Json elements](#types-of-json-elements) * [Json element builders](#json-element-builders) * [Decoding Json elements](#decoding-json-elements) + * [Encoding literal Json content (experimental)](#encoding-literal-json-content-experimental) + * [Serializing large decimal numbers](#serializing-large-decimal-numbers) + * [Using `JsonUnquotedLiteral` to create a literal unquoted value of `null` is forbidden](#using-jsonunquotedliteral-to-create-a-literal-unquoted-value-of-null-is-forbidden) * [Json transformations](#json-transformations) * [Array wrapping](#array-wrapping) * [Array unwrapping](#array-unwrapping) @@ -236,7 +239,7 @@ Project(name=kotlinx.serialization, language=Kotlin) ### Encoding defaults Default values of properties are not encoded by default because they will be assigned to missing fields during decoding anyway. -See the [Defaults are not encoded](basic-serialization.md#defaults-are-not-encoded) section for details and an example. +See the [Defaults are not encoded](basic-serialization.md#defaults-are-not-encoded-by-default) section for details and an example. This is especially useful for nullable properties with null defaults and avoids writing the corresponding null values. The default behavior can be changed by setting the [encodeDefaults][JsonBuilder.encodeDefaults] property to `true`: @@ -612,6 +615,153 @@ Project(name=kotlinx.serialization, language=Kotlin) +### Encoding literal Json content (experimental) + +> This functionality is experimental and requires opting-in to [the experimental Kotlinx Serialization API](compatibility.md#experimental-api). + +In some cases it might be necessary to encode an arbitrary unquoted value. +This can be achieved with [JsonUnquotedLiteral]. + +#### Serializing large decimal numbers + +The JSON specification does not restrict the size or precision of numbers, however it is not possible to serialize +numbers of arbitrary size or precision using [JsonPrimitive()]. + +If [Double] is used, then the numbers are limited in precision, meaning that large numbers are truncated. +When using Kotlin/JVM [BigDecimal] can be used instead, but [JsonPrimitive()] will encode the value as a string, not a +number. + +```kotlin +import java.math.BigDecimal + +val format = Json { prettyPrint = true } + +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) + } + + println(format.encodeToString(piObject)) +} +``` + +> You can get the full code [here](../guide/example/example-json-16.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. + +```text +{ + "pi_double": 3.141592653589793, + "pi_string": "3.141592653589793238462643383279" +} +``` + + + +To avoid precision loss, the string value of `pi` can be encoded using [JsonUnquotedLiteral]. + +```kotlin +import java.math.BigDecimal + +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) + } + + println(format.encodeToString(piObject)) +} +``` + +> You can get the full code [here](../guide/example/example-json-17.kt). + +`pi_literal` now accurately matches the value defined. + +```text +{ + "pi_literal": 3.141592653589793238462643383279, + "pi_double": 3.141592653589793, + "pi_string": "3.141592653589793238462643383279" +} +``` + + + +To decode `pi` back to a [BigDecimal], the string content of the [JsonPrimitive] can be used. + +(This demonstration uses a [JsonPrimitive] for simplicity. For a more re-usable method of handling serialization, see +[Json Transformations](#json-transformations) below.) + + +```kotlin +import java.math.BigDecimal + +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) +} +``` + +> You can get the full code [here](../guide/example/example-json-18.kt). + +The exact value of `pi` is decoded, with all 30 decimal places of precision that were in the source JSON. + +```text +3.141592653589793238462643383279 +``` + + + +#### Using `JsonUnquotedLiteral` to create a literal unquoted value of `null` is forbidden + +To avoid creating an inconsistent state, encoding a String equal to `"null"` is forbidden. +Use [JsonNull] or [JsonPrimitive] instead. + +```kotlin +fun main() { + // caution: creating null with JsonUnquotedLiteral will cause an exception! + JsonUnquotedLiteral("null") +} +``` + +> You can get the full code [here](../guide/example/example-json-19.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 +``` + + + + ## Json transformations To affect the shape and contents of JSON output after serialization, or adapt input to deserialization, @@ -679,7 +829,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-20.kt). The output shows that both cases are correctly deserialized into a Kotlin [List]. @@ -731,7 +881,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-21.kt). You end up with a single JSON object, not an array with one element: @@ -776,7 +926,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-22.kt). See the effect of the custom serializer: @@ -849,7 +999,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-23.kt). No class discriminator is added in the JSON output: @@ -945,7 +1095,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-24.kt). This gives you fine-grained control on the representation of the `Response` class in the JSON output: @@ -1010,7 +1160,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-25.kt). ```text UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"}) @@ -1025,8 +1175,10 @@ The next chapter covers [Alternative and custom formats (experimental)](formats. [RFC-4627]: https://www.ietf.org/rfc/rfc4627.txt +[BigDecimal]: https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html +[Double]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/ [Double.NaN]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/-na-n.html [List]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/ [Map]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/ @@ -1079,6 +1231,8 @@ The next chapter covers [Alternative and custom formats (experimental)](formats. [buildJsonArray]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/build-json-array.html [buildJsonObject]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/build-json-object.html [Json.decodeFromJsonElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/decode-from-json-element.html +[JsonUnquotedLiteral]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-unquoted-literal.html +[JsonNull]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-null/index.html [JsonTransformingSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-transforming-serializer/index.html [Json.encodeToString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/encode-to-string.html [JsonContentPolymorphicSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-content-polymorphic-serializer/index.html diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index e0fe5a26a9..2c9751d557 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -123,6 +123,9 @@ Once the project is set up, we can start serializing some classes. * [Types of Json elements](json.md#types-of-json-elements) * [Json element builders](json.md#json-element-builders) * [Decoding Json elements](json.md#decoding-json-elements) + * [Encoding literal Json content (experimental)](json.md#encoding-literal-json-content-experimental) + * [Serializing large decimal numbers](json.md#serializing-large-decimal-numbers) + * [Using `JsonUnquotedLiteral` to create a literal unquoted value of `null` is forbidden](json.md#using-jsonunquotedliteral-to-create-a-literal-unquoted-value-of-null-is-forbidden) * [Json transformations](json.md#json-transformations) * [Array wrapping](json.md#array-wrapping) * [Array unwrapping](json.md#array-unwrapping) diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonPrimitiveSerializerTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonPrimitiveSerializerTest.kt index dfa7f4c7f6..6f26d2ab95 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonPrimitiveSerializerTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonPrimitiveSerializerTest.kt @@ -46,6 +46,28 @@ class JsonPrimitiveSerializerTest : JsonTestBase() { assertEquals(JsonPrimitiveWrapper(JsonPrimitive("239")), default.decodeFromString(JsonPrimitiveWrapper.serializer(), string, jsonTestingMode)) } + @Test + fun testJsonUnquotedLiteralNumbers() = parametrizedTest { jsonTestingMode -> + listOf( + "99999999999999999999999999999999999999999999999999999999999999999999999999", + "99999999999999999999999999999999999999.999999999999999999999999999999999999", + "-99999999999999999999999999999999999999999999999999999999999999999999999999", + "-99999999999999999999999999999999999999.999999999999999999999999999999999999", + "2.99792458e8", + "-2.99792458e8", + ).forEach { literalNum -> + val literalNumJson = JsonUnquotedLiteral(literalNum) + val wrapper = JsonPrimitiveWrapper(literalNumJson) + val string = default.encodeToString(JsonPrimitiveWrapper.serializer(), wrapper, jsonTestingMode) + assertEquals("{\"primitive\":$literalNum}", string, "mode:$jsonTestingMode") + assertEquals( + JsonPrimitiveWrapper(literalNumJson), + default.decodeFromString(JsonPrimitiveWrapper.serializer(), string, jsonTestingMode), + "mode:$jsonTestingMode", + ) + } + } + @Test fun testTopLevelPrimitive() = parametrizedTest { jsonTestingMode -> val string = default.encodeToString(JsonPrimitive.serializer(), JsonPrimitive(42), jsonTestingMode) @@ -76,7 +98,7 @@ class JsonPrimitiveSerializerTest : JsonTestBase() { } @Test - fun testJsonLiterals() { + fun testJsonLiterals() { testLiteral(0L, "0") testLiteral(0, "0") testLiteral(0.0, "0.0") diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt new file mode 100644 index 0000000000..e8090044cb --- /dev/null +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt @@ -0,0 +1,140 @@ +package kotlinx.serialization.json.serializers + +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.json.* +import kotlinx.serialization.test.assertFailsWithSerialMessage +import kotlin.test.Test +import kotlin.test.assertEquals + +class JsonUnquotedLiteralTest : JsonTestBase() { + + private fun assertUnquotedLiteralEncoded(inputValue: String) { + val unquotedElement = JsonUnquotedLiteral(inputValue) + + assertEquals( + inputValue, + unquotedElement.toString(), + "expect JsonElement.toString() returns the unquoted input value" + ) + + parametrizedTest { mode -> + assertEquals(inputValue, default.encodeToString(JsonElement.serializer(), unquotedElement, mode)) + } + } + + @Test + fun testUnquotedJsonNumbers() { + assertUnquotedLiteralEncoded("1") + assertUnquotedLiteralEncoded("-1") + assertUnquotedLiteralEncoded("100.0") + assertUnquotedLiteralEncoded("-100.0") + + assertUnquotedLiteralEncoded("9999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999") + assertUnquotedLiteralEncoded("-9999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999") + + assertUnquotedLiteralEncoded("99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999") + assertUnquotedLiteralEncoded("-99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999") + + assertUnquotedLiteralEncoded("2.99792458e8") + assertUnquotedLiteralEncoded("-2.99792458e8") + + assertUnquotedLiteralEncoded("2.99792458E8") + assertUnquotedLiteralEncoded("-2.99792458E8") + + assertUnquotedLiteralEncoded("11.399999999999") + assertUnquotedLiteralEncoded("0.30000000000000004") + assertUnquotedLiteralEncoded("0.1000000000000000055511151231257827021181583404541015625") + } + + @Test + fun testUnquotedJsonWhitespaceStrings() { + assertUnquotedLiteralEncoded("") + assertUnquotedLiteralEncoded(" ") + assertUnquotedLiteralEncoded("\t") + assertUnquotedLiteralEncoded("\t\t\t") + assertUnquotedLiteralEncoded("\r\n") + assertUnquotedLiteralEncoded("\n") + assertUnquotedLiteralEncoded("\n\n\n") + } + + @Test + fun testUnquotedJsonStrings() { + assertUnquotedLiteralEncoded("lorem") + assertUnquotedLiteralEncoded(""""lorem"""") + assertUnquotedLiteralEncoded( + """ + Well, my name is Freddy Kreuger + I've got the Elm Street blues + I've got a hand like a knife rack + And I die in every film! + """.trimIndent() + ) + } + + @Test + fun testUnquotedJsonObjects() { + assertUnquotedLiteralEncoded("""{"some":"json"}""") + assertUnquotedLiteralEncoded("""{"some":"json","object":true,"count":1,"array":[1,2.0,-333,"4",boolean]}""") + } + + @Test + fun testUnquotedJsonArrays() { + assertUnquotedLiteralEncoded("""[1,2,3]""") + assertUnquotedLiteralEncoded("""["a","b","c"]""") + assertUnquotedLiteralEncoded("""[true,false]""") + assertUnquotedLiteralEncoded("""[1,2.0,-333,"4",boolean]""") + assertUnquotedLiteralEncoded("""[{"some":"json","object":true,"count":1,"array":[1,2.0,-333,"4",boolean]}]""") + assertUnquotedLiteralEncoded("""[{"some":"json","object":true,"count":1,"array":[1,2.0,-333,"4",boolean]},{"some":"json","object":true,"count":1,"array":[1,2.0,-333,"4",boolean]}]""") + } + + @Test + fun testUnquotedJsonNull() { + assertEquals(JsonNull, JsonUnquotedLiteral(null)) + } + + @Test + fun testUnquotedJsonNullString() { + fun test(block: () -> Unit) { + assertFailsWithSerialMessage( + exceptionName = "JsonEncodingException", + message = "Creating a literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive", + block = block, + ) + } + + test { JsonUnquotedLiteral("null") } + test { JsonUnquotedLiteral(JsonNull.content) } + test { buildJsonObject { put("key", JsonUnquotedLiteral("null")) } } + } + + @Test + fun testUnquotedJsonInvalidMapKeyIsEscaped() { + val mapSerializer = MapSerializer( + JsonPrimitive.serializer(), + JsonPrimitive.serializer(), + ) + + fun test(expected: String, input: String) = parametrizedTest { mode -> + val data = mapOf(JsonUnquotedLiteral(input) to JsonPrimitive("invalid key")) + + assertEquals( + """ {"$expected":"invalid key"} """.trim(), + default.encodeToString(mapSerializer, data, mode), + ) + } + + test(" ", " ") + test( + """ \\\"\\\" """.trim(), + """ \"\" """.trim(), + ) + test( + """ \\\\\\\" """.trim(), + """ \\\" """.trim(), + ) + test( + """ {\\\"I'm not a valid JSON object key\\\"} """.trim(), + """ {\"I'm not a valid JSON object key\"} """.trim(), + ) + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/BigDecimalTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/BigDecimalTest.kt new file mode 100644 index 0000000000..a0c4c73acb --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/BigDecimalTest.kt @@ -0,0 +1,193 @@ +package kotlinx.serialization + +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.* +import org.junit.Test +import java.math.BigDecimal + +private typealias BigDecimalKxs = @Serializable(with = BigDecimalNumericSerializer::class) BigDecimal + +class BigDecimalTest : JsonTestBase() { + + private val json = Json { + prettyPrint = true + } + + private inline fun assertBigDecimalJsonFormAndRestored( + expected: String, + actual: T, + serializer: KSerializer = serializer(), + ) = assertJsonFormAndRestored( + serializer, + actual, + expected, + json + ) + + @Test + fun bigDecimal() { + fun test(expected: String, actual: BigDecimal) = + assertBigDecimalJsonFormAndRestored(expected, actual, BigDecimalNumericSerializer) + + test("0", BigDecimal.ZERO) + test("1", BigDecimal.ONE) + test("-1", BigDecimal("-1")) + test("10", BigDecimal.TEN) + test(bdExpected1, bdActual1) + test(bdExpected2, bdActual2) + test(bdExpected3, bdActual3) + test(bdExpected4, bdActual4) + test(bdExpected5, bdActual5) + test(bdExpected6, bdActual6) + } + + @Test + fun bigDecimalList() { + + val bdList: List = listOf( + bdActual1, + bdActual2, + bdActual3, + bdActual4, + bdActual5, + bdActual6, + ) + + val expected = + """ + [ + $bdExpected1, + $bdExpected2, + $bdExpected3, + $bdExpected4, + $bdExpected5, + $bdExpected6 + ] + """.trimIndent() + + assertJsonFormAndRestored( + ListSerializer(BigDecimalNumericSerializer), + bdList, + expected, + json, + ) + } + + @Test + fun bigDecimalMap() { + val bdMap: Map = mapOf( + bdActual1 to bdActual2, + bdActual3 to bdActual4, + bdActual5 to bdActual6, + ) + + val expected = + """ + { + "$bdExpected1": $bdExpected2, + "$bdExpected3": $bdExpected4, + "$bdExpected5": $bdExpected6 + } + """.trimIndent() + + assertJsonFormAndRestored( + MapSerializer(BigDecimalNumericSerializer, BigDecimalNumericSerializer), + bdMap, + expected, + json, + ) + } + + @Test + fun bigDecimalHolder() { + val bdHolder = BigDecimalHolder( + bd = bdActual1, + bdList = listOf( + bdActual1, + bdActual2, + bdActual3, + ), + bdMap = mapOf( + bdActual1 to bdActual2, + bdActual3 to bdActual4, + bdActual5 to bdActual6, + ), + ) + + val expected = + """ + { + "bd": $bdExpected1, + "bdList": [ + $bdExpected1, + $bdExpected2, + $bdExpected3 + ], + "bdMap": { + "$bdExpected1": $bdExpected2, + "$bdExpected3": $bdExpected4, + "$bdExpected5": $bdExpected6 + } + } + """.trimIndent() + + assertBigDecimalJsonFormAndRestored( + expected, + bdHolder, + ) + } + + companion object { + + // test data + private val bdActual1 = BigDecimal("725345854747326287606413621318.311864440287151714280387858224") + private val bdActual2 = BigDecimal("336052472523017262165484244513.836582112201211216526831524328") + private val bdActual3 = BigDecimal("211054843014778386028147282517.011200287614476453868782405400") + private val bdActual4 = BigDecimal("364751025728628060231208776573.207325218263752602211531367642") + private val bdActual5 = BigDecimal("508257556021513833656664177125.824502734715222686411316853148") + private val bdActual6 = BigDecimal("127134584027580606401102614002.366672301517071543257300444000") + + private const val bdExpected1 = "725345854747326287606413621318.311864440287151714280387858224" + private const val bdExpected2 = "336052472523017262165484244513.836582112201211216526831524328" + private const val bdExpected3 = "211054843014778386028147282517.011200287614476453868782405400" + private const val bdExpected4 = "364751025728628060231208776573.207325218263752602211531367642" + private const val bdExpected5 = "508257556021513833656664177125.824502734715222686411316853148" + private const val bdExpected6 = "127134584027580606401102614002.366672301517071543257300444000" + } + +} + +@Serializable +private data class BigDecimalHolder( + val bd: BigDecimalKxs, + val bdList: List, + val bdMap: Map, +) + +private object BigDecimalNumericSerializer : KSerializer { + + override val descriptor = PrimitiveSerialDescriptor("java.math.BigDecimal", PrimitiveKind.DOUBLE) + + override fun deserialize(decoder: Decoder): BigDecimal { + return if (decoder is JsonDecoder) { + BigDecimal(decoder.decodeJsonElement().jsonPrimitive.content) + } else { + BigDecimal(decoder.decodeString()) + } + } + + override fun serialize(encoder: Encoder, value: BigDecimal) { + val bdString = value.toPlainString() + + if (encoder is JsonEncoder) { + encoder.encodeJsonElement(JsonUnquotedLiteral(bdString)) + } else { + encoder.encodeString(bdString) + } + } +} diff --git a/formats/json/api/kotlinx-serialization-json.api b/formats/json/api/kotlinx-serialization-json.api index 7f2f69b367..24aaf10f84 100644 --- a/formats/json/api/kotlinx-serialization-json.api +++ b/formats/json/api/kotlinx-serialization-json.api @@ -187,6 +187,7 @@ public final class kotlinx/serialization/json/JsonElementKt { public static final fun JsonPrimitive (Ljava/lang/Number;)Lkotlinx/serialization/json/JsonPrimitive; public static final fun JsonPrimitive (Ljava/lang/String;)Lkotlinx/serialization/json/JsonPrimitive; public static final fun JsonPrimitive (Ljava/lang/Void;)Lkotlinx/serialization/json/JsonNull; + public static final fun JsonUnquotedLiteral (Ljava/lang/String;)Lkotlinx/serialization/json/JsonPrimitive; public static final fun getBoolean (Lkotlinx/serialization/json/JsonPrimitive;)Z public static final fun getBooleanOrNull (Lkotlinx/serialization/json/JsonPrimitive;)Ljava/lang/Boolean; public static final fun getContentOrNull (Lkotlinx/serialization/json/JsonPrimitive;)Ljava/lang/String; diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index abfc567a17..634c447949 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -7,7 +7,11 @@ package kotlinx.serialization.json import kotlinx.serialization.* +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.internal.InlinePrimitiveDescriptor import kotlinx.serialization.json.internal.* +import kotlin.native.concurrent.SharedImmutable /** * Class representing single JSON element. @@ -76,13 +80,52 @@ public fun JsonPrimitive(value: String?): JsonPrimitive { @Suppress("FunctionName", "UNUSED_PARAMETER") // allows to call `JsonPrimitive(null)` public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull +/** + * Creates a [JsonPrimitive] from the given string, without surrounding it in quotes. + * + * This function is provided for encoding raw JSON values that cannot be encoded using the [JsonPrimitive] functions. + * For example, + * + * * precise numeric values (avoiding floating-point precision errors associated with [Double] and [Float]), + * * large numbers, + * * or complex JSON objects. + * + * Be aware that it is possible to create invalid JSON using this function. + * + * Creating a literal unquoted value of `null` (as in, `value == "null"`) is forbidden. If you want to create + * JSON null literal, use [JsonNull] object, otherwise, use [JsonPrimitive]. + * + * @see JsonPrimitive is the preferred method for encoding JSON primitives. + * @throws JsonEncodingException if `value == "null"` + */ +@ExperimentalSerializationApi +@Suppress("FunctionName") +public fun JsonUnquotedLiteral(value: String?): JsonPrimitive { + return when (value) { + null -> JsonNull + JsonNull.content -> throw JsonEncodingException("Creating a literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive") + else -> JsonLiteral(value, isString = false, coerceToInlineType = jsonUnquotedLiteralDescriptor) + } +} + +/** Used as a marker to indicate during encoding that the [JsonEncoder] should use `encodeInline()` */ +@SharedImmutable +internal val jsonUnquotedLiteralDescriptor: SerialDescriptor = + InlinePrimitiveDescriptor("kotlinx.serialization.json.JsonUnquotedLiteral", String.serializer()) + + // JsonLiteral is deprecated for public use and no longer available. Please use JsonPrimitive instead internal class JsonLiteral internal constructor( body: Any, - public override val isString: Boolean + public override val isString: Boolean, + internal val coerceToInlineType: SerialDescriptor? = null, ) : JsonPrimitive() { public override val content: String = body.toString() + init { + if (coerceToInlineType != null) require(coerceToInlineType.isInline) + } + public override fun toString(): String = if (isString) buildString { printQuoted(content) } else content @@ -121,7 +164,9 @@ public object JsonNull : JsonPrimitive() { * traditional methods like [Map.get] or [Map.getValue] to obtain Json elements. */ @Serializable(JsonObjectSerializer::class) -public class JsonObject(private val content: Map) : JsonElement(), Map by content { +public class JsonObject( + private val content: Map +) : JsonElement(), Map by content { public override fun equals(other: Any?): Boolean = content == other public override fun hashCode(): Int = content.hashCode() public override fun toString(): String { @@ -229,7 +274,8 @@ public val JsonPrimitive.floatOrNull: Float? get() = content.toFloatOrNull() * Returns content of current element as boolean * @throws IllegalStateException if current element doesn't represent boolean */ -public val JsonPrimitive.boolean: Boolean get() = content.toBooleanStrictOrNull() ?: throw IllegalStateException("$this does not represent a Boolean") +public val JsonPrimitive.boolean: Boolean + get() = content.toBooleanStrictOrNull() ?: throw IllegalStateException("$this does not represent a Boolean") /** * Returns content of current element as boolean or `null` if current element is not a valid representation of boolean diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt index 6fcfa2c0a0..788ce93f9a 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt @@ -116,6 +116,10 @@ private object JsonLiteralSerializer : KSerializer { return encoder.encodeString(value.content) } + if (value.coerceToInlineType != null) { + return encoder.encodeInline(value.coerceToInlineType).encodeString(value.content) + } + value.longOrNull?.let { return encoder.encodeLong(it) } // most unsigned values fit to .longOrNull, but not ULong diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt index 113a829610..d5841552c4 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt @@ -38,7 +38,7 @@ internal open class Composer(@JvmField internal val writer: JsonWriter) { open fun print(v: Int) = writer.writeLong(v.toLong()) open fun print(v: Long) = writer.writeLong(v) open fun print(v: Boolean) = writer.write(v.toString()) - fun printQuoted(value: String) = writer.writeQuoted(value) + open fun printQuoted(value: String) = writer.writeQuoted(value) } @SuppressAnimalSniffer // Long(Integer).toUnsignedString(long) @@ -60,6 +60,13 @@ internal class ComposerForUnsignedNumbers(writer: JsonWriter, private val forceQ } } +@SuppressAnimalSniffer +internal class ComposerForUnquotedLiterals(writer: JsonWriter, private val forceQuoting: Boolean) : Composer(writer) { + override fun printQuoted(value: String) { + if (forceQuoting) super.printQuoted(value) else super.print(value) + } +} + internal class ComposerWithPrettyPrint( writer: JsonWriter, private val json: Json diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt index dd7682fe20..bc954ce9c7 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt @@ -23,6 +23,9 @@ private val unsignedNumberDescriptors = setOf( internal val SerialDescriptor.isUnsignedNumber: Boolean get() = this.isInline && this in unsignedNumberDescriptors +internal val SerialDescriptor.isUnquotedLiteral: Boolean + get() = this.isInline && this == jsonUnquotedLiteralDescriptor + @OptIn(ExperimentalSerializationApi::class) internal class StreamingJsonEncoder( private val composer: Composer, @@ -156,17 +159,18 @@ internal class StreamingJsonEncoder( } override fun encodeInline(descriptor: SerialDescriptor): Encoder = - if (descriptor.isUnsignedNumber) StreamingJsonEncoder( - composerForUnsignedNumbers(), json, mode, null - ) - else super.encodeInline(descriptor) + when { + descriptor.isUnsignedNumber -> StreamingJsonEncoder(composerAs(::ComposerForUnsignedNumbers), json, mode, null) + descriptor.isUnquotedLiteral -> StreamingJsonEncoder(composerAs(::ComposerForUnquotedLiterals), json, mode, null) + else -> super.encodeInline(descriptor) + } - private fun composerForUnsignedNumbers(): Composer { + private inline fun composerAs(composerCreator: (writer: JsonWriter, forceQuoting: Boolean) -> T): T { // If we're inside encodeInline().encodeSerializableValue, we should preserve the forceQuoting state // inside the composer, but not in the encoder (otherwise we'll get into `if (forceQuoting) encodeString(value.toString())` part // and unsigned numbers would be encoded incorrectly) - return if (composer is ComposerForUnsignedNumbers) composer - else ComposerForUnsignedNumbers(composer.writer, forceQuoting) + return if (composer is T) composer + else composerCreator(composer.writer, forceQuoting) } override fun encodeNull() { diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt index b33c88ee9e..643e158e15 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt @@ -102,9 +102,15 @@ private sealed class AbstractJsonTreeEncoder( putElement(tag, JsonPrimitive(value.toString())) } - @SuppressAnimalSniffer // Long(Integer).toUnsignedString(long) override fun encodeTaggedInline(tag: String, inlineDescriptor: SerialDescriptor): Encoder = - if (inlineDescriptor.isUnsignedNumber) object : AbstractEncoder() { + when { + inlineDescriptor.isUnsignedNumber -> inlineUnsignedNumberEncoder(tag) + inlineDescriptor.isUnquotedLiteral -> inlineUnquotedLiteralEncoder(tag, inlineDescriptor) + else -> super.encodeTaggedInline(tag, inlineDescriptor) + } + + @SuppressAnimalSniffer // Long(Integer).toUnsignedString(long) + private fun inlineUnsignedNumberEncoder(tag: String) = object : AbstractEncoder() { override val serializersModule: SerializersModule = json.serializersModule fun putUnquotedString(s: String) = putElement(tag, JsonLiteral(s, isString = false)) @@ -113,7 +119,12 @@ private sealed class AbstractJsonTreeEncoder( override fun encodeByte(value: Byte) = putUnquotedString(value.toUByte().toString()) override fun encodeShort(value: Short) = putUnquotedString(value.toUShort().toString()) } - else super.encodeTaggedInline(tag, inlineDescriptor) + + private fun inlineUnquotedLiteralEncoder(tag: String, inlineDescriptor: SerialDescriptor) = object : AbstractEncoder() { + override val serializersModule: SerializersModule get() = json.serializersModule + + override fun encodeString(value: String) = putElement(tag, JsonLiteral(value, isString = false, coerceToInlineType = inlineDescriptor)) + } override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { val consumer = diff --git a/gradle.properties b/gradle.properties index 070e739165..f1ea1fb117 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ jackson_version=2.10.0.pr1 dokka_version=1.7.0 native.deploy= validator_version=0.11.0 -knit_version=0.3.0 +knit_version=0.4.0 coroutines_version=1.3.9 kover_version=0.4.2 okio_version=3.1.0 diff --git a/guide/example/example-json-16.kt b/guide/example/example-json-16.kt index b66d3ac20d..46c8b3f5ee 100644 --- a/guide/example/example-json-16.kt +++ b/guide/example/example-json-16.kt @@ -4,29 +4,20 @@ package example.exampleJson16 import kotlinx.serialization.* import kotlinx.serialization.json.* -import kotlinx.serialization.builtins.* +import java.math.BigDecimal -@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 -} +val format = Json { prettyPrint = true } fun main() { - println(Json.decodeFromString(""" - {"name":"kotlinx.serialization","users":{"name":"kotlin"}} - """)) - println(Json.decodeFromString(""" - {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]} - """)) + 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) + } + + println(format.encodeToString(piObject)) } diff --git a/guide/example/example-json-17.kt b/guide/example/example-json-17.kt index 7b1b88f341..c41bf1e9c7 100644 --- a/guide/example/example-json-17.kt +++ b/guide/example/example-json-17.kt @@ -4,27 +4,24 @@ package example.exampleJson17 import kotlinx.serialization.* import kotlinx.serialization.json.* -import kotlinx.serialization.builtins.* +import java.math.BigDecimal -@Serializable -data class Project( - val name: String, - @Serializable(with = UserListSerializer::class) - val users: List -) +val format = Json { prettyPrint = true } -@Serializable -data class User(val name: String) +fun main() { + val pi = BigDecimal("3.141592653589793238462643383279") -object UserListSerializer : JsonTransformingSerializer>(ListSerializer(User.serializer())) { + // use JsonUnquotedLiteral to encode raw JSON content + val piJsonLiteral = JsonUnquotedLiteral(pi.toString()) - override fun transformSerialize(element: JsonElement): JsonElement { - require(element is JsonArray) // this serializer is used only with lists - return element.singleOrNull() ?: element + 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) } -} -fun main() { - val data = Project("kotlinx.serialization", listOf(User("kotlin"))) - println(Json.encodeToString(data)) + println(format.encodeToString(piObject)) } diff --git a/guide/example/example-json-18.kt b/guide/example/example-json-18.kt index d3da62d3c8..471d320933 100644 --- a/guide/example/example-json-18.kt +++ b/guide/example/example-json-18.kt @@ -4,19 +4,20 @@ package example.exampleJson18 import kotlinx.serialization.* import kotlinx.serialization.json.* -@Serializable -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" - }) -} +import java.math.BigDecimal 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 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-19.kt b/guide/example/example-json-19.kt index 4455d63723..4fd0e2924b 100644 --- a/guide/example/example-json-19.kt +++ b/guide/example/example-json-19.kt @@ -4,33 +4,7 @@ package example.exampleJson19 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() - } -} - 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)) + // caution: creating null with JsonUnquotedLiteral will cause an exception! + JsonUnquotedLiteral("null") } diff --git a/guide/example/example-json-20.kt b/guide/example/example-json-20.kt index e613a08f2a..949a25811d 100644 --- a/guide/example/example-json-20.kt +++ b/guide/example/example-json-20.kt @@ -4,56 +4,29 @@ package example.exampleJson20 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() -} - -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 Project( + val name: String, + @Serializable(with = UserListSerializer::class) + val users: List +) - 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)) - } +@Serializable +data class User(val name: String) - 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) - } +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 } -@Serializable -data class Project(val name: String) - fun main() { - val responses = listOf( - Response.Ok(Project("kotlinx.serialization")), - Response.Error("Not found") - ) - val string = Json.encodeToString(responses) - println(string) - println(Json.decodeFromString>>(string)) + 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-21.kt b/guide/example/example-json-21.kt index 92de429b17..1d25360073 100644 --- a/guide/example/example-json-21.kt +++ b/guide/example/example-json-21.kt @@ -4,34 +4,27 @@ package example.exampleJson21 import kotlinx.serialization.* import kotlinx.serialization.json.* -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* +import kotlinx.serialization.builtins.* -data class UnknownProject(val name: String, val details: JsonObject) +@Serializable +data class Project( + val name: String, + @Serializable(with = UserListSerializer::class) + val users: List +) -object UnknownProjectSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") { - element("name") - element("details") - } +@Serializable +data class User(val name: String) - 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)) - } +object UserListSerializer : JsonTransformingSerializer>(ListSerializer(User.serializer())) { - override fun serialize(encoder: Encoder, value: UnknownProject) { - error("Serialization is not supported") + override fun transformSerialize(element: JsonElement): JsonElement { + require(element is JsonArray) // this serializer is used only with lists + return element.singleOrNull() ?: element } } fun main() { - println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}""")) + val data = Project("kotlinx.serialization", listOf(User("kotlin"))) + println(Json.encodeToString(data)) } diff --git a/guide/example/example-json-22.kt b/guide/example/example-json-22.kt new file mode 100644 index 0000000000..b987c27309 --- /dev/null +++ b/guide/example/example-json-22.kt @@ -0,0 +1,22 @@ +// This file was automatically generated from json.md by Knit tool. Do not edit. +package example.exampleJson22 + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +@Serializable +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 = 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-23.kt b/guide/example/example-json-23.kt new file mode 100644 index 0000000000..06570cafec --- /dev/null +++ b/guide/example/example-json-23.kt @@ -0,0 +1,36 @@ +// This file was automatically generated from json.md by Knit tool. Do not edit. +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() + } +} + +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)) +} diff --git a/guide/example/example-json-24.kt b/guide/example/example-json-24.kt new file mode 100644 index 0000000000..02a05ee6c1 --- /dev/null +++ b/guide/example/example-json-24.kt @@ -0,0 +1,59 @@ +// This file was automatically generated from json.md by Knit tool. Do not edit. +package example.exampleJson24 + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* + +@Serializable(with = ResponseSerializer::class) +sealed class Response { + data class Ok(val data: T) : Response() + data class Error(val message: String) : Response() +} + +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): 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 Project(val name: String) + +fun main() { + 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-25.kt b/guide/example/example-json-25.kt new file mode 100644 index 0000000000..2b078bfe11 --- /dev/null +++ b/guide/example/example-json-25.kt @@ -0,0 +1,37 @@ +// This file was automatically generated from json.md by Knit tool. Do not edit. +package example.exampleJson25 + +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 c92f57bf61..a38539e8d3 100644 --- a/guide/test/JsonTest.kt +++ b/guide/test/JsonTest.kt @@ -118,45 +118,80 @@ class JsonTest { @Test fun testExampleJson16() { captureOutput("ExampleJson16") { example.exampleJson16.main() }.verifyOutputLines( - "Project(name=kotlinx.serialization, users=[User(name=kotlin)])", - "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])" + "{", + " \"pi_double\": 3.141592653589793,", + " \"pi_string\": \"3.141592653589793238462643383279\"", + "}" ) } @Test fun testExampleJson17() { captureOutput("ExampleJson17") { example.exampleJson17.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}" + "{", + " \"pi_literal\": 3.141592653589793238462643383279,", + " \"pi_double\": 3.141592653589793,", + " \"pi_string\": \"3.141592653589793238462643383279\"", + "}" ) } @Test fun testExampleJson18() { captureOutput("ExampleJson18") { example.exampleJson18.main() }.verifyOutputLines( + "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" + ) + } + + @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)])" + ) + } + + @Test + fun testExampleJson21() { + captureOutput("ExampleJson21") { example.exampleJson21.main() }.verifyOutputLines( + "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}" + ) + } + + @Test + fun testExampleJson22() { + captureOutput("ExampleJson22") { example.exampleJson22.main() }.verifyOutputLines( "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}", "{\"name\":\"kotlinx.serialization\"}" ) } @Test - fun testExampleJson19() { - captureOutput("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLines( + fun testExampleJson23() { + captureOutput("ExampleJson23") { example.exampleJson23.main() }.verifyOutputLines( "[{\"name\":\"kotlinx.serialization\",\"owner\":\"kotlin\"},{\"name\":\"example\"}]", "[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]" ) } @Test - fun testExampleJson20() { - captureOutput("ExampleJson20") { example.exampleJson20.main() }.verifyOutputLines( + fun testExampleJson24() { + captureOutput("ExampleJson24") { example.exampleJson24.main() }.verifyOutputLines( "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]", "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]" ) } @Test - fun testExampleJson21() { - captureOutput("ExampleJson21") { example.exampleJson21.main() }.verifyOutputLines( + fun testExampleJson25() { + captureOutput("ExampleJson25") { example.exampleJson25.main() }.verifyOutputLines( "UnknownProject(name=example, details={\"type\":\"unknown\",\"maintainer\":\"Unknown\",\"license\":\"Apache 2.0\"})" ) }