Skip to content

Commit ff8fd99

Browse files
committed
implement JsonRawElement, for encoding literal JSON strings
1 parent dc9983a commit ff8fd99

File tree

11 files changed

+425
-13
lines changed

11 files changed

+425
-13
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)

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

Lines changed: 22 additions & 0 deletions
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 testJsonRawElementNumbers() = 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 = JsonRawElement(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)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package kotlinx.serialization.json.serializers
2+
3+
import kotlinx.serialization.builtins.MapSerializer
4+
import kotlinx.serialization.json.*
5+
import kotlinx.serialization.test.assertFailsWithSerialMessage
6+
import kotlin.test.Test
7+
import kotlin.test.assertEquals
8+
9+
class JsonRawElementTest : JsonTestBase() {
10+
11+
private fun assertRawJsonValueEncoded(inputValue: String) = parametrizedTest { mode ->
12+
val rawElement = JsonRawElement(inputValue)
13+
14+
assertEquals(inputValue, rawElement.toString(), "expect JsonElement.toString() returns the raw input value")
15+
assertEquals(inputValue, default.encodeToString(JsonElement.serializer(), rawElement, mode))
16+
}
17+
18+
@Test
19+
fun testRawJsonNumbers() {
20+
assertRawJsonValueEncoded("1")
21+
assertRawJsonValueEncoded("-1")
22+
assertRawJsonValueEncoded("100.0")
23+
assertRawJsonValueEncoded("-100.0")
24+
25+
assertRawJsonValueEncoded("9999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999")
26+
assertRawJsonValueEncoded("-9999999999999999999999999999999999999999999999999999999.9999999999999999999999999999999999999999999999999999999")
27+
28+
assertRawJsonValueEncoded("99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999")
29+
assertRawJsonValueEncoded("-99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999")
30+
31+
assertRawJsonValueEncoded("2.99792458e8")
32+
assertRawJsonValueEncoded("-2.99792458e8")
33+
34+
assertRawJsonValueEncoded("2.99792458E8")
35+
assertRawJsonValueEncoded("-2.99792458E8")
36+
37+
assertRawJsonValueEncoded("11.399999999999")
38+
assertRawJsonValueEncoded("0.30000000000000004")
39+
assertRawJsonValueEncoded("0.1000000000000000055511151231257827021181583404541015625")
40+
}
41+
42+
@Test
43+
fun testRawJsonWhitespaceStrings() {
44+
assertRawJsonValueEncoded("")
45+
assertRawJsonValueEncoded(" ")
46+
assertRawJsonValueEncoded("\t")
47+
assertRawJsonValueEncoded("\t\t\t")
48+
assertRawJsonValueEncoded("\r\n")
49+
assertRawJsonValueEncoded("\n")
50+
assertRawJsonValueEncoded("\n\n\n")
51+
}
52+
53+
@Test
54+
fun testRawJsonStrings() {
55+
assertRawJsonValueEncoded("lorem")
56+
assertRawJsonValueEncoded(""""lorem"""")
57+
assertRawJsonValueEncoded(
58+
"""
59+
Well, my name is Freddy Kreuger
60+
I've got the Elm Street blues
61+
I've got a hand like a knife rack
62+
And I die in every film!
63+
""".trimIndent()
64+
)
65+
}
66+
67+
@Test
68+
fun testRawJsonObjects() {
69+
assertRawJsonValueEncoded("""{"some":"json"}""")
70+
assertRawJsonValueEncoded("""{"some":"json","object":true,"count":1,"array":[1,2.0,-333,"4",boolean]}""")
71+
}
72+
73+
@Test
74+
fun testRawJsonArrays() {
75+
assertRawJsonValueEncoded("""[1,2,3]""")
76+
assertRawJsonValueEncoded("""["a","b","c"]""")
77+
assertRawJsonValueEncoded("""[true,false]""")
78+
assertRawJsonValueEncoded("""[1,2.0,-333,"4",boolean]""")
79+
assertRawJsonValueEncoded("""[{"some":"json","object":true,"count":1,"array":[1,2.0,-333,"4",boolean]}]""")
80+
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]}]""")
81+
}
82+
83+
@Test
84+
fun testRawJsonNull() {
85+
assertEquals(JsonNull, JsonRawElement(null))
86+
}
87+
88+
@Test
89+
fun testRawJsonNullString() {
90+
fun test(block: () -> Unit) {
91+
assertFailsWithSerialMessage(
92+
"JsonEncodingException",
93+
"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",
94+
block = block,
95+
)
96+
}
97+
98+
test { JsonRawElement("null") }
99+
test { JsonRawElement(JsonNull.content) }
100+
}
101+
102+
@Test
103+
fun testRawJsonInvalidMapKeyIsEscaped() {
104+
val mapSerializer = MapSerializer(
105+
JsonPrimitive.serializer(),
106+
JsonPrimitive.serializer(),
107+
)
108+
109+
fun test(expected: String, input: String) = parametrizedTest { mode ->
110+
val data = mapOf(JsonRawElement(input) to JsonPrimitive("invalid key"))
111+
112+
assertEquals(
113+
""" {"$expected":"invalid key"} """.trim(),
114+
default.encodeToString(mapSerializer, data, mode),
115+
)
116+
}
117+
118+
test(" ", " ")
119+
test(
120+
""" \\\"\\\" """.trim(),
121+
""" \"\" """.trim(),
122+
)
123+
test(
124+
""" \\\\\\\" """.trim(),
125+
""" \\\" """.trim(),
126+
)
127+
test(
128+
""" {\\\"I'm not a valid JSON object key\\\"} """.trim(),
129+
""" {\"I'm not a valid JSON object key\"} """.trim(),
130+
)
131+
}
132+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package kotlinx.serialization
2+
3+
import kotlinx.serialization.builtins.ListSerializer
4+
import kotlinx.serialization.builtins.MapSerializer
5+
import kotlinx.serialization.descriptors.PrimitiveKind
6+
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
7+
import kotlinx.serialization.encoding.Decoder
8+
import kotlinx.serialization.encoding.Encoder
9+
import kotlinx.serialization.json.*
10+
import org.junit.Test
11+
import java.math.BigDecimal
12+
13+
private typealias BigDecimalKxs = @Serializable(with = BigDecimalNumericSerializer::class) BigDecimal
14+
15+
class BigDecimalTest : JsonTestBase() {
16+
17+
private val json = Json {
18+
prettyPrint = true
19+
}
20+
21+
private inline fun <reified T> assertBigDecimalJsonFormAndRestored(
22+
expected: String,
23+
actual: T,
24+
serializer: KSerializer<T> = serializer(),
25+
) = assertJsonFormAndRestored(
26+
serializer,
27+
actual,
28+
expected,
29+
json
30+
)
31+
32+
@Test
33+
fun bigDecimal() {
34+
fun test(expected: String, actual: BigDecimal) =
35+
assertBigDecimalJsonFormAndRestored(expected, actual, BigDecimalNumericSerializer)
36+
37+
test("0", BigDecimal.ZERO)
38+
test("1", BigDecimal.ONE)
39+
test("-1", BigDecimal("-1"))
40+
test("10", BigDecimal.TEN)
41+
test(bdExpected1, bdActual1)
42+
test(bdExpected2, bdActual2)
43+
test(bdExpected3, bdActual3)
44+
test(bdExpected4, bdActual4)
45+
test(bdExpected5, bdActual5)
46+
test(bdExpected6, bdActual6)
47+
}
48+
49+
@Test
50+
fun bigDecimalList() {
51+
52+
val bdList: List<BigDecimal> = listOf(
53+
bdActual1,
54+
bdActual2,
55+
bdActual3,
56+
bdActual4,
57+
bdActual5,
58+
bdActual6,
59+
)
60+
61+
val expected =
62+
"""
63+
[
64+
$bdExpected1,
65+
$bdExpected2,
66+
$bdExpected3,
67+
$bdExpected4,
68+
$bdExpected5,
69+
$bdExpected6
70+
]
71+
""".trimIndent()
72+
73+
assertJsonFormAndRestored(
74+
ListSerializer(BigDecimalNumericSerializer),
75+
bdList,
76+
expected,
77+
json,
78+
)
79+
}
80+
81+
@Test
82+
fun bigDecimalMap() {
83+
val bdMap: Map<BigDecimal, BigDecimal> = mapOf(
84+
bdActual1 to bdActual2,
85+
bdActual3 to bdActual4,
86+
bdActual5 to bdActual6,
87+
)
88+
89+
val expected =
90+
"""
91+
{
92+
"$bdExpected1": $bdExpected2,
93+
"$bdExpected3": $bdExpected4,
94+
"$bdExpected5": $bdExpected6
95+
}
96+
""".trimIndent()
97+
98+
assertJsonFormAndRestored(
99+
MapSerializer(BigDecimalNumericSerializer, BigDecimalNumericSerializer),
100+
bdMap,
101+
expected,
102+
json,
103+
)
104+
}
105+
106+
@Test
107+
fun bigDecimalHolder() {
108+
val bdHolder = BigDecimalHolder(
109+
bd = bdActual1,
110+
bdList = listOf(
111+
bdActual1,
112+
bdActual2,
113+
bdActual3,
114+
),
115+
bdMap = mapOf(
116+
bdActual1 to bdActual2,
117+
bdActual3 to bdActual4,
118+
bdActual5 to bdActual6,
119+
),
120+
)
121+
122+
val expected =
123+
"""
124+
{
125+
"bd": $bdExpected1,
126+
"bdList": [
127+
$bdExpected1,
128+
$bdExpected2,
129+
$bdExpected3
130+
],
131+
"bdMap": {
132+
"$bdExpected1": $bdExpected2,
133+
"$bdExpected3": $bdExpected4,
134+
"$bdExpected5": $bdExpected6
135+
}
136+
}
137+
""".trimIndent()
138+
139+
assertBigDecimalJsonFormAndRestored(
140+
expected,
141+
bdHolder,
142+
)
143+
}
144+
145+
companion object {
146+
147+
// test data
148+
private val bdActual1 = BigDecimal("725345854747326287606413621318.311864440287151714280387858224")
149+
private val bdActual2 = BigDecimal("336052472523017262165484244513.836582112201211216526831524328")
150+
private val bdActual3 = BigDecimal("211054843014778386028147282517.011200287614476453868782405400")
151+
private val bdActual4 = BigDecimal("364751025728628060231208776573.207325218263752602211531367642")
152+
private val bdActual5 = BigDecimal("508257556021513833656664177125.824502734715222686411316853148")
153+
private val bdActual6 = BigDecimal("127134584027580606401102614002.366672301517071543257300444000")
154+
155+
private const val bdExpected1 = "725345854747326287606413621318.311864440287151714280387858224"
156+
private const val bdExpected2 = "336052472523017262165484244513.836582112201211216526831524328"
157+
private const val bdExpected3 = "211054843014778386028147282517.011200287614476453868782405400"
158+
private const val bdExpected4 = "364751025728628060231208776573.207325218263752602211531367642"
159+
private const val bdExpected5 = "508257556021513833656664177125.824502734715222686411316853148"
160+
private const val bdExpected6 = "127134584027580606401102614002.366672301517071543257300444000"
161+
}
162+
163+
}
164+
165+
@Serializable
166+
private data class BigDecimalHolder(
167+
val bd: BigDecimalKxs,
168+
val bdList: List<BigDecimalKxs>,
169+
val bdMap: Map<BigDecimalKxs, BigDecimalKxs>,
170+
)
171+
172+
private object BigDecimalNumericSerializer : KSerializer<BigDecimal> {
173+
174+
override val descriptor = PrimitiveSerialDescriptor("java.math.BigDecimal", PrimitiveKind.DOUBLE)
175+
176+
override fun deserialize(decoder: Decoder): BigDecimal {
177+
return if (decoder is JsonDecoder) {
178+
BigDecimal(decoder.decodeJsonElement().jsonPrimitive.content)
179+
} else {
180+
BigDecimal(decoder.decodeString())
181+
}
182+
}
183+
184+
override fun serialize(encoder: Encoder, value: BigDecimal) {
185+
val bdString = value.toPlainString()
186+
187+
if (encoder is JsonEncoder) {
188+
encoder.encodeJsonElement(JsonRawElement(bdString))
189+
} else {
190+
encoder.encodeString(bdString)
191+
}
192+
}
193+
}

formats/json/api/kotlinx-serialization-json.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ public final class kotlinx/serialization/json/JsonElementKt {
187187
public static final fun JsonPrimitive (Ljava/lang/Number;)Lkotlinx/serialization/json/JsonPrimitive;
188188
public static final fun JsonPrimitive (Ljava/lang/String;)Lkotlinx/serialization/json/JsonPrimitive;
189189
public static final fun JsonPrimitive (Ljava/lang/Void;)Lkotlinx/serialization/json/JsonNull;
190+
public static final fun JsonRawElement (Ljava/lang/String;)Lkotlinx/serialization/json/JsonPrimitive;
190191
public static final fun getBoolean (Lkotlinx/serialization/json/JsonPrimitive;)Z
191192
public static final fun getBooleanOrNull (Lkotlinx/serialization/json/JsonPrimitive;)Ljava/lang/Boolean;
192193
public static final fun getContentOrNull (Lkotlinx/serialization/json/JsonPrimitive;)Ljava/lang/String;

0 commit comments

Comments
 (0)