Skip to content

Commit 46a5ff6

Browse files
authored
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.
1 parent a7cee0b commit 46a5ff6

File tree

25 files changed

+889
-181
lines changed

25 files changed

+889
-181
lines changed

core/api/kotlinx-serialization-core.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,10 @@ public final class kotlinx/serialization/internal/InlineClassDescriptor : kotlin
774774
public fun isInline ()Z
775775
}
776776

777+
public final class kotlinx/serialization/internal/InlineClassDescriptorKt {
778+
public static final fun InlinePrimitiveDescriptor (Ljava/lang/String;Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/descriptors/SerialDescriptor;
779+
}
780+
777781
public final class kotlinx/serialization/internal/IntArrayBuilder : kotlinx/serialization/internal/PrimitiveArrayBuilder {
778782
public synthetic fun build$kotlinx_serialization_core ()Ljava/lang/Object;
779783
}

core/commonMain/src/kotlinx/serialization/internal/InlineClassDescriptor.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ internal class InlineClassDescriptor(
2525
}
2626
}
2727

28-
internal fun <T> InlinePrimitiveDescriptor(name: String, primitiveSerializer: KSerializer<T>): SerialDescriptor =
28+
@InternalSerializationApi
29+
public fun <T> InlinePrimitiveDescriptor(name: String, primitiveSerializer: KSerializer<T>): SerialDescriptor =
2930
InlineClassDescriptor(name, object : GeneratedSerializer<T> {
3031
// object needed only to pass childSerializers()
3132
override fun childSerializers(): Array<KSerializer<*>> = arrayOf(primitiveSerializer)

docs/json.md

Lines changed: 161 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json
2525
* [Types of Json elements](#types-of-json-elements)
2626
* [Json element builders](#json-element-builders)
2727
* [Decoding Json elements](#decoding-json-elements)
28+
* [Encoding literal Json content (experimental)](#encoding-literal-json-content-experimental)
29+
* [Serializing large decimal numbers](#serializing-large-decimal-numbers)
30+
* [Using `JsonUnquotedLiteral` to create a literal unquoted value of `null` is forbidden](#using-jsonunquotedliteral-to-create-a-literal-unquoted-value-of-null-is-forbidden)
2831
* [Json transformations](#json-transformations)
2932
* [Array wrapping](#array-wrapping)
3033
* [Array unwrapping](#array-unwrapping)
@@ -236,7 +239,7 @@ Project(name=kotlinx.serialization, language=Kotlin)
236239
### Encoding defaults
237240

238241
Default values of properties are not encoded by default because they will be assigned to missing fields during decoding anyway.
239-
See the [Defaults are not encoded](basic-serialization.md#defaults-are-not-encoded) section for details and an example.
242+
See the [Defaults are not encoded](basic-serialization.md#defaults-are-not-encoded-by-default) section for details and an example.
240243
This is especially useful for nullable properties with null defaults and avoids writing the corresponding null values.
241244
The default behavior can be changed by setting the [encodeDefaults][JsonBuilder.encodeDefaults] property to `true`:
242245

@@ -612,6 +615,153 @@ Project(name=kotlinx.serialization, language=Kotlin)
612615

613616
<!--- TEST -->
614617

618+
### Encoding literal Json content (experimental)
619+
620+
> This functionality is experimental and requires opting-in to [the experimental Kotlinx Serialization API](compatibility.md#experimental-api).
621+
622+
In some cases it might be necessary to encode an arbitrary unquoted value.
623+
This can be achieved with [JsonUnquotedLiteral].
624+
625+
#### Serializing large decimal numbers
626+
627+
The JSON specification does not restrict the size or precision of numbers, however it is not possible to serialize
628+
numbers of arbitrary size or precision using [JsonPrimitive()].
629+
630+
If [Double] is used, then the numbers are limited in precision, meaning that large numbers are truncated.
631+
When using Kotlin/JVM [BigDecimal] can be used instead, but [JsonPrimitive()] will encode the value as a string, not a
632+
number.
633+
634+
```kotlin
635+
import java.math.BigDecimal
636+
637+
val format = Json { prettyPrint = true }
638+
639+
fun main() {
640+
val pi = BigDecimal("3.141592653589793238462643383279")
641+
642+
val piJsonDouble = JsonPrimitive(pi.toDouble())
643+
val piJsonString = JsonPrimitive(pi.toString())
644+
645+
val piObject = buildJsonObject {
646+
put("pi_double", piJsonDouble)
647+
put("pi_string", piJsonString)
648+
}
649+
650+
println(format.encodeToString(piObject))
651+
}
652+
```
653+
654+
> You can get the full code [here](../guide/example/example-json-16.kt).
655+
656+
Even though `pi` was defined as a number with 30 decimal places, the resulting JSON does not reflect this.
657+
The [Double] value is truncated to 15 decimal places, and the String is wrapped in quotes - which is not a JSON number.
658+
659+
```text
660+
{
661+
"pi_double": 3.141592653589793,
662+
"pi_string": "3.141592653589793238462643383279"
663+
}
664+
```
665+
666+
<!--- TEST -->
667+
668+
To avoid precision loss, the string value of `pi` can be encoded using [JsonUnquotedLiteral].
669+
670+
```kotlin
671+
import java.math.BigDecimal
672+
673+
val format = Json { prettyPrint = true }
674+
675+
fun main() {
676+
val pi = BigDecimal("3.141592653589793238462643383279")
677+
678+
// use JsonUnquotedLiteral to encode raw JSON content
679+
val piJsonLiteral = JsonUnquotedLiteral(pi.toString())
680+
681+
val piJsonDouble = JsonPrimitive(pi.toDouble())
682+
val piJsonString = JsonPrimitive(pi.toString())
683+
684+
val piObject = buildJsonObject {
685+
put("pi_literal", piJsonLiteral)
686+
put("pi_double", piJsonDouble)
687+
put("pi_string", piJsonString)
688+
}
689+
690+
println(format.encodeToString(piObject))
691+
}
692+
```
693+
694+
> You can get the full code [here](../guide/example/example-json-17.kt).
695+
696+
`pi_literal` now accurately matches the value defined.
697+
698+
```text
699+
{
700+
"pi_literal": 3.141592653589793238462643383279,
701+
"pi_double": 3.141592653589793,
702+
"pi_string": "3.141592653589793238462643383279"
703+
}
704+
```
705+
706+
<!--- TEST -->
707+
708+
To decode `pi` back to a [BigDecimal], the string content of the [JsonPrimitive] can be used.
709+
710+
(This demonstration uses a [JsonPrimitive] for simplicity. For a more re-usable method of handling serialization, see
711+
[Json Transformations](#json-transformations) below.)
712+
713+
714+
```kotlin
715+
import java.math.BigDecimal
716+
717+
fun main() {
718+
val piObjectJson = """
719+
{
720+
"pi_literal": 3.141592653589793238462643383279
721+
}
722+
""".trimIndent()
723+
724+
val piObject: JsonObject = Json.decodeFromString(piObjectJson)
725+
726+
val piJsonLiteral = piObject["pi_literal"]!!.jsonPrimitive.content
727+
728+
val pi = BigDecimal(piJsonLiteral)
729+
730+
println(pi)
731+
}
732+
```
733+
734+
> You can get the full code [here](../guide/example/example-json-18.kt).
735+
736+
The exact value of `pi` is decoded, with all 30 decimal places of precision that were in the source JSON.
737+
738+
```text
739+
3.141592653589793238462643383279
740+
```
741+
742+
<!--- TEST -->
743+
744+
#### Using `JsonUnquotedLiteral` to create a literal unquoted value of `null` is forbidden
745+
746+
To avoid creating an inconsistent state, encoding a String equal to `"null"` is forbidden.
747+
Use [JsonNull] or [JsonPrimitive] instead.
748+
749+
```kotlin
750+
fun main() {
751+
// caution: creating null with JsonUnquotedLiteral will cause an exception!
752+
JsonUnquotedLiteral("null")
753+
}
754+
```
755+
756+
> You can get the full code [here](../guide/example/example-json-19.kt).
757+
758+
```text
759+
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
760+
```
761+
762+
<!--- TEST LINES_START -->
763+
764+
615765
## Json transformations
616766

617767
To affect the shape and contents of JSON output after serialization, or adapt input to deserialization,
@@ -679,7 +829,7 @@ fun main() {
679829
}
680830
```
681831

682-
> You can get the full code [here](../guide/example/example-json-16.kt).
832+
> You can get the full code [here](../guide/example/example-json-20.kt).
683833
684834
The output shows that both cases are correctly deserialized into a Kotlin [List].
685835

@@ -731,7 +881,7 @@ fun main() {
731881
}
732882
```
733883

734-
> You can get the full code [here](../guide/example/example-json-17.kt).
884+
> You can get the full code [here](../guide/example/example-json-21.kt).
735885
736886
You end up with a single JSON object, not an array with one element:
737887

@@ -776,7 +926,7 @@ fun main() {
776926
}
777927
```
778928

779-
> You can get the full code [here](../guide/example/example-json-18.kt).
929+
> You can get the full code [here](../guide/example/example-json-22.kt).
780930
781931
See the effect of the custom serializer:
782932

@@ -849,7 +999,7 @@ fun main() {
849999
}
8501000
```
8511001

852-
> You can get the full code [here](../guide/example/example-json-19.kt).
1002+
> You can get the full code [here](../guide/example/example-json-23.kt).
8531003
8541004
No class discriminator is added in the JSON output:
8551005

@@ -945,7 +1095,7 @@ fun main() {
9451095
}
9461096
```
9471097

948-
> You can get the full code [here](../guide/example/example-json-20.kt).
1098+
> You can get the full code [here](../guide/example/example-json-24.kt).
9491099
9501100
This gives you fine-grained control on the representation of the `Response` class in the JSON output:
9511101

@@ -1010,7 +1160,7 @@ fun main() {
10101160
}
10111161
```
10121162

1013-
> You can get the full code [here](../guide/example/example-json-21.kt).
1163+
> You can get the full code [here](../guide/example/example-json-25.kt).
10141164
10151165
```text
10161166
UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})
@@ -1025,8 +1175,10 @@ The next chapter covers [Alternative and custom formats (experimental)](formats.
10251175

10261176
<!-- references -->
10271177
[RFC-4627]: https://www.ietf.org/rfc/rfc4627.txt
1178+
[BigDecimal]: https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html
10281179

10291180
<!-- stdlib references -->
1181+
[Double]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/
10301182
[Double.NaN]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double/-na-n.html
10311183
[List]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/
10321184
[Map]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-map/
@@ -1079,6 +1231,8 @@ The next chapter covers [Alternative and custom formats (experimental)](formats.
10791231
[buildJsonArray]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/build-json-array.html
10801232
[buildJsonObject]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/build-json-object.html
10811233
[Json.decodeFromJsonElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/decode-from-json-element.html
1234+
[JsonUnquotedLiteral]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-unquoted-literal.html
1235+
[JsonNull]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-null/index.html
10821236
[JsonTransformingSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-transforming-serializer/index.html
10831237
[Json.encodeToString]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/encode-to-string.html
10841238
[JsonContentPolymorphicSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-content-polymorphic-serializer/index.html

docs/serialization-guide.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ Once the project is set up, we can start serializing some classes.
123123
* <a name='types-of-json-elements'></a>[Types of Json elements](json.md#types-of-json-elements)
124124
* <a name='json-element-builders'></a>[Json element builders](json.md#json-element-builders)
125125
* <a name='decoding-json-elements'></a>[Decoding Json elements](json.md#decoding-json-elements)
126+
* <a name='encoding-literal-json-content-experimental'></a>[Encoding literal Json content (experimental)](json.md#encoding-literal-json-content-experimental)
127+
* <a name='serializing-large-decimal-numbers'></a>[Serializing large decimal numbers](json.md#serializing-large-decimal-numbers)
128+
* <a name='using-jsonunquotedliteral-to-create-a-literal-unquoted-value-of-null-is-forbidden'></a>[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)
126129
* <a name='json-transformations'></a>[Json transformations](json.md#json-transformations)
127130
* <a name='array-wrapping'></a>[Array wrapping](json.md#array-wrapping)
128131
* <a name='array-unwrapping'></a>[Array unwrapping](json.md#array-unwrapping)

formats/json-tests/commonTest/src/kotlinx/serialization/json/serializers/JsonPrimitiveSerializerTest.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,28 @@ class JsonPrimitiveSerializerTest : JsonTestBase() {
4646
assertEquals(JsonPrimitiveWrapper(JsonPrimitive("239")), default.decodeFromString(JsonPrimitiveWrapper.serializer(), string, jsonTestingMode))
4747
}
4848

49+
@Test
50+
fun testJsonUnquotedLiteralNumbers() = parametrizedTest { jsonTestingMode ->
51+
listOf(
52+
"99999999999999999999999999999999999999999999999999999999999999999999999999",
53+
"99999999999999999999999999999999999999.999999999999999999999999999999999999",
54+
"-99999999999999999999999999999999999999999999999999999999999999999999999999",
55+
"-99999999999999999999999999999999999999.999999999999999999999999999999999999",
56+
"2.99792458e8",
57+
"-2.99792458e8",
58+
).forEach { literalNum ->
59+
val literalNumJson = JsonUnquotedLiteral(literalNum)
60+
val wrapper = JsonPrimitiveWrapper(literalNumJson)
61+
val string = default.encodeToString(JsonPrimitiveWrapper.serializer(), wrapper, jsonTestingMode)
62+
assertEquals("{\"primitive\":$literalNum}", string, "mode:$jsonTestingMode")
63+
assertEquals(
64+
JsonPrimitiveWrapper(literalNumJson),
65+
default.decodeFromString(JsonPrimitiveWrapper.serializer(), string, jsonTestingMode),
66+
"mode:$jsonTestingMode",
67+
)
68+
}
69+
}
70+
4971
@Test
5072
fun testTopLevelPrimitive() = parametrizedTest { jsonTestingMode ->
5173
val string = default.encodeToString(JsonPrimitive.serializer(), JsonPrimitive(42), jsonTestingMode)
@@ -76,7 +98,7 @@ class JsonPrimitiveSerializerTest : JsonTestBase() {
7698
}
7799

78100
@Test
79-
fun testJsonLiterals() {
101+
fun testJsonLiterals() {
80102
testLiteral(0L, "0")
81103
testLiteral(0, "0")
82104
testLiteral(0.0, "0.0")

0 commit comments

Comments
 (0)