diff options
author | aSemy <897017+aSemy@users.noreply.github.com> | 2022-10-21 16:05:55 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-21 17:05:55 +0300 |
commit | 46a5ff60b21b85f0a1d98c66f4d077e86e405ea6 (patch) | |
tree | 67ff28ce541849e53501dd50665d11dba1999483 /formats/json | |
parent | a7cee0b7ad9099a28a674af9797ecc6c5df111f1 (diff) | |
download | kotlinx.serialization-46a5ff60b21b85f0a1d98c66f4d077e86e405ea6.tar.gz |
Support unquoted literal JSON values (#2041)
This PR provides a new function for encoding raw JSON content, without quoting it as a string. This allows for encoding JSON numbers of any size or precision, so BigDecimal and BigInteger can be supported.
Fixes #1051
Fixes #1405
The implementation is similar to how unsigned numbers are handled.
JsonUnquotedLiteral() is a new function that allows creating literal JSON content.
Added val coerceToInlineType to JsonLiteral, so that JsonUnquotedLiteral could use encodeInline()
Defined val jsonUnquotedLiteralDescriptor as a 'marker', for use with encodeInline()
ComposerForUnquotedLiterals (based on ComposerForUnsignedNumbers) will 'override' the encoder when a JsonLiteral has the jsonUnquotedLiteralDescriptor marker, and will encode the content as a string without surrounding quotes.
Diffstat (limited to 'formats/json')
6 files changed, 87 insertions, 14 deletions
diff --git a/formats/json/api/kotlinx-serialization-json.api b/formats/json/api/kotlinx-serialization-json.api index 7f2f69b3..24aaf10f 100644 --- a/formats/json/api/kotlinx-serialization-json.api +++ b/formats/json/api/kotlinx-serialization-json.api @@ -187,6 +187,7 @@ public final class kotlinx/serialization/json/JsonElementKt { public static final fun JsonPrimitive (Ljava/lang/Number;)Lkotlinx/serialization/json/JsonPrimitive; public static final fun JsonPrimitive (Ljava/lang/String;)Lkotlinx/serialization/json/JsonPrimitive; public static final fun JsonPrimitive (Ljava/lang/Void;)Lkotlinx/serialization/json/JsonNull; + public static final fun JsonUnquotedLiteral (Ljava/lang/String;)Lkotlinx/serialization/json/JsonPrimitive; public static final fun getBoolean (Lkotlinx/serialization/json/JsonPrimitive;)Z public static final fun getBooleanOrNull (Lkotlinx/serialization/json/JsonPrimitive;)Ljava/lang/Boolean; public static final fun getContentOrNull (Lkotlinx/serialization/json/JsonPrimitive;)Ljava/lang/String; diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index abfc567a..634c4479 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -7,7 +7,11 @@ package kotlinx.serialization.json import kotlinx.serialization.* +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.internal.InlinePrimitiveDescriptor import kotlinx.serialization.json.internal.* +import kotlin.native.concurrent.SharedImmutable /** * Class representing single JSON element. @@ -76,13 +80,52 @@ public fun JsonPrimitive(value: String?): JsonPrimitive { @Suppress("FunctionName", "UNUSED_PARAMETER") // allows to call `JsonPrimitive(null)` public fun JsonPrimitive(value: Nothing?): JsonNull = JsonNull +/** + * Creates a [JsonPrimitive] from the given string, without surrounding it in quotes. + * + * This function is provided for encoding raw JSON values that cannot be encoded using the [JsonPrimitive] functions. + * For example, + * + * * precise numeric values (avoiding floating-point precision errors associated with [Double] and [Float]), + * * large numbers, + * * or complex JSON objects. + * + * Be aware that it is possible to create invalid JSON using this function. + * + * Creating a literal unquoted value of `null` (as in, `value == "null"`) is forbidden. If you want to create + * JSON null literal, use [JsonNull] object, otherwise, use [JsonPrimitive]. + * + * @see JsonPrimitive is the preferred method for encoding JSON primitives. + * @throws JsonEncodingException if `value == "null"` + */ +@ExperimentalSerializationApi +@Suppress("FunctionName") +public fun JsonUnquotedLiteral(value: String?): JsonPrimitive { + return when (value) { + null -> JsonNull + JsonNull.content -> throw JsonEncodingException("Creating a literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive") + else -> JsonLiteral(value, isString = false, coerceToInlineType = jsonUnquotedLiteralDescriptor) + } +} + +/** Used as a marker to indicate during encoding that the [JsonEncoder] should use `encodeInline()` */ +@SharedImmutable +internal val jsonUnquotedLiteralDescriptor: SerialDescriptor = + InlinePrimitiveDescriptor("kotlinx.serialization.json.JsonUnquotedLiteral", String.serializer()) + + // JsonLiteral is deprecated for public use and no longer available. Please use JsonPrimitive instead internal class JsonLiteral internal constructor( body: Any, - public override val isString: Boolean + public override val isString: Boolean, + internal val coerceToInlineType: SerialDescriptor? = null, ) : JsonPrimitive() { public override val content: String = body.toString() + init { + if (coerceToInlineType != null) require(coerceToInlineType.isInline) + } + public override fun toString(): String = if (isString) buildString { printQuoted(content) } else content @@ -121,7 +164,9 @@ public object JsonNull : JsonPrimitive() { * traditional methods like [Map.get] or [Map.getValue] to obtain Json elements. */ @Serializable(JsonObjectSerializer::class) -public class JsonObject(private val content: Map<String, JsonElement>) : JsonElement(), Map<String, JsonElement> by content { +public class JsonObject( + private val content: Map<String, JsonElement> +) : JsonElement(), Map<String, JsonElement> by content { public override fun equals(other: Any?): Boolean = content == other public override fun hashCode(): Int = content.hashCode() public override fun toString(): String { @@ -229,7 +274,8 @@ public val JsonPrimitive.floatOrNull: Float? get() = content.toFloatOrNull() * Returns content of current element as boolean * @throws IllegalStateException if current element doesn't represent boolean */ -public val JsonPrimitive.boolean: Boolean get() = content.toBooleanStrictOrNull() ?: throw IllegalStateException("$this does not represent a Boolean") +public val JsonPrimitive.boolean: Boolean + get() = content.toBooleanStrictOrNull() ?: throw IllegalStateException("$this does not represent a Boolean") /** * Returns content of current element as boolean or `null` if current element is not a valid representation of boolean diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt index 6fcfa2c0..788ce93f 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<JsonLiteral> { 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 113a8296..d5841552 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt @@ -38,7 +38,7 @@ internal open class Composer(@JvmField internal val writer: JsonWriter) { open fun print(v: Int) = writer.writeLong(v.toLong()) open fun print(v: Long) = writer.writeLong(v) open fun print(v: Boolean) = writer.write(v.toString()) - fun printQuoted(value: String) = writer.writeQuoted(value) + open fun printQuoted(value: String) = writer.writeQuoted(value) } @SuppressAnimalSniffer // Long(Integer).toUnsignedString(long) @@ -60,6 +60,13 @@ internal class ComposerForUnsignedNumbers(writer: JsonWriter, private val forceQ } } +@SuppressAnimalSniffer +internal class ComposerForUnquotedLiterals(writer: JsonWriter, private val forceQuoting: Boolean) : Composer(writer) { + override fun printQuoted(value: String) { + if (forceQuoting) super.printQuoted(value) else super.print(value) + } +} + internal class ComposerWithPrettyPrint( writer: JsonWriter, private val json: Json diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt index dd7682fe..bc954ce9 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt @@ -23,6 +23,9 @@ private val unsignedNumberDescriptors = setOf( internal val SerialDescriptor.isUnsignedNumber: Boolean get() = this.isInline && this in unsignedNumberDescriptors +internal val SerialDescriptor.isUnquotedLiteral: Boolean + get() = this.isInline && this == jsonUnquotedLiteralDescriptor + @OptIn(ExperimentalSerializationApi::class) internal class StreamingJsonEncoder( private val composer: Composer, @@ -156,17 +159,18 @@ internal class StreamingJsonEncoder( } override fun encodeInline(descriptor: SerialDescriptor): Encoder = - if (descriptor.isUnsignedNumber) StreamingJsonEncoder( - composerForUnsignedNumbers(), json, mode, null - ) - else super.encodeInline(descriptor) + when { + descriptor.isUnsignedNumber -> StreamingJsonEncoder(composerAs(::ComposerForUnsignedNumbers), json, mode, null) + descriptor.isUnquotedLiteral -> StreamingJsonEncoder(composerAs(::ComposerForUnquotedLiterals), json, mode, null) + else -> super.encodeInline(descriptor) + } - private fun composerForUnsignedNumbers(): Composer { + private inline fun <reified T: Composer> 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 b33c88ee..643e158e 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt @@ -102,9 +102,15 @@ private sealed class AbstractJsonTreeEncoder( putElement(tag, JsonPrimitive(value.toString())) } - @SuppressAnimalSniffer // Long(Integer).toUnsignedString(long) override fun encodeTaggedInline(tag: String, inlineDescriptor: SerialDescriptor): Encoder = - if (inlineDescriptor.isUnsignedNumber) object : AbstractEncoder() { + when { + inlineDescriptor.isUnsignedNumber -> inlineUnsignedNumberEncoder(tag) + inlineDescriptor.isUnquotedLiteral -> inlineUnquotedLiteralEncoder(tag, inlineDescriptor) + else -> super.encodeTaggedInline(tag, inlineDescriptor) + } + + @SuppressAnimalSniffer // Long(Integer).toUnsignedString(long) + private fun inlineUnsignedNumberEncoder(tag: String) = object : AbstractEncoder() { override val serializersModule: SerializersModule = json.serializersModule fun putUnquotedString(s: String) = putElement(tag, JsonLiteral(s, isString = false)) @@ -113,7 +119,12 @@ private sealed class AbstractJsonTreeEncoder( override fun encodeByte(value: Byte) = putUnquotedString(value.toUByte().toString()) override fun encodeShort(value: Short) = putUnquotedString(value.toUShort().toString()) } - else super.encodeTaggedInline(tag, inlineDescriptor) + + private fun inlineUnquotedLiteralEncoder(tag: String, inlineDescriptor: SerialDescriptor) = object : AbstractEncoder() { + override val serializersModule: SerializersModule get() = json.serializersModule + + override fun encodeString(value: String) = putElement(tag, JsonLiteral(value, isString = false, coerceToInlineType = inlineDescriptor)) + } override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { val consumer = |