Skip to content

Commit 6a5ebd5

Browse files
authored
Support quoting unsigned integers when used as map keys (#1969)
Fixes #1966
1 parent 4b64100 commit 6a5ebd5

File tree

4 files changed

+88
-17
lines changed

4 files changed

+88
-17
lines changed

formats/json/commonMain/src/kotlinx/serialization/json/internal/Composers.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/*
2-
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
@file:OptIn(ExperimentalSerializationApi::class)
66
package kotlinx.serialization.json.internal
77

8-
import kotlinx.serialization.ExperimentalSerializationApi
9-
import kotlinx.serialization.json.Json
10-
import kotlin.jvm.JvmField
8+
import kotlinx.serialization.*
9+
import kotlinx.serialization.json.*
10+
import kotlin.jvm.*
1111

1212
internal fun Composer(sb: JsonStringBuilder, json: Json): Composer =
1313
if (json.configuration.prettyPrint) ComposerWithPrettyPrint(sb, json) else Composer(sb)
@@ -42,21 +42,21 @@ internal open class Composer(@JvmField internal val sb: JsonStringBuilder) {
4242
}
4343

4444
@ExperimentalUnsignedTypes
45-
internal class ComposerForUnsignedNumbers(sb: JsonStringBuilder) : Composer(sb) {
45+
internal class ComposerForUnsignedNumbers(sb: JsonStringBuilder, private val forceQuoting: Boolean) : Composer(sb) {
4646
override fun print(v: Int) {
47-
return super.print(v.toUInt().toString())
47+
if (forceQuoting) printQuoted(v.toUInt().toString()) else print(v.toUInt().toString())
4848
}
4949

5050
override fun print(v: Long) {
51-
return super.print(v.toULong().toString())
51+
if (forceQuoting) printQuoted(v.toULong().toString()) else print(v.toULong().toString())
5252
}
5353

5454
override fun print(v: Byte) {
55-
return super.print(v.toUByte().toString())
55+
if (forceQuoting) printQuoted(v.toUByte().toString()) else print(v.toUByte().toString())
5656
}
5757

5858
override fun print(v: Short) {
59-
return super.print(v.toUShort().toString())
59+
if (forceQuoting) printQuoted(v.toUShort().toString()) else print(v.toUShort().toString())
6060
}
6161
}
6262

formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package kotlinx.serialization.json.internal
@@ -160,10 +160,18 @@ internal class StreamingJsonEncoder(
160160

161161
override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder =
162162
if (inlineDescriptor.isUnsignedNumber) StreamingJsonEncoder(
163-
ComposerForUnsignedNumbers(composer.sb), json, mode, null
163+
composerForUnsignedNumbers(), json, mode, null
164164
)
165165
else super.encodeInline(inlineDescriptor)
166166

167+
private fun composerForUnsignedNumbers(): Composer {
168+
// If we're inside encodeInline().encodeSerializableValue, we should preserve the forceQuoting state
169+
// inside the composer, but not in the encoder (otherwise we'll get into `if (forceQuoting) encodeString(value.toString())` part
170+
// and unsigned numbers would be encoded incorrectly)
171+
return if (composer is ComposerForUnsignedNumbers) composer
172+
else ComposerForUnsignedNumbers(composer.sb, forceQuoting)
173+
}
174+
167175
override fun encodeNull() {
168176
composer.print(NULL)
169177
}

formats/json/commonTest/src/kotlinx/serialization/features/inline/EncodeInlineElementTest.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
/*
2-
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
@file:OptIn(ExperimentalUnsignedTypes::class)
6-
/*
7-
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
8-
*/
96

107
package kotlinx.serialization.features.inline
118

129
import kotlinx.serialization.*
1310
import kotlinx.serialization.builtins.*
14-
import kotlinx.serialization.encoding.*
1511
import kotlinx.serialization.descriptors.*
12+
import kotlinx.serialization.encoding.*
1613
import kotlinx.serialization.test.*
17-
import kotlin.test.Test
14+
import kotlin.test.*
1815

1916
@Serializable(WithUnsignedSerializer::class)
2017
data class WithUnsigned(val u: UInt)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.features.inline
6+
7+
import kotlinx.serialization.*
8+
import kotlinx.serialization.builtins.*
9+
import kotlinx.serialization.descriptors.*
10+
import kotlinx.serialization.encoding.*
11+
import kotlinx.serialization.json.*
12+
import kotlinx.serialization.test.*
13+
import kotlin.jvm.*
14+
import kotlin.test.*
15+
16+
class InlineMapQuotedTest : JsonTestBase() {
17+
@Serializable(with = CustomULong.Serializer::class)
18+
data class CustomULong(val value: ULong) {
19+
@OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class)
20+
internal object Serializer : KSerializer<CustomULong> {
21+
override val descriptor: SerialDescriptor =
22+
@OptIn(ExperimentalUnsignedTypes::class) ULong.serializer().descriptor
23+
24+
override fun deserialize(decoder: Decoder): CustomULong =
25+
CustomULong(decoder.decodeInline(descriptor).decodeSerializableValue(ULong.serializer()))
26+
27+
override fun serialize(encoder: Encoder, value: CustomULong) {
28+
encoder.encodeInline(descriptor).encodeSerializableValue(ULong.serializer(), value.value)
29+
}
30+
}
31+
}
32+
33+
@JvmInline
34+
@Serializable
35+
value class WrappedLong(val value: Long)
36+
37+
@JvmInline
38+
@Serializable
39+
value class WrappedULong(val value: ULong)
40+
41+
@Serializable
42+
data class Carrier(
43+
val mapLong: Map<Long, Long>,
44+
val mapULong: Map<ULong, Long>,
45+
val wrappedLong: Map<WrappedLong, Long>,
46+
val mapWrappedU: Map<WrappedULong, Long>,
47+
val mapCustom: Map<CustomULong, Long>
48+
)
49+
50+
@Test
51+
fun testInlineClassAsMapKey() = noLegacyJs {
52+
println(Long.MAX_VALUE.toULong() + 2UL)
53+
val c = Carrier(
54+
mapOf(1L to 1L),
55+
mapOf(Long.MAX_VALUE.toULong() + 2UL to 2L),
56+
mapOf(WrappedLong(3L) to 3L),
57+
mapOf(WrappedULong(Long.MAX_VALUE.toULong() + 4UL) to 4L),
58+
mapOf(CustomULong(Long.MAX_VALUE.toULong() + 5UL) to 5L)
59+
)
60+
assertJsonFormAndRestored(
61+
serializer(),
62+
c,
63+
"""{"mapLong":{"1":1},"mapULong":{"9223372036854775809":2},"wrappedLong":{"3":3},"mapWrappedU":{"9223372036854775811":4},"mapCustom":{"9223372036854775812":5}}"""
64+
)
65+
}
66+
}

0 commit comments

Comments
 (0)