Skip to content

Commit 8926b06

Browse files
committed
Add .serialName to MissingFieldException
Make new constructor that requires it, and allow creation with nullable serial name only via internal mechanism. Old constructor without serial name should be eventually hidden. Fixes #2958
1 parent 15b249e commit 8926b06

File tree

6 files changed

+89
-23
lines changed

6 files changed

+89
-23
lines changed

core/api/kotlinx-serialization-core.api

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,12 @@ public abstract interface annotation class kotlinx/serialization/MetaSerializabl
5151
}
5252

5353
public final class kotlinx/serialization/MissingFieldException : kotlinx/serialization/SerializationException {
54-
public fun <init> (Ljava/lang/String;)V
54+
public synthetic fun <init> (Ljava/lang/String;)V
5555
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
5656
public fun <init> (Ljava/util/List;Ljava/lang/String;)V
5757
public fun <init> (Ljava/util/List;Ljava/lang/String;Ljava/lang/Throwable;)V
5858
public final fun getMissingFields ()Ljava/util/List;
59+
public final fun getSerialName ()Ljava/lang/String;
5960
}
6061

6162
public abstract interface annotation class kotlinx/serialization/Polymorphic : java/lang/annotation/Annotation {
@@ -848,6 +849,7 @@ public final class kotlinx/serialization/internal/IntSerializer : kotlinx/serial
848849

849850
public final class kotlinx/serialization/internal/JsonInternalDependenciesKt {
850851
public static final fun jsonCachedSerialNames (Lkotlinx/serialization/descriptors/SerialDescriptor;)Ljava/util/Set;
852+
public static final fun missingFieldExceptionWithNewMessage (Lkotlinx/serialization/MissingFieldException;Ljava/lang/String;)Lkotlinx/serialization/MissingFieldException;
851853
}
852854

853855
public abstract class kotlinx/serialization/internal/KeyValueSerializer : kotlinx/serialization/KSerializer {

core/api/kotlinx-serialization-core.klib.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,8 @@ final class kotlinx.serialization/MissingFieldException : kotlinx.serialization/
743743

744744
final val missingFields // kotlinx.serialization/MissingFieldException.missingFields|{}missingFields[0]
745745
final fun <get-missingFields>(): kotlin.collections/List<kotlin/String> // kotlinx.serialization/MissingFieldException.missingFields.<get-missingFields>|<get-missingFields>(){}[0]
746+
final val serialName // kotlinx.serialization/MissingFieldException.serialName|{}serialName[0]
747+
final fun <get-serialName>(): kotlin/String? // kotlinx.serialization/MissingFieldException.serialName.<get-serialName>|<get-serialName>(){}[0]
746748
}
747749

748750
final class kotlinx.serialization/UnknownFieldException : kotlinx.serialization/SerializationException { // kotlinx.serialization/UnknownFieldException|null[0]
@@ -1153,6 +1155,7 @@ final fun kotlinx.serialization.descriptors/listSerialDescriptor(kotlinx.seriali
11531155
final fun kotlinx.serialization.descriptors/mapSerialDescriptor(kotlinx.serialization.descriptors/SerialDescriptor, kotlinx.serialization.descriptors/SerialDescriptor): kotlinx.serialization.descriptors/SerialDescriptor // kotlinx.serialization.descriptors/mapSerialDescriptor|mapSerialDescriptor(kotlinx.serialization.descriptors.SerialDescriptor;kotlinx.serialization.descriptors.SerialDescriptor){}[0]
11541156
final fun kotlinx.serialization.descriptors/serialDescriptor(kotlin.reflect/KType): kotlinx.serialization.descriptors/SerialDescriptor // kotlinx.serialization.descriptors/serialDescriptor|serialDescriptor(kotlin.reflect.KType){}[0]
11551157
final fun kotlinx.serialization.descriptors/setSerialDescriptor(kotlinx.serialization.descriptors/SerialDescriptor): kotlinx.serialization.descriptors/SerialDescriptor // kotlinx.serialization.descriptors/setSerialDescriptor|setSerialDescriptor(kotlinx.serialization.descriptors.SerialDescriptor){}[0]
1158+
final fun kotlinx.serialization.internal/missingFieldExceptionWithNewMessage(kotlinx.serialization/MissingFieldException, kotlin/String): kotlinx.serialization/MissingFieldException // kotlinx.serialization.internal/missingFieldExceptionWithNewMessage|missingFieldExceptionWithNewMessage(kotlinx.serialization.MissingFieldException;kotlin.String){}[0]
11561159
final fun kotlinx.serialization.internal/throwArrayMissingFieldException(kotlin/IntArray, kotlin/IntArray, kotlinx.serialization.descriptors/SerialDescriptor) // kotlinx.serialization.internal/throwArrayMissingFieldException|throwArrayMissingFieldException(kotlin.IntArray;kotlin.IntArray;kotlinx.serialization.descriptors.SerialDescriptor){}[0]
11571160
final fun kotlinx.serialization.internal/throwMissingFieldException(kotlin/Int, kotlin/Int, kotlinx.serialization.descriptors/SerialDescriptor) // kotlinx.serialization.internal/throwMissingFieldException|throwMissingFieldException(kotlin.Int;kotlin.Int;kotlinx.serialization.descriptors.SerialDescriptor){}[0]
11581161
final fun kotlinx.serialization.modules/EmptySerializersModule(): kotlinx.serialization.modules/SerializersModule // kotlinx.serialization.modules/EmptySerializersModule|EmptySerializersModule(){}[0]

core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,17 @@ public open class SerializationException : IllegalArgumentException {
7171
* [MissingFieldException] is constructed from the following properties:
7272
* - [missingFields] -- fields that were required for the deserialization but have not been found.
7373
* They are always non-empty and their names match the corresponding names in [SerialDescriptor.elementNames]
74-
* - Optional `serialName` -- serial name of the enclosing class that failed to get deserialized.
74+
* - [serialName] -- a serial name of the enclosing class that failed to get deserialized.
7575
* Matches the corresponding [SerialDescriptor.serialName].
7676
*
7777
* @see SerializationException
7878
* @see KSerializer
7979
*/
8080
@ExperimentalSerializationApi
81-
public class MissingFieldException(
82-
missingFields: List<String>, message: String?, cause: Throwable?
81+
public class MissingFieldException private constructor(
82+
// only private constructor allows nullable serialName and message
83+
// reordered things a bit to avoid signature clash with public one
84+
message: String?, cause: Throwable?, missingFields: List<String>, serialName: String?
8385
) : SerializationException(message, cause) {
8486

8587
/**
@@ -88,6 +90,15 @@ public class MissingFieldException(
8890
*/
8991
public val missingFields: List<String> = missingFields
9092

93+
/**
94+
* Returns a serial name of a serializable class that cannot be deserialized due to missing fields.
95+
* Typically, equal to the [SerialDescriptor.serialName] of the serializable class.
96+
*
97+
* However, in cases the class was compiled with an old Kotlin serialization plugin,
98+
* its serial name may be unavailable and this property is `null`.
99+
*/
100+
public val serialName: String? = serialName
101+
91102
/**
92103
* Creates an instance of [MissingFieldException] for the given [missingFields] and [serialName] of
93104
* the corresponding serializer.
@@ -96,10 +107,11 @@ public class MissingFieldException(
96107
missingFields: List<String>,
97108
serialName: String
98109
) : this(
99-
missingFields,
100-
if (missingFields.size == 1) "Field '${missingFields[0]}' is required for type with serial name '$serialName', but it was missing"
110+
missingFields = missingFields,
111+
serialName = serialName,
112+
message = if (missingFields.size == 1) "Field '${missingFields[0]}' is required for type with serial name '$serialName', but it was missing"
101113
else "Fields $missingFields are required for type with serial name '$serialName', but they were missing",
102-
null
114+
cause = null
103115
)
104116

105117
/**
@@ -110,16 +122,33 @@ public class MissingFieldException(
110122
missingField: String,
111123
serialName: String
112124
) : this(
113-
listOf(missingField),
114-
"Field '$missingField' is required for type with serial name '$serialName', but it was missing",
115-
null
125+
missingFields = listOf(missingField),
126+
serialName = serialName,
127+
message = "Field '$missingField' is required for type with serial name '$serialName', but it was missing",
128+
cause = null
116129
)
117130

118-
@PublishedApi // Constructor used by the generated serializers
131+
// Deprecated since 1.10.0, should be HIDDEN when MFE is stable
132+
@Deprecated("Use constructor which accepts serialName parameter", ReplaceWith("MissingFieldException(missingFields, descriptor.serialName, message, cause)"), level = DeprecationLevel.ERROR)
133+
public constructor(
134+
missingFields: List<String>, message: String?, cause: Throwable?
135+
) : this(message, cause, missingFields, serialName = null)
136+
137+
@PublishedApi
138+
@Deprecated("Constructor used by the serializers generated by plugins older than Kotlin 1.5", level = DeprecationLevel.HIDDEN)
119139
internal constructor(missingField: String) : this(
120-
listOf(missingField),
121-
"Field '$missingField' is required, but it was missing",
122-
null
140+
message = "Field '$missingField' is required, but it was missing",
141+
cause = null,
142+
missingFields = listOf(missingField),
143+
serialName = null
144+
)
145+
146+
// It's better to have internal function calling private ctor rather than just internal ctor (because the latter is visible from Java)
147+
internal fun withNewMessageInternal(newMessage: String): MissingFieldException = MissingFieldException(
148+
message = newMessage,
149+
cause = this,
150+
missingFields = missingFields,
151+
serialName = serialName
123152
)
124153
}
125154

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package kotlinx.serialization.internal
22

3+
import kotlinx.serialization.ExperimentalSerializationApi
4+
import kotlinx.serialization.MissingFieldException
35
import kotlinx.serialization.descriptors.*
46

57
/*
68
* Methods that are required for kotlinx-serialization-json, but are not effectively public.
79
*
8-
* Anything marker with this annotation is not intended for public use.
10+
* Anything marked with this annotation is not intended for public use.
911
*/
1012
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
1113
internal annotation class CoreFriendModuleApi
1214

1315
@CoreFriendModuleApi
1416
public fun SerialDescriptor.jsonCachedSerialNames(): Set<String> = cachedSerialNames()
17+
18+
@ExperimentalSerializationApi
19+
@CoreFriendModuleApi
20+
public fun missingFieldExceptionWithNewMessage(exception: MissingFieldException, message: String): MissingFieldException =
21+
exception.withNewMessageInternal(message)

formats/json-tests/jvmTest/src/kotlinx/serialization/JvmMissingFieldsExceptionTest.kt

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package kotlinx.serialization
22

33
import org.junit.Test
44
import kotlinx.serialization.json.Json
5+
import kotlinx.serialization.builtins.serializer
56
import kotlinx.serialization.modules.SerializersModule
67
import kotlinx.serialization.modules.polymorphic
78
import kotlinx.serialization.modules.subclass
@@ -74,7 +75,10 @@ class JvmMissingFieldsExceptionTest {
7475

7576
@Test
7677
fun testShortPlaneClass() {
77-
assertFailsWithMessages(listOf("f2", "f4")) {
78+
assertFailsWithMessages(
79+
fields = listOf("f2", "f4"),
80+
expectedSerialName = ShortPlaneClass.serializer().descriptor.serialName
81+
) {
7882
Json.decodeFromString<ShortPlaneClass>("""{"f1":1}""")
7983
}
8084
}
@@ -86,7 +90,10 @@ class JvmMissingFieldsExceptionTest {
8690
val optionalFields = arrayOf("f3", "f5", "f7")
8791
missedFields.removeAll(definedInJsonFields)
8892
missedFields.removeAll(optionalFields)
89-
assertFailsWithMessages(missedFields) {
93+
assertFailsWithMessages(
94+
fields = missedFields,
95+
expectedSerialName = BigPlaneClass.serializer().descriptor.serialName
96+
) {
9097
Json.decodeFromString<BigPlaneClass>("""{"f1":1, "f15": 15, "f34": 34}""")
9198
}
9299
}
@@ -102,38 +109,56 @@ class JvmMissingFieldsExceptionTest {
102109
serializersModule = module
103110
}
104111

105-
assertFailsWithMessages(listOf("p2", "c3")) {
112+
assertFailsWithMessages(
113+
fields = listOf("p2", "c3"),
114+
// For polymorphic, the error originates from the actual subtype
115+
expectedSerialName = ChildA.serializer().descriptor.serialName
116+
) {
106117
json.decodeFromString<PolymorphicWrapper>("""{"nested": {"type": "a", "p1": 1, "c1": 11}}""")
107118
}
108119
}
109120

110121

111122
@Test
112123
fun testSealed() {
113-
assertFailsWithMessages(listOf("p3", "c2")) {
124+
assertFailsWithMessages(
125+
fields = listOf("p3", "c2"),
126+
expectedSerialName = Parent.Child.serializer().descriptor.serialName
127+
) {
114128
Json.decodeFromString<Parent>("""{"type": "child", "p1":1, "c1": 11}""")
115129
}
116130
}
117131

118132
@Test
119133
fun testTransient() {
120-
assertFailsWithMessages(listOf("f3", "f4")) {
134+
assertFailsWithMessages(
135+
fields = listOf("f3", "f4"),
136+
expectedSerialName = WithTransient.serializer().descriptor.serialName
137+
) {
121138
Json.decodeFromString<WithTransient>("""{"f1":1}""")
122139
}
123140
}
124141

125142
@Test
126143
fun testGeneric() {
127-
assertFailsWithMessages(listOf("f2", "f3")) {
144+
assertFailsWithMessages(
145+
fields = listOf("f2", "f3"),
146+
expectedSerialName = Generic.serializer(Int.serializer(), Int.serializer(), Int.serializer()).descriptor.serialName
147+
) {
128148
Json.decodeFromString<Generic<Int, Int, Int>>("""{"f1":1}""")
129149
}
130150
}
131151

132152

133-
private inline fun assertFailsWithMessages(fields: List<String>, block: () -> Any?) {
153+
private inline fun assertFailsWithMessages(
154+
fields: List<String>,
155+
expectedSerialName: String,
156+
block: () -> Any?
157+
) {
134158
val exception = assertFailsWith(MissingFieldException::class, null, block)
135159
val missedMessages = fields.filter { !exception.message!!.contains(it) }
136160
assertEquals(exception.missingFields.sorted(), fields.sorted())
161+
assertEquals(expectedSerialName, exception.serialName)
137162
assertTrue(missedMessages.isEmpty(), "Expected message '${exception.message}' to contain substrings $fields")
138163
}
139164
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ internal open class StreamingJsonDecoder(
9292
// Add "at path" if and only if we've just caught an exception and it hasn't been augmented yet
9393
if (e.message!!.contains("at path")) throw e
9494
// NB: we could've use some additional flag marker or augment the stacktrace, but it seemed to be as too much of a burden
95-
throw MissingFieldException(e.missingFields, e.message + " at path: " + lexer.path.getPath(), e)
95+
throw missingFieldExceptionWithNewMessage(e, e.message + " at path: " + lexer.path.getPath())
9696
}
9797
}
9898

0 commit comments

Comments
 (0)