Skip to content

Commit fc9aef5

Browse files
authored
Fix value class encoding in various corner cases (#2242)
- Value class is located at top-level, but wraps non-primitive and thus does not fall in 'primitive on top-level' branch - Value class is a subclass in a polymorphic hierarchy, but either is primitive or explicitly recorded without type info Note that type info is omitted in the latter case and 'can't add type info to primitive' error is not thrown deliberately, as there seems to be use-cases for that. Fixes #1774 Fixes #2159
1 parent 5084435 commit fc9aef5

File tree

6 files changed

+126
-9
lines changed

6 files changed

+126
-9
lines changed

core/api/kotlinx-serialization-core.api

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,7 +1067,7 @@ public abstract class kotlinx/serialization/internal/TaggedDecoder : kotlinx/ser
10671067
public final fun decodeEnum (Lkotlinx/serialization/descriptors/SerialDescriptor;)I
10681068
public final fun decodeFloat ()F
10691069
public final fun decodeFloatElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)F
1070-
public final fun decodeInline (Lkotlinx/serialization/descriptors/SerialDescriptor;)Lkotlinx/serialization/encoding/Decoder;
1070+
public fun decodeInline (Lkotlinx/serialization/descriptors/SerialDescriptor;)Lkotlinx/serialization/encoding/Decoder;
10711071
public final fun decodeInlineElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Lkotlinx/serialization/encoding/Decoder;
10721072
public final fun decodeInt ()I
10731073
public final fun decodeIntElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)I
@@ -1123,7 +1123,7 @@ public abstract class kotlinx/serialization/internal/TaggedEncoder : kotlinx/ser
11231123
public final fun encodeEnum (Lkotlinx/serialization/descriptors/SerialDescriptor;I)V
11241124
public final fun encodeFloat (F)V
11251125
public final fun encodeFloatElement (Lkotlinx/serialization/descriptors/SerialDescriptor;IF)V
1126-
public final fun encodeInline (Lkotlinx/serialization/descriptors/SerialDescriptor;)Lkotlinx/serialization/encoding/Encoder;
1126+
public fun encodeInline (Lkotlinx/serialization/descriptors/SerialDescriptor;)Lkotlinx/serialization/encoding/Encoder;
11271127
public final fun encodeInlineElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Lkotlinx/serialization/encoding/Encoder;
11281128
public final fun encodeInt (I)V
11291129
public final fun encodeIntElement (Lkotlinx/serialization/descriptors/SerialDescriptor;II)V

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public abstract class TaggedEncoder<Tag : Any?> : Encoder, CompositeEncoder {
5151
protected open fun encodeTaggedInline(tag: Tag, inlineDescriptor: SerialDescriptor): Encoder =
5252
this.apply { pushTag(tag) }
5353

54-
final override fun encodeInline(descriptor: SerialDescriptor): Encoder =
54+
override fun encodeInline(descriptor: SerialDescriptor): Encoder =
5555
encodeTaggedInline(popTag(), descriptor)
5656

5757
// ---- Implementation of low-level API ----
@@ -209,7 +209,7 @@ public abstract class TaggedDecoder<Tag : Any?> : Decoder, CompositeDecoder {
209209

210210
// ---- Implementation of low-level API ----
211211

212-
final override fun decodeInline(descriptor: SerialDescriptor): Decoder =
212+
override fun decodeInline(descriptor: SerialDescriptor): Decoder =
213213
decodeTaggedInline(popTag(), descriptor)
214214

215215
// TODO this method should be overridden by any sane format that supports top-level nulls

formats/json-tests/commonTest/src/kotlinx/serialization/features/inline/InlineClassesTest.kt

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ data class SimpleContainerForUInt(val i: UInt)
2222
@JvmInline
2323
value class MyUInt(val m: Int)
2424

25-
object MyUIntSerializer: KSerializer<MyUInt> {
25+
object MyUIntSerializer : KSerializer<MyUInt> {
2626
override val descriptor = UInt.serializer().descriptor
2727
override fun serialize(encoder: Encoder, value: MyUInt) {
2828
encoder.encodeInline(descriptor).encodeInt(value.m)
@@ -73,12 +73,45 @@ value class ResourceKind(val kind: SampleEnum)
7373
@Serializable
7474
data class ResourceIdentifier(val id: ResourceId, val type: ResourceType, val type2: ValueWrapper)
7575

76-
@Serializable @JvmInline
76+
@Serializable
77+
@JvmInline
7778
value class ValueWrapper(val wrapped: ResourceType)
7879

80+
@Serializable
81+
@JvmInline
82+
value class Outer(val inner: Inner)
83+
84+
@Serializable
85+
data class Inner(val n: Int)
86+
87+
@Serializable
88+
data class OuterOuter(val outer: Outer)
89+
90+
@Serializable
91+
@JvmInline
92+
value class WithList(val value: List<Int>)
93+
7994
class InlineClassesTest : JsonTestBase() {
8095
private val precedent: UInt = Int.MAX_VALUE.toUInt() + 10.toUInt()
8196

97+
@Test
98+
fun withList() = noLegacyJs {
99+
val withList = WithList(listOf(1, 2, 3))
100+
assertJsonFormAndRestored(WithList.serializer(), withList, """[1,2,3]""")
101+
}
102+
103+
@Test
104+
fun testOuterInner() = noLegacyJs {
105+
val o = Outer(Inner(10))
106+
assertJsonFormAndRestored(Outer.serializer(), o, """{"n":10}""")
107+
}
108+
109+
@Test
110+
fun testOuterOuterInner() = noLegacyJs {
111+
val o = OuterOuter(Outer(Inner(10)))
112+
assertJsonFormAndRestored(OuterOuter.serializer(), o, """{"outer":{"n":10}}""")
113+
}
114+
82115
@Test
83116
fun testTopLevel() = noLegacyJs {
84117
assertJsonFormAndRestored(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2017-2023 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.json.*
9+
import kotlinx.serialization.test.*
10+
import kotlin.jvm.*
11+
import kotlin.test.*
12+
13+
class ValueClassesInSealedHierarchyTest : JsonTestBase() {
14+
@Test
15+
fun testSingle() = noLegacyJs {
16+
val single = "foo"
17+
assertJsonFormAndRestored(
18+
AnyValue.serializer(),
19+
AnyValue.Single(single),
20+
"\"$single\""
21+
)
22+
}
23+
24+
@Test
25+
fun testComplex() = noLegacyJs {
26+
val complexJson = """{"id":"1","name":"object"}"""
27+
assertJsonFormAndRestored(
28+
AnyValue.serializer(),
29+
AnyValue.Complex(mapOf("id" to "1", "name" to "object")),
30+
complexJson
31+
)
32+
}
33+
34+
@Test
35+
fun testMulti() = noLegacyJs {
36+
val multiJson = """["list","of","strings"]"""
37+
assertJsonFormAndRestored(AnyValue.serializer(), AnyValue.Multi(listOf("list", "of", "strings")), multiJson)
38+
}
39+
}
40+
41+
42+
// From https://github.com/Kotlin/kotlinx.serialization/issues/2159
43+
@Serializable(with = AnyValue.Companion.Serializer::class)
44+
sealed interface AnyValue {
45+
46+
@JvmInline
47+
@Serializable
48+
value class Single(val value: String) : AnyValue
49+
50+
@JvmInline
51+
@Serializable
52+
value class Multi(val values: List<String>) : AnyValue
53+
54+
@JvmInline
55+
@Serializable
56+
value class Complex(val values: Map<String, String>) : AnyValue
57+
58+
@JvmInline
59+
@Serializable
60+
value class Unknown(val value: JsonElement) : AnyValue
61+
62+
companion object {
63+
object Serializer : JsonContentPolymorphicSerializer<AnyValue>(AnyValue::class) {
64+
65+
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<AnyValue> =
66+
when {
67+
element is JsonArray && element.all { it is JsonPrimitive && it.isString } -> Multi.serializer()
68+
element is JsonObject && element.values.all { it is JsonPrimitive && it.isString } -> Complex.serializer()
69+
element is JsonPrimitive && element.isString -> Single.serializer()
70+
else -> Unknown.serializer()
71+
}
72+
}
73+
}
74+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,14 @@ private sealed class AbstractJsonTreeDecoder(
165165
override fun decodeTaggedInline(tag: String, inlineDescriptor: SerialDescriptor): Decoder =
166166
if (inlineDescriptor.isUnsignedNumber) JsonDecoderForUnsignedTypes(StringJsonLexer(getPrimitiveValue(tag).content), json)
167167
else super.decodeTaggedInline(tag, inlineDescriptor)
168+
169+
override fun decodeInline(descriptor: SerialDescriptor): Decoder {
170+
return if (currentTagOrNull != null) super.decodeInline(descriptor)
171+
else JsonPrimitiveDecoder(json, value).decodeInline(descriptor)
172+
}
168173
}
169174

170-
private class JsonPrimitiveDecoder(json: Json, override val value: JsonPrimitive) : AbstractJsonTreeDecoder(json, value) {
175+
private class JsonPrimitiveDecoder(json: Json, override val value: JsonElement) : AbstractJsonTreeDecoder(json, value) {
171176

172177
init {
173178
pushTag(PRIMITIVE_TAG)

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public fun <T> Json.writeJson(value: T, serializer: SerializationStrategy<T>): J
2525
@ExperimentalSerializationApi
2626
private sealed class AbstractJsonTreeEncoder(
2727
final override val json: Json,
28-
private val nodeConsumer: (JsonElement) -> Unit
28+
protected val nodeConsumer: (JsonElement) -> Unit
2929
) : NamedValueEncoder(), JsonEncoder {
3030

3131
final override val serializersModule: SerializersModule
@@ -80,7 +80,6 @@ private sealed class AbstractJsonTreeEncoder(
8080
encodePolymorphically(serializer, value) { polymorphicDiscriminator = it }
8181
} else JsonPrimitiveEncoder(json, nodeConsumer).apply {
8282
encodeSerializableValue(serializer, value)
83-
endEncode(serializer.descriptor)
8483
}
8584
}
8685

@@ -112,6 +111,11 @@ private sealed class AbstractJsonTreeEncoder(
112111
else -> super.encodeTaggedInline(tag, inlineDescriptor)
113112
}
114113

114+
override fun encodeInline(descriptor: SerialDescriptor): Encoder {
115+
return if (currentTagOrNull != null) super.encodeInline(descriptor)
116+
else JsonPrimitiveEncoder(json, nodeConsumer).encodeInline(descriptor)
117+
}
118+
115119
@SuppressAnimalSniffer // Long(Integer).toUnsignedString(long)
116120
private fun inlineUnsignedNumberEncoder(tag: String) = object : AbstractEncoder() {
117121
override val serializersModule: SerializersModule = json.serializersModule
@@ -176,6 +180,7 @@ private class JsonPrimitiveEncoder(
176180
require(key === PRIMITIVE_TAG) { "This output can only consume primitives with '$PRIMITIVE_TAG' tag" }
177181
require(content == null) { "Primitive element was already recorded. Does call to .encodeXxx happen more than once?" }
178182
content = element
183+
nodeConsumer(element)
179184
}
180185

181186
override fun getCurrent(): JsonElement =

0 commit comments

Comments
 (0)