Skip to content

Commit a2f92e4

Browse files
authored
Fix serializing nulls for a property of a parameterized type with a nullable upper bound with Protobuf (#2561)
This is done by setting `nullableMode` depending on `serializer.descriptor.isNullable` in `encodeSerializableElement`. Because of https://youtrack.jetbrains.com/issue/KT-66206 nullable types can still be encountered in `encodeSerializableElement` despite having explicit `encodeNullableSerializableElement`
1 parent b811fa3 commit a2f92e4

File tree

5 files changed

+146
-22
lines changed

5 files changed

+146
-22
lines changed

formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,27 @@ internal abstract class ProtobufTaggedEncoder : ProtobufTaggedBase(), Encoder, C
123123
public final override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String): Unit =
124124
encodeTaggedString(descriptor.getTag(index), value)
125125

126+
private fun SerialKind.isMapOrList() =
127+
this == StructureKind.MAP || this == StructureKind.LIST
128+
126129
public final override fun <T : Any?> encodeSerializableElement(
127130
descriptor: SerialDescriptor,
128131
index: Int,
129132
serializer: SerializationStrategy<T>,
130133
value: T
131134
) {
132-
nullableMode = NullableMode.NOT_NULL
135+
nullableMode =
136+
if (descriptor.isElementOptional(index))
137+
NullableMode.OPTIONAL
138+
else {
139+
val elementDescriptor = descriptor.getElementDescriptor(index)
140+
if (elementDescriptor.kind.isMapOrList())
141+
NullableMode.COLLECTION
142+
else if (!descriptor.kind.isMapOrList() && elementDescriptor.isNullable) // or: `serializer.descriptor`
143+
NullableMode.ACCEPTABLE
144+
else
145+
NullableMode.NOT_NULL
146+
}
133147

134148
pushTag(descriptor.getTag(index))
135149
encodeSerializableValue(serializer, value)
@@ -141,14 +155,12 @@ internal abstract class ProtobufTaggedEncoder : ProtobufTaggedBase(), Encoder, C
141155
serializer: SerializationStrategy<T>,
142156
value: T?
143157
) {
144-
val elementKind = descriptor.getElementDescriptor(index).kind
145-
nullableMode = if (descriptor.isElementOptional(index)) {
158+
nullableMode = if (descriptor.isElementOptional(index))
146159
NullableMode.OPTIONAL
147-
} else if (elementKind == StructureKind.MAP || elementKind == StructureKind.LIST) {
160+
else if (descriptor.getElementDescriptor(index).kind.isMapOrList())
148161
NullableMode.COLLECTION
149-
} else {
162+
else
150163
NullableMode.ACCEPTABLE
151-
}
152164

153165
pushTag(descriptor.getTag(index))
154166
encodeNullableSerializableValue(serializer, value)

formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufNothingTest.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,23 @@
55
package kotlinx.serialization.protobuf
66

77
import kotlinx.serialization.*
8-
import kotlinx.serialization.test.*
98
import kotlin.test.*
109

1110
class ProtobufNothingTest {
1211
@Serializable
1312
/*private*/ data class NullableNothingBox(val value: Nothing?) // `private` doesn't work on the JS legacy target
1413

1514
@Serializable
16-
private data class ParameterizedBox<T : Any>(val value: T?)
15+
private data class NullablePropertyNotNullUpperBoundParameterizedBox<T : Any>(val value: T?)
16+
17+
@Serializable
18+
private data class NullableUpperBoundParameterizedBox<T : Any?>(val value: T)
1719

18-
private inline fun <reified T> testConversion(data: T, expectedHexString: String) {
19-
val string = ProtoBuf.encodeToHexString(data).uppercase()
20-
assertEquals(expectedHexString, string)
21-
assertEquals(data, ProtoBuf.decodeFromHexString(string))
22-
}
2320

2421
@Test
2522
fun testNothing() {
2623
testConversion(NullableNothingBox(null), "")
27-
testConversion(ParameterizedBox(null), "")
24+
testConversion(NullablePropertyNotNullUpperBoundParameterizedBox(null), "")
25+
testConversion(NullableUpperBoundParameterizedBox(null), "")
2826
}
2927
}

formats/protobuf/commonTest/src/kotlinx/serialization/protobuf/ProtobufPrimitivesTest.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,10 @@
33
*/
44
package kotlinx.serialization.protobuf
55

6-
import kotlinx.serialization.*
76
import kotlinx.serialization.builtins.*
87
import kotlin.test.*
98

109
class ProtobufPrimitivesTest {
11-
12-
private fun <T> testConversion(data: T, serializer: KSerializer<T>, expectedHexString: String) {
13-
val string = ProtoBuf.encodeToHexString(serializer, data).uppercase()
14-
assertEquals(expectedHexString, string)
15-
assertEquals(data, ProtoBuf.decodeFromHexString(serializer, string))
16-
}
17-
1810
@Test
1911
fun testPrimitiveTypes() {
2012
testConversion(true, Boolean.serializer(), "01")
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.protobuf
6+
7+
import kotlinx.serialization.*
8+
import kotlin.test.*
9+
10+
class ProtobufTypeParameterTest {
11+
@Serializable
12+
data class Box<T>(val value: T)
13+
14+
@Serializable
15+
data class ExplicitNullableUpperBoundBox<T : Any?>(val value: T)
16+
17+
@Serializable
18+
data class ExplicitNullableUpperNullablePropertyBoundBox<T : Any?>(val value: T?)
19+
20+
inline fun <reified T> testBox(value: T, expectedHexString: String) {
21+
testConversion(Box(value), expectedHexString)
22+
testConversion(ExplicitNullableUpperBoundBox(value), expectedHexString)
23+
testConversion(ExplicitNullableUpperNullablePropertyBoundBox(value), expectedHexString)
24+
}
25+
26+
@Serializable
27+
private data class DefaultArgPair<T>(val first: T, val second: T = first)
28+
29+
companion object {
30+
val testList0 = emptyList<Int>()
31+
val testList1 = listOf(0)
32+
val testMap0 = emptyMap<Int, Int>()
33+
val testMap1 = mapOf(0 to 0)
34+
}
35+
36+
37+
@Test
38+
fun testNothingBoxesWithNull() {
39+
// Cannot use 'Nothing?' as reified type parameter
40+
//testBox(null, "")
41+
testConversion(Box(null), "")
42+
testConversion(ExplicitNullableUpperBoundBox(null), "")
43+
@Suppress("RemoveExplicitTypeArguments")
44+
testConversion(ExplicitNullableUpperNullablePropertyBoundBox<Nothing>(null), "")
45+
testConversion(ExplicitNullableUpperNullablePropertyBoundBox<Nothing?>(null), "")
46+
}
47+
48+
@Test
49+
fun testIntBoxes() {
50+
testBox(0, "0800")
51+
testBox(1, "0801")
52+
}
53+
54+
@Test
55+
fun testNullableIntBoxes() {
56+
testBox<Int?>(null, "")
57+
testBox<Int?>(0, "0800")
58+
}
59+
60+
@Test
61+
fun testCollectionBoxes() {
62+
testBox(testList0, "")
63+
testBox(testList1, "0800")
64+
testBox(testMap0, "")
65+
testBox(testMap1, "0A0408001000")
66+
}
67+
68+
@Test
69+
fun testNullableCollectionBoxes() {
70+
fun assertFailsForNullForCollectionTypes(block: () -> Unit) {
71+
try {
72+
block()
73+
fail()
74+
} catch (e: SerializationException) {
75+
assertEquals(
76+
"'null' is not supported for collection types in ProtoBuf", e.message
77+
)
78+
}
79+
}
80+
assertFailsForNullForCollectionTypes {
81+
testBox<List<Int>?>(null, "")
82+
}
83+
assertFailsForNullForCollectionTypes {
84+
testBox<Map<Int, Int>?>(null, "")
85+
}
86+
testBox<List<Int>?>(testList0, "")
87+
testBox<Map<Int, Int>?>(testMap0, "")
88+
}
89+
90+
@Test
91+
fun testWithDefaultArguments() {
92+
testConversion(DefaultArgPair(null), "")
93+
testConversion(DefaultArgPair(1), "0801")
94+
testConversion(DefaultArgPair(null, null), "")
95+
testConversion(DefaultArgPair(null, 1), "1001")
96+
assertFailsWith<SerializationException> {
97+
testConversion(DefaultArgPair(1, null), "0801")
98+
}
99+
testConversion(DefaultArgPair(1, 1), "0801")
100+
testConversion(DefaultArgPair(1, 2), "08011002")
101+
}
102+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.protobuf
6+
7+
import kotlinx.serialization.*
8+
import kotlin.test.*
9+
10+
fun <T> testConversion(data: T, serializer: KSerializer<T>, expectedHexString: String) {
11+
val string = ProtoBuf.encodeToHexString(serializer, data).uppercase()
12+
assertEquals(expectedHexString, string)
13+
assertEquals(data, ProtoBuf.decodeFromHexString(serializer, string))
14+
}
15+
16+
inline fun <reified T> testConversion(data: T, expectedHexString: String) {
17+
val string = ProtoBuf.encodeToHexString(data).uppercase()
18+
assertEquals(expectedHexString, string)
19+
assertEquals(data, ProtoBuf.decodeFromHexString(string))
20+
}

0 commit comments

Comments
 (0)