summaryrefslogtreecommitdiff
path: root/formats/json
diff options
context:
space:
mode:
authoraSemy <897017+aSemy@users.noreply.github.com>2022-10-21 16:05:55 +0200
committerGitHub <noreply@github.com>2022-10-21 17:05:55 +0300
commit46a5ff60b21b85f0a1d98c66f4d077e86e405ea6 (patch)
tree67ff28ce541849e53501dd50665d11dba1999483 /formats/json
parenta7cee0b7ad9099a28a674af9797ecc6c5df111f1 (diff)
downloadkotlinx.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')
-rw-r--r--formats/json/api/kotlinx-serialization-json.api1
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt52
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt4
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt9
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt18
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/TreeJsonEncoder.kt17
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 =