From ff8fd996207be2aa2a859e1702b63e6be4008049 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 13 Oct 2022 16:32:37 +0200 Subject: [PATCH 01/28] implement JsonRawElement, for encoding literal JSON strings --- core/api/kotlinx-serialization-core.api | 4 + .../internal/InlineClassDescriptor.kt | 3 +- .../JsonPrimitiveSerializerTest.kt | 22 ++ .../json/serializers/JsonRawElementTest.kt | 132 ++++++++++++ .../kotlinx/serialization/BigDecimalTest.kt | 193 ++++++++++++++++++ .../json/api/kotlinx-serialization-json.api | 1 + .../kotlinx/serialization/json/JsonElement.kt | 35 +++- .../json/JsonElementSerializers.kt | 4 + .../serialization/json/internal/Composers.kt | 9 +- .../json/internal/StreamingJsonEncoder.kt | 18 +- .../json/internal/TreeJsonEncoder.kt | 17 +- 11 files changed, 425 insertions(+), 13 deletions(-) create mode 100644 formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonRawElementTest.kt create mode 100644 formats/json-tests/jvmTest/src/kotlinx/serialization/BigDecimalTest.kt 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/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..b45cc7af36 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 testJsonRawElementNumbers() = parametrizedTest { jsonTestingMode -> + listOf( + "99999999999999999999999999999999999999999999999999999999999999999999999999", + "99999999999999999999999999999999999999.999999999999999999999999999999999999", + "-99999999999999999999999999999999999999999999999999999999999999999999999999", + "-99999999999999999999999999999999999999.999999999999999999999999999999999999", + "2.99792458e8", + "-2.99792458e8", + ).forEach { literalNum -> + val literalNumJson = JsonRawElement(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) diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonRawElementTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonRawElementTest.kt new file mode 100644 index 0000000000..e978dbeada --- /dev/null +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonRawElementTest.kt @@ -0,0 +1,132 @@ +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 JsonRawElementTest : JsonTestBase() { + + private fun assertRawJsonValueEncoded(inputValue: String) = parametrizedTest { mode -> + val rawElement = JsonRawElement(inputValue) + + assertEquals(inputValue, rawElement.toString(), "expect JsonElement.toString() returns the raw input value") + assertEquals(inputValue, default.encodeToString(JsonElement.serializer(), rawElement, mode)) + } + + @Test + fun testRawJsonNumbers() { + assertRawJsonValueEncoded("1") + assertRawJsonValueEncoded("-1") + assertRawJsonValueEncoded("100.0") + assertRawJsonValueEncoded("-100.0") + + assertRawJsonValueEncoded("9999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999") + assertRawJsonValueEncoded("-9999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999") + + assertRawJsonValueEncoded("99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999") + assertRawJsonValueEncoded("-99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999") + + assertRawJsonValueEncoded("2.99792458e8") + assertRawJsonValueEncoded("-2.99792458e8") + + assertRawJsonValueEncoded("2.99792458E8") + assertRawJsonValueEncoded("-2.99792458E8") + + assertRawJsonValueEncoded("11.399999999999") + assertRawJsonValueEncoded("0.30000000000000004") + assertRawJsonValueEncoded("0.1000000000000000055511151231257827021181583404541015625") + } + + @Test + fun testRawJsonWhitespaceStrings() { + assertRawJsonValueEncoded("") + assertRawJsonValueEncoded(" ") + assertRawJsonValueEncoded("\t") + assertRawJsonValueEncoded("\t\t\t") + assertRawJsonValueEncoded("\r\n") + assertRawJsonValueEncoded("\n") + assertRawJsonValueEncoded("\n\n\n") + } + + @Test + fun testRawJsonStrings() { + assertRawJsonValueEncoded("lorem") + assertRawJsonValueEncoded(""""lorem"""") + assertRawJsonValueEncoded( + """ + 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 testRawJsonObjects() { + assertRawJsonValueEncoded("""{"some":"json"}""") + assertRawJsonValueEncoded("""{"some":"json","object":true,"count":1,"array":[1,2.0,-333,"4",boolean]}""") + } + + @Test + fun testRawJsonArrays() { + assertRawJsonValueEncoded("""[1,2,3]""") + assertRawJsonValueEncoded("""["a","b","c"]""") + assertRawJsonValueEncoded("""[true,false]""") + assertRawJsonValueEncoded("""[1,2.0,-333,"4",boolean]""") + assertRawJsonValueEncoded("""[{"some":"json","object":true,"count":1,"array":[1,2.0,-333,"4",boolean]}]""") + assertRawJsonValueEncoded("""[{"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 testRawJsonNull() { + assertEquals(JsonNull, JsonRawElement(null)) + } + + @Test + fun testRawJsonNullString() { + fun test(block: () -> Unit) { + assertFailsWithSerialMessage( + "JsonEncodingException", + "It is impossible to create a literal unquoted value of 'null'. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive", + block = block, + ) + } + + test { JsonRawElement("null") } + test { JsonRawElement(JsonNull.content) } + } + + @Test + fun testRawJsonInvalidMapKeyIsEscaped() { + val mapSerializer = MapSerializer( + JsonPrimitive.serializer(), + JsonPrimitive.serializer(), + ) + + fun test(expected: String, input: String) = parametrizedTest { mode -> + val data = mapOf(JsonRawElement(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..e91b132c03 --- /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(JsonRawElement(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..08b482cd1a 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 JsonRawElement (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..30ef9527d6 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,42 @@ public fun JsonPrimitive(value: String?): JsonPrimitive { @Suppress("FunctionName", "UNUSED_PARAMETER") // allows to call `JsonPrimitive(null)` public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull +/** + * Create a [JsonPrimitive] from the given string. The value will be encoded as-is (with one exception, see below), + * without additional processing. + * + * This allows for encoding atypical values, such as numbers larger than [Double], or complex JSON objects. + * + * TODO Document why `value == "null"` forbidden + */ +@ExperimentalSerializationApi +@Suppress("FunctionName") +public fun JsonRawElement(value: String?): JsonPrimitive { + return when (value) { + null -> JsonNull + JsonNull.content -> throw JsonEncodingException("It is impossible to create a literal unquoted value of 'null'. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive") + else -> JsonLiteral(value, isString = false, coerceToInlineType = jsonRawElementDescriptor) + } +} + +/** Used as a marker to indicate during encoding that the [JsonEncoder] should use `encodeInline()` */ +@SharedImmutable +internal val jsonRawElementDescriptor: SerialDescriptor = + InlinePrimitiveDescriptor("kotlinx.serialization.json.JsonRawElement", 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 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..7eab79c358 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 ComposerForRawJsonElements(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..696314e5dd 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.isRawJsonElement: Boolean + get() = this.isInline && this == jsonRawElementDescriptor + @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.isRawJsonElement -> StreamingJsonEncoder(composerAs(::ComposerForRawJsonElements), 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..a9df262d99 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.isRawJsonElement -> inlineLiteralNumberEncoder(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 inlineLiteralNumberEncoder(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 = From ace3db0537ede08b3e0dae5b1dd05ce56cd7b2d6 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 13 Oct 2022 16:38:14 +0200 Subject: [PATCH 02/28] update name of inlineRawJsonElementEncoder --- .../kotlinx/serialization/json/internal/TreeJsonEncoder.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a9df262d99..0f5aca9474 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt @@ -105,7 +105,7 @@ private sealed class AbstractJsonTreeEncoder( override fun encodeTaggedInline(tag: String, inlineDescriptor: SerialDescriptor): Encoder = when { inlineDescriptor.isUnsignedNumber -> inlineUnsignedNumberEncoder(tag) - inlineDescriptor.isRawJsonElement -> inlineLiteralNumberEncoder(tag, inlineDescriptor) + inlineDescriptor.isRawJsonElement -> inlineRawJsonElementEncoder(tag, inlineDescriptor) else -> super.encodeTaggedInline(tag, inlineDescriptor) } @@ -120,7 +120,7 @@ private sealed class AbstractJsonTreeEncoder( override fun encodeShort(value: Short) = putUnquotedString(value.toUShort().toString()) } - private fun inlineLiteralNumberEncoder(tag: String, inlineDescriptor: SerialDescriptor) = object : AbstractEncoder() { + private fun inlineRawJsonElementEncoder(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)) From 83043d4d0fbd91f6570ad5ce5086b68488d923e7 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Mon, 17 Oct 2022 18:36:08 +0200 Subject: [PATCH 03/28] rename JsonRawElement -> JsonUnquotedLiteral --- .../json/serializers/JsonPrimitiveSerializerTest.kt | 4 ++-- ...nRawElementTest.kt => JsonUnquotedLiteralTest.kt} | 12 ++++++------ .../src/kotlinx/serialization/BigDecimalTest.kt | 2 +- formats/json/api/kotlinx-serialization-json.api | 2 +- .../src/kotlinx/serialization/json/JsonElement.kt | 6 +++--- .../json/internal/StreamingJsonEncoder.kt | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) rename formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/{JsonRawElementTest.kt => JsonUnquotedLiteralTest.kt} (92%) 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 b45cc7af36..afb88d7e9a 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 @@ -47,7 +47,7 @@ class JsonPrimitiveSerializerTest : JsonTestBase() { } @Test - fun testJsonRawElementNumbers() = parametrizedTest { jsonTestingMode -> + fun testJsonUnquotedLiteralNumbers() = parametrizedTest { jsonTestingMode -> listOf( "99999999999999999999999999999999999999999999999999999999999999999999999999", "99999999999999999999999999999999999999.999999999999999999999999999999999999", @@ -56,7 +56,7 @@ class JsonPrimitiveSerializerTest : JsonTestBase() { "2.99792458e8", "-2.99792458e8", ).forEach { literalNum -> - val literalNumJson = JsonRawElement(literalNum) + val literalNumJson = JsonUnquotedLiteral(literalNum) val wrapper = JsonPrimitiveWrapper(literalNumJson) val string = default.encodeToString(JsonPrimitiveWrapper.serializer(), wrapper, jsonTestingMode) assertEquals("{\"primitive\":$literalNum}", string, "mode:$jsonTestingMode") diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonRawElementTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt similarity index 92% rename from formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonRawElementTest.kt rename to formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt index e978dbeada..63d3fd7c0c 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonRawElementTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt @@ -6,10 +6,10 @@ import kotlinx.serialization.test.assertFailsWithSerialMessage import kotlin.test.Test import kotlin.test.assertEquals -class JsonRawElementTest : JsonTestBase() { +class JsonUnquotedLiteralTest : JsonTestBase() { private fun assertRawJsonValueEncoded(inputValue: String) = parametrizedTest { mode -> - val rawElement = JsonRawElement(inputValue) + val rawElement = JsonUnquotedLiteral(inputValue) assertEquals(inputValue, rawElement.toString(), "expect JsonElement.toString() returns the raw input value") assertEquals(inputValue, default.encodeToString(JsonElement.serializer(), rawElement, mode)) @@ -82,7 +82,7 @@ class JsonRawElementTest : JsonTestBase() { @Test fun testRawJsonNull() { - assertEquals(JsonNull, JsonRawElement(null)) + assertEquals(JsonNull, JsonUnquotedLiteral(null)) } @Test @@ -95,8 +95,8 @@ class JsonRawElementTest : JsonTestBase() { ) } - test { JsonRawElement("null") } - test { JsonRawElement(JsonNull.content) } + test { JsonUnquotedLiteral("null") } + test { JsonUnquotedLiteral(JsonNull.content) } } @Test @@ -107,7 +107,7 @@ class JsonRawElementTest : JsonTestBase() { ) fun test(expected: String, input: String) = parametrizedTest { mode -> - val data = mapOf(JsonRawElement(input) to JsonPrimitive("invalid key")) + val data = mapOf(JsonUnquotedLiteral(input) to JsonPrimitive("invalid key")) assertEquals( """ {"$expected":"invalid key"} """.trim(), diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/BigDecimalTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/BigDecimalTest.kt index e91b132c03..a0c4c73acb 100644 --- a/formats/json-tests/jvmTest/src/kotlinx/serialization/BigDecimalTest.kt +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/BigDecimalTest.kt @@ -185,7 +185,7 @@ private object BigDecimalNumericSerializer : KSerializer { val bdString = value.toPlainString() if (encoder is JsonEncoder) { - encoder.encodeJsonElement(JsonRawElement(bdString)) + 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 08b482cd1a..24aaf10f84 100644 --- a/formats/json/api/kotlinx-serialization-json.api +++ b/formats/json/api/kotlinx-serialization-json.api @@ -187,7 +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 JsonRawElement (Ljava/lang/String;)Lkotlinx/serialization/json/JsonPrimitive; + 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 30ef9527d6..792984c327 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -90,17 +90,17 @@ public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull */ @ExperimentalSerializationApi @Suppress("FunctionName") -public fun JsonRawElement(value: String?): JsonPrimitive { +public fun JsonUnquotedLiteral(value: String?): JsonPrimitive { return when (value) { null -> JsonNull JsonNull.content -> throw JsonEncodingException("It is impossible to create a literal unquoted value of 'null'. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive") - else -> JsonLiteral(value, isString = false, coerceToInlineType = jsonRawElementDescriptor) + else -> JsonLiteral(value, isString = false, coerceToInlineType = jsonUnquotedLiteralDescriptor) } } /** Used as a marker to indicate during encoding that the [JsonEncoder] should use `encodeInline()` */ @SharedImmutable -internal val jsonRawElementDescriptor: SerialDescriptor = +internal val jsonUnquotedLiteralDescriptor: SerialDescriptor = InlinePrimitiveDescriptor("kotlinx.serialization.json.JsonRawElement", String.serializer()) 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 696314e5dd..ba2b0bdec7 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt @@ -24,7 +24,7 @@ internal val SerialDescriptor.isUnsignedNumber: Boolean get() = this.isInline && this in unsignedNumberDescriptors internal val SerialDescriptor.isRawJsonElement: Boolean - get() = this.isInline && this == jsonRawElementDescriptor + get() = this.isInline && this == jsonUnquotedLiteralDescriptor @OptIn(ExperimentalSerializationApi::class) internal class StreamingJsonEncoder( From 462af90685fd88e52e6872dccda94b9d8fb26173 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Wed, 19 Oct 2022 08:56:59 +0200 Subject: [PATCH 04/28] finish renaming JsonRawElement -> JsonUnquotedLiteral --- .../serializers/JsonUnquotedLiteralTest.kt | 94 ++++++++++--------- .../kotlinx/serialization/json/JsonElement.kt | 2 +- .../serialization/json/internal/Composers.kt | 2 +- .../json/internal/StreamingJsonEncoder.kt | 4 +- .../json/internal/TreeJsonEncoder.kt | 4 +- 5 files changed, 55 insertions(+), 51 deletions(-) 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 index 63d3fd7c0c..e1b8458f8f 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt @@ -8,53 +8,57 @@ import kotlin.test.assertEquals class JsonUnquotedLiteralTest : JsonTestBase() { - private fun assertRawJsonValueEncoded(inputValue: String) = parametrizedTest { mode -> - val rawElement = JsonUnquotedLiteral(inputValue) + private fun assertUnquotedLiteralEncoded(inputValue: String) = parametrizedTest { mode -> + val unquotedElement = JsonUnquotedLiteral(inputValue) - assertEquals(inputValue, rawElement.toString(), "expect JsonElement.toString() returns the raw input value") - assertEquals(inputValue, default.encodeToString(JsonElement.serializer(), rawElement, mode)) + assertEquals( + inputValue, + unquotedElement.toString(), + "expect JsonElement.toString() returns the unquoted input value" + ) + assertEquals(inputValue, default.encodeToString(JsonElement.serializer(), unquotedElement, mode)) } @Test - fun testRawJsonNumbers() { - assertRawJsonValueEncoded("1") - assertRawJsonValueEncoded("-1") - assertRawJsonValueEncoded("100.0") - assertRawJsonValueEncoded("-100.0") + fun testUnquotedJsonNumbers() { + assertUnquotedLiteralEncoded("1") + assertUnquotedLiteralEncoded("-1") + assertUnquotedLiteralEncoded("100.0") + assertUnquotedLiteralEncoded("-100.0") - assertRawJsonValueEncoded("9999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999") - assertRawJsonValueEncoded("-9999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999") + assertUnquotedLiteralEncoded("9999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999") + assertUnquotedLiteralEncoded("-9999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999") - assertRawJsonValueEncoded("99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999") - assertRawJsonValueEncoded("-99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999") + assertUnquotedLiteralEncoded("99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999") + assertUnquotedLiteralEncoded("-99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999") - assertRawJsonValueEncoded("2.99792458e8") - assertRawJsonValueEncoded("-2.99792458e8") + assertUnquotedLiteralEncoded("2.99792458e8") + assertUnquotedLiteralEncoded("-2.99792458e8") - assertRawJsonValueEncoded("2.99792458E8") - assertRawJsonValueEncoded("-2.99792458E8") + assertUnquotedLiteralEncoded("2.99792458E8") + assertUnquotedLiteralEncoded("-2.99792458E8") - assertRawJsonValueEncoded("11.399999999999") - assertRawJsonValueEncoded("0.30000000000000004") - assertRawJsonValueEncoded("0.1000000000000000055511151231257827021181583404541015625") + assertUnquotedLiteralEncoded("11.399999999999") + assertUnquotedLiteralEncoded("0.30000000000000004") + assertUnquotedLiteralEncoded("0.1000000000000000055511151231257827021181583404541015625") } @Test - fun testRawJsonWhitespaceStrings() { - assertRawJsonValueEncoded("") - assertRawJsonValueEncoded(" ") - assertRawJsonValueEncoded("\t") - assertRawJsonValueEncoded("\t\t\t") - assertRawJsonValueEncoded("\r\n") - assertRawJsonValueEncoded("\n") - assertRawJsonValueEncoded("\n\n\n") + fun testUnquotedJsonWhitespaceStrings() { + assertUnquotedLiteralEncoded("") + assertUnquotedLiteralEncoded(" ") + assertUnquotedLiteralEncoded("\t") + assertUnquotedLiteralEncoded("\t\t\t") + assertUnquotedLiteralEncoded("\r\n") + assertUnquotedLiteralEncoded("\n") + assertUnquotedLiteralEncoded("\n\n\n") } @Test - fun testRawJsonStrings() { - assertRawJsonValueEncoded("lorem") - assertRawJsonValueEncoded(""""lorem"""") - assertRawJsonValueEncoded( + fun testUnquotedJsonStrings() { + assertUnquotedLiteralEncoded("lorem") + assertUnquotedLiteralEncoded(""""lorem"""") + assertUnquotedLiteralEncoded( """ Well, my name is Freddy Kreuger I've got the Elm Street blues @@ -65,28 +69,28 @@ class JsonUnquotedLiteralTest : JsonTestBase() { } @Test - fun testRawJsonObjects() { - assertRawJsonValueEncoded("""{"some":"json"}""") - assertRawJsonValueEncoded("""{"some":"json","object":true,"count":1,"array":[1,2.0,-333,"4",boolean]}""") + fun testUnquotedJsonObjects() { + assertUnquotedLiteralEncoded("""{"some":"json"}""") + assertUnquotedLiteralEncoded("""{"some":"json","object":true,"count":1,"array":[1,2.0,-333,"4",boolean]}""") } @Test - fun testRawJsonArrays() { - assertRawJsonValueEncoded("""[1,2,3]""") - assertRawJsonValueEncoded("""["a","b","c"]""") - assertRawJsonValueEncoded("""[true,false]""") - assertRawJsonValueEncoded("""[1,2.0,-333,"4",boolean]""") - assertRawJsonValueEncoded("""[{"some":"json","object":true,"count":1,"array":[1,2.0,-333,"4",boolean]}]""") - assertRawJsonValueEncoded("""[{"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]}]""") + 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 testRawJsonNull() { + fun testUnquotedJsonNull() { assertEquals(JsonNull, JsonUnquotedLiteral(null)) } @Test - fun testRawJsonNullString() { + fun testUnquotedJsonNullString() { fun test(block: () -> Unit) { assertFailsWithSerialMessage( "JsonEncodingException", @@ -100,7 +104,7 @@ class JsonUnquotedLiteralTest : JsonTestBase() { } @Test - fun testRawJsonInvalidMapKeyIsEscaped() { + fun testUnquotedJsonInvalidMapKeyIsEscaped() { val mapSerializer = MapSerializer( JsonPrimitive.serializer(), JsonPrimitive.serializer(), diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index 792984c327..02c4314e57 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -101,7 +101,7 @@ public fun JsonUnquotedLiteral(value: String?): JsonPrimitive { /** Used as a marker to indicate during encoding that the [JsonEncoder] should use `encodeInline()` */ @SharedImmutable internal val jsonUnquotedLiteralDescriptor: SerialDescriptor = - InlinePrimitiveDescriptor("kotlinx.serialization.json.JsonRawElement", String.serializer()) + InlinePrimitiveDescriptor("kotlinx.serialization.json.JsonUnquotedLiteral", String.serializer()) // JsonLiteral is deprecated for public use and no longer available. Please use JsonPrimitive instead 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 7eab79c358..d5841552c4 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt @@ -61,7 +61,7 @@ internal class ComposerForUnsignedNumbers(writer: JsonWriter, private val forceQ } @SuppressAnimalSniffer -internal class ComposerForRawJsonElements(writer: JsonWriter, private val forceQuoting: Boolean) : Composer(writer) { +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) } 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 ba2b0bdec7..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,7 +23,7 @@ private val unsignedNumberDescriptors = setOf( internal val SerialDescriptor.isUnsignedNumber: Boolean get() = this.isInline && this in unsignedNumberDescriptors -internal val SerialDescriptor.isRawJsonElement: Boolean +internal val SerialDescriptor.isUnquotedLiteral: Boolean get() = this.isInline && this == jsonUnquotedLiteralDescriptor @OptIn(ExperimentalSerializationApi::class) @@ -161,7 +161,7 @@ internal class StreamingJsonEncoder( override fun encodeInline(descriptor: SerialDescriptor): Encoder = when { descriptor.isUnsignedNumber -> StreamingJsonEncoder(composerAs(::ComposerForUnsignedNumbers), json, mode, null) - descriptor.isRawJsonElement -> StreamingJsonEncoder(composerAs(::ComposerForRawJsonElements), json, mode, null) + descriptor.isUnquotedLiteral -> StreamingJsonEncoder(composerAs(::ComposerForUnquotedLiterals), json, mode, null) else -> super.encodeInline(descriptor) } 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 0f5aca9474..643e158e15 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt @@ -105,7 +105,7 @@ private sealed class AbstractJsonTreeEncoder( override fun encodeTaggedInline(tag: String, inlineDescriptor: SerialDescriptor): Encoder = when { inlineDescriptor.isUnsignedNumber -> inlineUnsignedNumberEncoder(tag) - inlineDescriptor.isRawJsonElement -> inlineRawJsonElementEncoder(tag, inlineDescriptor) + inlineDescriptor.isUnquotedLiteral -> inlineUnquotedLiteralEncoder(tag, inlineDescriptor) else -> super.encodeTaggedInline(tag, inlineDescriptor) } @@ -120,7 +120,7 @@ private sealed class AbstractJsonTreeEncoder( override fun encodeShort(value: Short) = putUnquotedString(value.toUShort().toString()) } - private fun inlineRawJsonElementEncoder(tag: String, inlineDescriptor: SerialDescriptor) = object : AbstractEncoder() { + 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)) From b79528d5a6e895d26ea773a4d1c944bd68e5dad5 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Wed, 19 Oct 2022 09:13:43 +0200 Subject: [PATCH 05/28] update docs for JsonUnquotedLiteral --- .../kotlinx/serialization/json/JsonElement.kt | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index 02c4314e57..72a1a1e0e9 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -81,12 +81,24 @@ public fun JsonPrimitive(value: String?): JsonPrimitive { public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull /** - * Create a [JsonPrimitive] from the given string. The value will be encoded as-is (with one exception, see below), - * without additional processing. + * Create a [JsonPrimitive] from the given string, without surrounding it in quotes. * - * This allows for encoding atypical values, such as numbers larger than [Double], or complex JSON objects. + * The value will be encoded without surrounding it in quotes. This allows 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. + * + * ### Warnings + * + * Unlike the [JsonPrimitive] functions, it is possible to produce invalid JSON using this function! + * + * Encoding a string with a value of `"null"` is forbidden because... * * TODO Document why `value == "null"` forbidden + * + * @see JsonPrimitive This is the preferred method for encoding JSON primitives. */ @ExperimentalSerializationApi @Suppress("FunctionName") @@ -154,7 +166,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 { @@ -262,7 +276,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 From c437fc4d12abcfaf1c3cea320689ad21bf7f7de1 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Wed, 19 Oct 2022 20:14:14 +0200 Subject: [PATCH 06/28] update docs for JsonUnquotedLiteral --- .../serializers/JsonPrimitiveSerializerTest.kt | 2 +- .../serializers/JsonUnquotedLiteralTest.kt | 5 +++-- .../kotlinx/serialization/json/JsonElement.kt | 18 ++++++++---------- 3 files changed, 12 insertions(+), 13 deletions(-) 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 afb88d7e9a..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 @@ -98,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 index e1b8458f8f..08aba9c3e8 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt @@ -93,14 +93,15 @@ class JsonUnquotedLiteralTest : JsonTestBase() { fun testUnquotedJsonNullString() { fun test(block: () -> Unit) { assertFailsWithSerialMessage( - "JsonEncodingException", - "It is impossible to create a literal unquoted value of 'null'. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive", + exceptionName = "JsonEncodingException", + message = "Creating an 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 diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index 72a1a1e0e9..2b18af28fa 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -83,29 +83,27 @@ public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull /** * Create a [JsonPrimitive] from the given string, without surrounding it in quotes. * - * The value will be encoded without surrounding it in quotes. This allows for encoding raw JSON values that cannot be - * encoded using the [JsonPrimitive] functions. For example, + * **Note:** 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]. + * + * 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. * - * ### Warnings - * - * Unlike the [JsonPrimitive] functions, it is possible to produce invalid JSON using this function! - * - * Encoding a string with a value of `"null"` is forbidden because... - * - * TODO Document why `value == "null"` forbidden + * Be aware that it is possible to create invalid JSON using this function! * * @see JsonPrimitive This 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("It is impossible to create a literal unquoted value of 'null'. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive") + JsonNull.content -> throw JsonEncodingException("Creating an 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) } } From 74a91a19ed0b884627fa4dbac9e16f6369472464 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Wed, 19 Oct 2022 21:42:12 +0200 Subject: [PATCH 07/28] add knit docs for JsonUnquotedLiteral --- docs/json.md | 133 +++++++++++++++++++++++++++++-- docs/serialization-guide.md | 3 + guide/example/example-json-16.kt | 35 +++----- guide/example/example-json-17.kt | 31 ++++--- guide/example/example-json-18.kt | 16 +--- guide/example/example-json-19.kt | 36 ++++----- guide/example/example-json-20.kt | 59 ++++---------- guide/example/example-json-21.kt | 37 +++------ guide/example/example-json-22.kt | 36 +++++++++ guide/example/example-json-23.kt | 59 ++++++++++++++ guide/example/example-json-24.kt | 37 +++++++++ guide/test/JsonTest.kt | 48 ++++++++--- 12 files changed, 372 insertions(+), 158 deletions(-) create mode 100644 guide/example/example-json-22.kt create mode 100644 guide/example/example-json-23.kt create mode 100644 guide/example/example-json-24.kt diff --git a/docs/json.md b/docs/json.md index 59e52d3e20..ad81634384 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 raw, literal Json content (experimental)](#encoding-raw-literal-json-content-experimental) + * [Example: Serializing large decimal numbers](#example:-serializing-large-decimal-numbers) + * [Using `JsonUnquotedLiteral` to encode `"null"` is forbidden](#using-jsonunquotedliteral-to-encode-"null"-is-forbidden) * [Json transformations](#json-transformations) * [Array wrapping](#array-wrapping) * [Array unwrapping](#array-unwrapping) @@ -612,6 +615,121 @@ Project(name=kotlinx.serialization, language=Kotlin) +### Encoding raw, literal Json content (experimental) + +In exceptional cases it might be necessary to encode a value as-is, without any alteration. This can be achieved with +[JsonUnquotedLiteral]. + +#### Example: 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 this is limited in precision, and so large numbers are truncated. When using Kotlin/JVM +[BigDecimal] can be used instead, but [JsonPrimitive()] will wrap the string content in quotes. + +```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 XX 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 the loss of precision, the raw value 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" +} +``` + + + + +#### Using `JsonUnquotedLiteral` to encode `"null"` is forbidden + +To avoid creating an inconsistent state, encoding a String equal to `"null"` is forbidden. +Use [JsonNull] or [JsonPrimitive] instead. + +```kotlin +import java.math.BigDecimal + +fun main() { + // creating a + JsonUnquotedLiteral("null") +} +``` + +> You can get the full code [here](../guide/example/example-json-18.kt). + + + +```text +Exception in thread "main" kotlinx.serialization.json.internal.JsonEncodingException: Creating an 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 +797,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-19.kt). The output shows that both cases are correctly deserialized into a Kotlin [List]. @@ -731,7 +849,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-20.kt). You end up with a single JSON object, not an array with one element: @@ -776,7 +894,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-21.kt). See the effect of the custom serializer: @@ -849,7 +967,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-22.kt). No class discriminator is added in the JSON output: @@ -945,7 +1063,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-23.kt). This gives you fine-grained control on the representation of the `Response` class in the JSON output: @@ -1010,7 +1128,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-24.kt). ```text UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"}) @@ -1027,6 +1145,7 @@ The next chapter covers [Alternative and custom formats (experimental)](formats. [RFC-4627]: https://www.ietf.org/rfc/rfc4627.txt +[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 +1198,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..b6a9ff2d26 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 raw, literal Json content (experimental)](json.md#encoding-raw-literal-json-content-experimental) + * [Example: Serializing large decimal numbers](json.md#example:-serializing-large-decimal-numbers) + * [Using `JsonUnquotedLiteral` to encode `"null"` is forbidden](json.md#using-jsonunquotedliteral-to-encode-"null"-is-forbidden) * [Json transformations](json.md#json-transformations) * [Array wrapping](json.md#array-wrapping) * [Array unwrapping](json.md#array-unwrapping) 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..10b5b3113b 100644 --- a/guide/example/example-json-18.kt +++ b/guide/example/example-json-18.kt @@ -4,19 +4,9 @@ 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 + // creating a + JsonUnquotedLiteral("null") } diff --git a/guide/example/example-json-19.kt b/guide/example/example-json-19.kt index 4455d63723..7a53defcd3 100644 --- a/guide/example/example-json-19.kt +++ b/guide/example/example-json-19.kt @@ -7,30 +7,26 @@ 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() - +data class Project( + val name: String, + @Serializable(with = UserListSerializer::class) + val users: List +) @Serializable -data class OwnedProject(override val name: String, val owner: String) : Project() +data class User(val name: String) -object ProjectSerializer : JsonContentPolymorphicSerializer(Project::class) { - override fun selectDeserializer(element: JsonElement) = when { - "owner" in element.jsonObject -> OwnedProject.serializer() - else -> BasicProject.serializer() - } +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() { - val data = listOf( - OwnedProject("kotlinx.serialization", "kotlin"), - BasicProject("example") - ) - val string = Json.encodeToString(ListSerializer(ProjectSerializer), data) - println(string) - println(Json.decodeFromString(ListSerializer(ProjectSerializer), 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-20.kt b/guide/example/example-json-20.kt index e613a08f2a..39e1dff92a 100644 --- a/guide/example/example-json-20.kt +++ b/guide/example/example-json-20.kt @@ -4,56 +4,27 @@ 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() -} +@Serializable +data class Project( + val name: String, + @Serializable(with = UserListSerializer::class) + val users: List +) -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 User(val name: String) - 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)) - } +object UserListSerializer : JsonTransformingSerializer>(ListSerializer(User.serializer())) { - 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) + override fun transformSerialize(element: JsonElement): JsonElement { + require(element is JsonArray) // this serializer is used only with lists + return element.singleOrNull() ?: 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)) + val data = Project("kotlinx.serialization", listOf(User("kotlin"))) + println(Json.encodeToString(data)) } diff --git a/guide/example/example-json-21.kt b/guide/example/example-json-21.kt index 92de429b17..a232d8f50f 100644 --- a/guide/example/example-json-21.kt +++ b/guide/example/example-json-21.kt @@ -4,34 +4,19 @@ package example.exampleJson21 import kotlinx.serialization.* import kotlinx.serialization.json.* -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* +@Serializable +class Project(val name: String, val language: String) -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") - } +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() { - println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}""")) + 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-22.kt b/guide/example/example-json-22.kt new file mode 100644 index 0000000000..5b5c68b3f4 --- /dev/null +++ b/guide/example/example-json-22.kt @@ -0,0 +1,36 @@ +// This file was automatically generated from json.md by Knit tool. Do not edit. +package example.exampleJson22 + +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-23.kt b/guide/example/example-json-23.kt new file mode 100644 index 0000000000..f99da9942d --- /dev/null +++ b/guide/example/example-json-23.kt @@ -0,0 +1,59 @@ +// 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.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-24.kt b/guide/example/example-json-24.kt new file mode 100644 index 0000000000..537b22c7e2 --- /dev/null +++ b/guide/example/example-json-24.kt @@ -0,0 +1,37 @@ +// 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.* + +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..36d0253f3e 100644 --- a/guide/test/JsonTest.kt +++ b/guide/test/JsonTest.kt @@ -118,45 +118,73 @@ 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( - "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}", - "{\"name\":\"kotlinx.serialization\"}" + captureOutput("ExampleJson18") { example.exampleJson18.main() }.verifyOutputLinesStart( + "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonEncodingException: Creating an literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive" ) } @Test fun testExampleJson19() { captureOutput("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLines( - "[{\"name\":\"kotlinx.serialization\",\"owner\":\"kotlin\"},{\"name\":\"example\"}]", - "[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]" + "Project(name=kotlinx.serialization, users=[User(name=kotlin)])", + "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])" ) } @Test fun testExampleJson20() { captureOutput("ExampleJson20") { example.exampleJson20.main() }.verifyOutputLines( - "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]", - "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]" + "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}" ) } @Test fun testExampleJson21() { captureOutput("ExampleJson21") { example.exampleJson21.main() }.verifyOutputLines( + "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}", + "{\"name\":\"kotlinx.serialization\"}" + ) + } + + @Test + fun testExampleJson22() { + captureOutput("ExampleJson22") { example.exampleJson22.main() }.verifyOutputLines( + "[{\"name\":\"kotlinx.serialization\",\"owner\":\"kotlin\"},{\"name\":\"example\"}]", + "[OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]" + ) + } + + @Test + fun testExampleJson23() { + captureOutput("ExampleJson23") { example.exampleJson23.main() }.verifyOutputLines( + "[{\"name\":\"kotlinx.serialization\"},{\"error\":\"Not found\"}]", + "[Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]" + ) + } + + @Test + fun testExampleJson24() { + captureOutput("ExampleJson24") { example.exampleJson24.main() }.verifyOutputLines( "UnknownProject(name=example, details={\"type\":\"unknown\",\"maintainer\":\"Unknown\",\"license\":\"Apache 2.0\"})" ) } From 816f50ee1b33ede9a93f794bcee72ebfe3ef6935 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Wed, 19 Oct 2022 21:44:29 +0200 Subject: [PATCH 08/28] grammar/editing knit docs --- docs/json.md | 18 +++++++----------- docs/serialization-guide.md | 2 +- guide/example/example-json-18.kt | 4 +--- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/docs/json.md b/docs/json.md index ad81634384..8e2de5e71f 100644 --- a/docs/json.md +++ b/docs/json.md @@ -26,7 +26,7 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json * [Json element builders](#json-element-builders) * [Decoding Json elements](#decoding-json-elements) * [Encoding raw, literal Json content (experimental)](#encoding-raw-literal-json-content-experimental) - * [Example: Serializing large decimal numbers](#example:-serializing-large-decimal-numbers) + * [Serializing large decimal numbers](#serializing-large-decimal-numbers) * [Using `JsonUnquotedLiteral` to encode `"null"` is forbidden](#using-jsonunquotedliteral-to-encode-"null"-is-forbidden) * [Json transformations](#json-transformations) * [Array wrapping](#array-wrapping) @@ -620,13 +620,13 @@ Project(name=kotlinx.serialization, language=Kotlin) In exceptional cases it might be necessary to encode a value as-is, without any alteration. This can be achieved with [JsonUnquotedLiteral]. -#### Example: Serializing large decimal numbers +#### 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 this is limited in precision, and so large numbers are truncated. When using Kotlin/JVM -[BigDecimal] can be used instead, but [JsonPrimitive()] will wrap the string content in quotes. +[BigDecimal] can be used instead, but [JsonPrimitive()] will encode this as a string, not a number. ```kotlin import java.math.BigDecimal @@ -650,8 +650,8 @@ fun main() { > 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 XX decimal places, and the String is wrapped in quotes - which is not a JSON number. +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 { @@ -663,7 +663,7 @@ The [Double] value is truncated to XX decimal places, and the String is wrapped -To avoid the loss of precision, the raw value can be encoded using [JsonUnquotedLiteral]. +To avoid the loss of precision, a string value can be encoded using [JsonUnquotedLiteral]. ```kotlin @@ -711,18 +711,14 @@ To avoid creating an inconsistent state, encoding a String equal to `"null"` is Use [JsonNull] or [JsonPrimitive] instead. ```kotlin -import java.math.BigDecimal - fun main() { - // creating a + // caution: creating null with JsonUnquotedLiteral will cause an exception! JsonUnquotedLiteral("null") } ``` > You can get the full code [here](../guide/example/example-json-18.kt). - - ```text Exception in thread "main" kotlinx.serialization.json.internal.JsonEncodingException: Creating an literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive ``` diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index b6a9ff2d26..b066ed0c30 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -124,7 +124,7 @@ Once the project is set up, we can start serializing some classes. * [Json element builders](json.md#json-element-builders) * [Decoding Json elements](json.md#decoding-json-elements) * [Encoding raw, literal Json content (experimental)](json.md#encoding-raw-literal-json-content-experimental) - * [Example: Serializing large decimal numbers](json.md#example:-serializing-large-decimal-numbers) + * [Serializing large decimal numbers](json.md#serializing-large-decimal-numbers) * [Using `JsonUnquotedLiteral` to encode `"null"` is forbidden](json.md#using-jsonunquotedliteral-to-encode-"null"-is-forbidden) * [Json transformations](json.md#json-transformations) * [Array wrapping](json.md#array-wrapping) diff --git a/guide/example/example-json-18.kt b/guide/example/example-json-18.kt index 10b5b3113b..402142cddb 100644 --- a/guide/example/example-json-18.kt +++ b/guide/example/example-json-18.kt @@ -4,9 +4,7 @@ package example.exampleJson18 import kotlinx.serialization.* import kotlinx.serialization.json.* -import java.math.BigDecimal - fun main() { - // creating a + // caution: creating null with JsonUnquotedLiteral will cause an exception! JsonUnquotedLiteral("null") } From e33fbe9c0f7e5f9869da5b4422ac02821f6deffc Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Wed, 19 Oct 2022 22:01:03 +0200 Subject: [PATCH 09/28] additional Knit test for BigDecimal decoding, and minor formatting/grammar updates --- docs/json.md | 54 ++++++++++++++++++++++------- guide/example/example-json-18.kt | 17 +++++++-- guide/example/example-json-19.kt | 26 ++------------ guide/example/example-json-20.kt | 16 +++++---- guide/example/example-json-21.kt | 28 +++++++++------ guide/example/example-json-22.kt | 36 ++++++------------- guide/example/example-json-23.kt | 59 ++++++++++---------------------- guide/example/example-json-24.kt | 58 +++++++++++++++++++++---------- guide/example/example-json-25.kt | 37 ++++++++++++++++++++ guide/test/JsonTest.kt | 31 ++++++++++------- 10 files changed, 211 insertions(+), 151 deletions(-) create mode 100644 guide/example/example-json-25.kt diff --git a/docs/json.md b/docs/json.md index 8e2de5e71f..74f8d7563c 100644 --- a/docs/json.md +++ b/docs/json.md @@ -625,8 +625,9 @@ In exceptional cases it might be necessary to encode a value as-is, without any 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 this is limited in precision, and so large numbers are truncated. When using Kotlin/JVM -[BigDecimal] can be used instead, but [JsonPrimitive()] will encode this as a string, not a number. +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 @@ -662,9 +663,7 @@ The [Double] value is truncated to 15 decimal places, and the String is wrapped - -To avoid the loss of precision, a string value can be encoded using [JsonUnquotedLiteral]. - +To avoid the loss of precision, the string value of `pi` can be encoded using [JsonUnquotedLiteral]. ```kotlin import java.math.BigDecimal @@ -704,6 +703,37 @@ fun main() { +To decode `pi` back to a [BigDecimal], the string content of the [JsonPrimitive] can be used. + +```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 encode `"null"` is forbidden @@ -717,7 +747,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). ```text Exception in thread "main" kotlinx.serialization.json.internal.JsonEncodingException: Creating an literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive @@ -793,7 +823,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 output shows that both cases are correctly deserialized into a Kotlin [List]. @@ -845,7 +875,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). You end up with a single JSON object, not an array with one element: @@ -890,7 +920,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). See the effect of the custom serializer: @@ -963,7 +993,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). No class discriminator is added in the JSON output: @@ -1059,7 +1089,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). This gives you fine-grained control on the representation of the `Response` class in the JSON output: @@ -1124,7 +1154,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). ```text UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"}) diff --git a/guide/example/example-json-18.kt b/guide/example/example-json-18.kt index 402142cddb..471d320933 100644 --- a/guide/example/example-json-18.kt +++ b/guide/example/example-json-18.kt @@ -4,7 +4,20 @@ package example.exampleJson18 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-19.kt b/guide/example/example-json-19.kt index 7a53defcd3..4fd0e2924b 100644 --- a/guide/example/example-json-19.kt +++ b/guide/example/example-json-19.kt @@ -4,29 +4,7 @@ package example.exampleJson19 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-20.kt b/guide/example/example-json-20.kt index 39e1dff92a..949a25811d 100644 --- a/guide/example/example-json-20.kt +++ b/guide/example/example-json-20.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-21.kt b/guide/example/example-json-21.kt index a232d8f50f..1d25360073 100644 --- a/guide/example/example-json-21.kt +++ b/guide/example/example-json-21.kt @@ -4,19 +4,27 @@ 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 +) + @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-22.kt b/guide/example/example-json-22.kt index 5b5c68b3f4..b987c27309 100644 --- a/guide/example/example-json-22.kt +++ b/guide/example/example-json-22.kt @@ -4,33 +4,19 @@ package example.exampleJson22 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-23.kt b/guide/example/example-json-23.kt index f99da9942d..06570cafec 100644 --- a/guide/example/example-json-23.kt +++ b/guide/example/example-json-23.kt @@ -4,56 +4,33 @@ package example.exampleJson23 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-24.kt b/guide/example/example-json-24.kt index 537b22c7e2..02a05ee6c1 100644 --- a/guide/example/example-json-24.kt +++ b/guide/example/example-json-24.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-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 36d0253f3e..02684fac42 100644 --- a/guide/test/JsonTest.kt +++ b/guide/test/JsonTest.kt @@ -138,53 +138,60 @@ class JsonTest { @Test fun testExampleJson18() { - captureOutput("ExampleJson18") { example.exampleJson18.main() }.verifyOutputLinesStart( - "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonEncodingException: Creating an literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive" + captureOutput("ExampleJson18") { example.exampleJson18.main() }.verifyOutputLines( + "3.141592653589793238462643383279" ) } @Test fun testExampleJson19() { - captureOutput("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLines( - "Project(name=kotlinx.serialization, users=[User(name=kotlin)])", - "Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])" + captureOutput("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLinesStart( + "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonEncodingException: Creating an 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( - "{\"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 testExampleJson21() { captureOutput("ExampleJson21") { example.exampleJson21.main() }.verifyOutputLines( - "{\"name\":\"kotlinx.serialization\",\"language\":\"Kotlin\"}", - "{\"name\":\"kotlinx.serialization\"}" + "{\"name\":\"kotlinx.serialization\",\"users\":{\"name\":\"kotlin\"}}" ) } @Test fun testExampleJson22() { captureOutput("ExampleJson22") { example.exampleJson22.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 testExampleJson23() { captureOutput("ExampleJson23") { example.exampleJson23.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 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 testExampleJson25() { + captureOutput("ExampleJson25") { example.exampleJson25.main() }.verifyOutputLines( "UnknownProject(name=example, details={\"type\":\"unknown\",\"maintainer\":\"Unknown\",\"license\":\"Apache 2.0\"})" ) } From 835816ef433d7d7b60e0f2f842b8fb0831c69b31 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Wed, 19 Oct 2022 22:04:38 +0200 Subject: [PATCH 10/28] add 'opt-in' note --- docs/json.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/json.md b/docs/json.md index 74f8d7563c..eead46af6a 100644 --- a/docs/json.md +++ b/docs/json.md @@ -617,6 +617,8 @@ Project(name=kotlinx.serialization, language=Kotlin) ### Encoding raw, literal Json content (experimental) +> This functionality is experimental and requires opting-in to [the experimental Kotlinx Serialization API](compatibility.md#experimental-api). + In exceptional cases it might be necessary to encode a value as-is, without any alteration. This can be achieved with [JsonUnquotedLiteral]. From 40e8f3f378c0adc06ad6e8a261682892e5e7f3fa Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Wed, 19 Oct 2022 22:16:06 +0200 Subject: [PATCH 11/28] add BigDecimal link --- docs/json.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/json.md b/docs/json.md index eead46af6a..cbd9b88a5d 100644 --- a/docs/json.md +++ b/docs/json.md @@ -1238,5 +1238,6 @@ The next chapter covers [Alternative and custom formats (experimental)](formats. [JsonDecoder.json]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-decoder/json.html [JsonEncoder.json]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-encoder/json.html [Json.encodeToJsonElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/encode-to-json-element.html +[BigDecimal]: https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html From ef3d88e10f6729e6d2f0147492711abcd4d6b4ec Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Wed, 19 Oct 2022 22:45:17 +0200 Subject: [PATCH 12/28] minor grammar tweak --- docs/json.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/json.md b/docs/json.md index cbd9b88a5d..934867a947 100644 --- a/docs/json.md +++ b/docs/json.md @@ -615,12 +615,12 @@ Project(name=kotlinx.serialization, language=Kotlin) -### Encoding raw, literal Json content (experimental) +### Encoding literal Json content (experimental) > This functionality is experimental and requires opting-in to [the experimental Kotlinx Serialization API](compatibility.md#experimental-api). -In exceptional cases it might be necessary to encode a value as-is, without any alteration. This can be achieved with -[JsonUnquotedLiteral]. +In exceptional cases it might be necessary to encode a raw, literal value. +This can be achieved with [JsonUnquotedLiteral]. #### Serializing large decimal numbers From 2309026633341b5792caf06c886adafac6c5b20b Mon Sep 17 00:00:00 2001 From: aSemy <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:57:10 +0200 Subject: [PATCH 13/28] Update docs/json.md Co-authored-by: Leonid Startsev --- docs/json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/json.md b/docs/json.md index 934867a947..1759db6cea 100644 --- a/docs/json.md +++ b/docs/json.md @@ -619,7 +619,7 @@ Project(name=kotlinx.serialization, language=Kotlin) > This functionality is experimental and requires opting-in to [the experimental Kotlinx Serialization API](compatibility.md#experimental-api). -In exceptional cases it might be necessary to encode a raw, literal value. +In some cases, it might be necessary to encode an arbitrary unquoted value. This can be achieved with [JsonUnquotedLiteral]. #### Serializing large decimal numbers From e2fe1c40cd5211a5985dd08549547d152ab0a296 Mon Sep 17 00:00:00 2001 From: aSemy <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:57:26 +0200 Subject: [PATCH 14/28] Update docs/json.md Co-authored-by: Leonid Startsev --- docs/json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/json.md b/docs/json.md index 1759db6cea..a4a713bd42 100644 --- a/docs/json.md +++ b/docs/json.md @@ -665,7 +665,7 @@ The [Double] value is truncated to 15 decimal places, and the String is wrapped -To avoid the loss of precision, the string value of `pi` can be encoded using [JsonUnquotedLiteral]. +To avoid precision loss, the string value of `pi` can be encoded using [JsonUnquotedLiteral]. ```kotlin import java.math.BigDecimal From e43023450e9fa716a831d60b078f951c39ab0bcb Mon Sep 17 00:00:00 2001 From: aSemy <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:58:11 +0200 Subject: [PATCH 15/28] Update formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt Co-authored-by: Leonid Startsev --- .../commonMain/src/kotlinx/serialization/json/JsonElement.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index 2b18af28fa..71d4a8b87b 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -93,7 +93,7 @@ public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull * * large numbers, * * or complex JSON objects. * - * Be aware that it is possible to create invalid JSON using this function! + * Be aware that it is possible to create invalid JSON using this function. * * @see JsonPrimitive This is the preferred method for encoding JSON primitives. * @throws JsonEncodingException If `value == "null"` From c5e40dcb062dd50111b2da21abffb4a36add0ce9 Mon Sep 17 00:00:00 2001 From: aSemy <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:58:31 +0200 Subject: [PATCH 16/28] Update formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt Co-authored-by: Leonid Startsev --- .../commonMain/src/kotlinx/serialization/json/JsonElement.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index 71d4a8b87b..07d93c8440 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -81,7 +81,7 @@ public fun JsonPrimitive(value: String?): JsonPrimitive { public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull /** - * Create a [JsonPrimitive] from the given string, without surrounding it in quotes. + * Creates a [JsonPrimitive] from the given string, without surrounding it in quotes. * * **Note:** 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]. From 374fcc05cf48e6edc478734e12c3ac10f04a2d58 Mon Sep 17 00:00:00 2001 From: aSemy <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:58:44 +0200 Subject: [PATCH 17/28] Update formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt Co-authored-by: Leonid Startsev --- .../commonMain/src/kotlinx/serialization/json/JsonElement.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index 07d93c8440..503d295411 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -95,7 +95,7 @@ public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull * * Be aware that it is possible to create invalid JSON using this function. * - * @see JsonPrimitive This is the preferred method for encoding JSON primitives. + * @see JsonPrimitive is the preferred method for encoding JSON primitives. * @throws JsonEncodingException If `value == "null"` */ @ExperimentalSerializationApi From be8a5d0dbfe99809c6a604f171061ef2c4e8572e Mon Sep 17 00:00:00 2001 From: aSemy <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:58:53 +0200 Subject: [PATCH 18/28] Update formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt Co-authored-by: Leonid Startsev --- .../commonMain/src/kotlinx/serialization/json/JsonElement.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index 503d295411..13e4004937 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -96,7 +96,7 @@ public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull * Be aware that it is possible to create invalid JSON using this function. * * @see JsonPrimitive is the preferred method for encoding JSON primitives. - * @throws JsonEncodingException If `value == "null"` + * @throws JsonEncodingException if `value == "null"` */ @ExperimentalSerializationApi @Suppress("FunctionName") From 1374e855768b727fee23c92cc65eefa1207504e6 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 17:03:46 +0200 Subject: [PATCH 19/28] move note regarding `"null"` --- .../src/kotlinx/serialization/json/JsonElement.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index 13e4004937..79f769cea2 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -83,9 +83,6 @@ public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull /** * Creates a [JsonPrimitive] from the given string, without surrounding it in quotes. * - * **Note:** 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]. - * * This function is provided for encoding raw JSON values that cannot be encoded using the [JsonPrimitive] functions. * For example, * @@ -95,6 +92,9 @@ public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull * * 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"` */ From 1a19d10a95ec52ec0dbaec06e1b283251620d2bc Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 17:04:48 +0200 Subject: [PATCH 20/28] move non-parmeterized assert out of parametrizedTest {} --- .../json/serializers/JsonUnquotedLiteralTest.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 index 08aba9c3e8..3f6f585990 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt @@ -8,7 +8,7 @@ import kotlin.test.assertEquals class JsonUnquotedLiteralTest : JsonTestBase() { - private fun assertUnquotedLiteralEncoded(inputValue: String) = parametrizedTest { mode -> + private fun assertUnquotedLiteralEncoded(inputValue: String) { val unquotedElement = JsonUnquotedLiteral(inputValue) assertEquals( @@ -16,7 +16,10 @@ class JsonUnquotedLiteralTest : JsonTestBase() { unquotedElement.toString(), "expect JsonElement.toString() returns the unquoted input value" ) - assertEquals(inputValue, default.encodeToString(JsonElement.serializer(), unquotedElement, mode)) + + parametrizedTest { mode -> + assertEquals(inputValue, default.encodeToString(JsonElement.serializer(), unquotedElement, mode)) + } } @Test From 61a7c42608889f6d27a42ba7e2c0f865086033da Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 17:07:03 +0200 Subject: [PATCH 21/28] update 'knit' --- docs/json.md | 4 ++-- docs/serialization-guide.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/json.md b/docs/json.md index a4a713bd42..f6fccd6e6f 100644 --- a/docs/json.md +++ b/docs/json.md @@ -25,7 +25,7 @@ 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 raw, literal Json content (experimental)](#encoding-raw-literal-json-content-experimental) + * [Encoding literal Json content (experimental)](#encoding-literal-json-content-experimental) * [Serializing large decimal numbers](#serializing-large-decimal-numbers) * [Using `JsonUnquotedLiteral` to encode `"null"` is forbidden](#using-jsonunquotedliteral-to-encode-"null"-is-forbidden) * [Json transformations](#json-transformations) @@ -1171,6 +1171,7 @@ 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/ @@ -1238,6 +1239,5 @@ The next chapter covers [Alternative and custom formats (experimental)](formats. [JsonDecoder.json]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-decoder/json.html [JsonEncoder.json]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-encoder/json.html [Json.encodeToJsonElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/encode-to-json-element.html -[BigDecimal]: https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index b066ed0c30..507109b28f 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -123,7 +123,7 @@ 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 raw, literal Json content (experimental)](json.md#encoding-raw-literal-json-content-experimental) + * [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 encode `"null"` is forbidden](json.md#using-jsonunquotedliteral-to-encode-"null"-is-forbidden) * [Json transformations](json.md#json-transformations) From d75ea074bd7ed1978687db4d4511934bfe5db788 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 17:10:10 +0200 Subject: [PATCH 22/28] bump knit version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5a8c304d5dbd57d83c26a431c8564d42da127bde Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 17:15:48 +0200 Subject: [PATCH 23/28] rm unnecessary comma --- docs/json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/json.md b/docs/json.md index f6fccd6e6f..4de275a3a8 100644 --- a/docs/json.md +++ b/docs/json.md @@ -619,7 +619,7 @@ Project(name=kotlinx.serialization, language=Kotlin) > 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. +In some cases it might be necessary to encode an arbitrary unquoted value. This can be achieved with [JsonUnquotedLiteral]. #### Serializing large decimal numbers From de14566373cebafc5e470c702009a6d9207ed9e2 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 17:30:17 +0200 Subject: [PATCH 24/28] fix link --- docs/json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/json.md b/docs/json.md index 4de275a3a8..4209699d10 100644 --- a/docs/json.md +++ b/docs/json.md @@ -239,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`: From a03fc104e7edf2e10475a4b1bed3de0617cd4e49 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 17:30:30 +0200 Subject: [PATCH 25/28] fix TOC link (temp fix) --- docs/json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/json.md b/docs/json.md index 4209699d10..c1bcb7da6c 100644 --- a/docs/json.md +++ b/docs/json.md @@ -27,7 +27,7 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json * [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 encode `"null"` is forbidden](#using-jsonunquotedliteral-to-encode-"null"-is-forbidden) + * [Using `JsonUnquotedLiteral` to encode `"null"` is forbidden](#using-jsonunquotedliteral-to-encode-null-is-forbidden) * [Json transformations](#json-transformations) * [Array wrapping](#array-wrapping) * [Array unwrapping](#array-unwrapping) From 32f6d6457d36269ab6859e3197dedda34ac40814 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 17:32:06 +0200 Subject: [PATCH 26/28] add note about creating a serializer --- docs/json.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/json.md b/docs/json.md index c1bcb7da6c..4115aba4ca 100644 --- a/docs/json.md +++ b/docs/json.md @@ -707,6 +707,10 @@ fun main() { 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 From 74a5e09d64dc0b3e018dbf172af74e67f17a9183 Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 18:12:21 +0200 Subject: [PATCH 27/28] gramma fix, and fix link --- docs/json.md | 6 +++--- docs/serialization-guide.md | 2 +- .../json/serializers/JsonUnquotedLiteralTest.kt | 2 +- .../src/kotlinx/serialization/json/JsonElement.kt | 2 +- guide/test/JsonTest.kt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/json.md b/docs/json.md index 4115aba4ca..b76edf3501 100644 --- a/docs/json.md +++ b/docs/json.md @@ -27,7 +27,7 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json * [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 encode `"null"` is forbidden](#using-jsonunquotedliteral-to-encode-null-is-forbidden) + * [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) @@ -741,7 +741,7 @@ The exact value of `pi` is decoded, with all 30 decimal places of precision that -#### Using `JsonUnquotedLiteral` to encode `"null"` is forbidden +#### 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. @@ -756,7 +756,7 @@ fun main() { > You can get the full code [here](../guide/example/example-json-19.kt). ```text -Exception in thread "main" kotlinx.serialization.json.internal.JsonEncodingException: Creating an literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive +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 ``` diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index 507109b28f..7a8a8ccebf 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -125,7 +125,7 @@ Once the project is set up, we can start serializing some classes. * [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 encode `"null"` is forbidden](json.md#using-jsonunquotedliteral-to-encode-"null"-is-forbidden) + * [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/JsonUnquotedLiteralTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt index 3f6f585990..e8090044cb 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonUnquotedLiteralTest.kt @@ -97,7 +97,7 @@ class JsonUnquotedLiteralTest : JsonTestBase() { fun test(block: () -> Unit) { assertFailsWithSerialMessage( exceptionName = "JsonEncodingException", - message = "Creating an literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive", + 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, ) } diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index 79f769cea2..634c447949 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -103,7 +103,7 @@ public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull public fun JsonUnquotedLiteral(value: String?): JsonPrimitive { return when (value) { null -> JsonNull - JsonNull.content -> throw JsonEncodingException("Creating an literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive") + 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) } } diff --git a/guide/test/JsonTest.kt b/guide/test/JsonTest.kt index 02684fac42..a38539e8d3 100644 --- a/guide/test/JsonTest.kt +++ b/guide/test/JsonTest.kt @@ -146,7 +146,7 @@ class JsonTest { @Test fun testExampleJson19() { captureOutput("ExampleJson19") { example.exampleJson19.main() }.verifyOutputLinesStart( - "Exception in thread \"main\" kotlinx.serialization.json.internal.JsonEncodingException: Creating an literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive" + "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" ) } From 4c2872b8e7c6ad5a6a48ddfdd54cc1efd2b2315d Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Thu, 20 Oct 2022 18:13:43 +0200 Subject: [PATCH 28/28] fix link --- docs/json.md | 4 ++-- docs/serialization-guide.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/json.md b/docs/json.md index b76edf3501..e2006a66fe 100644 --- a/docs/json.md +++ b/docs/json.md @@ -27,7 +27,7 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json * [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) + * [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) @@ -741,7 +741,7 @@ The exact value of `pi` is decoded, with all 30 decimal places of precision that -#### Using `JsonUnquotedLiteral` to create a literal unquoted value of 'null' is forbidden +#### 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. diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index 7a8a8ccebf..2c9751d557 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -125,7 +125,7 @@ Once the project is set up, we can start serializing some classes. * [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) + * [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)