Skip to content

Commit 4524b65

Browse files
authored
Make MissingFieldException public (#1983)
Fixes #1266
1 parent c232772 commit 4524b65

File tree

6 files changed

+68
-28
lines changed

6 files changed

+68
-28
lines changed

core/api/kotlinx-serialization-core.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ public abstract interface annotation class kotlinx/serialization/MetaSerializabl
4848

4949
public final class kotlinx/serialization/MissingFieldException : kotlinx/serialization/SerializationException {
5050
public fun <init> (Ljava/lang/String;)V
51+
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
52+
public fun <init> (Ljava/util/List;Ljava/lang/String;)V
53+
public fun <init> (Ljava/util/List;Ljava/lang/String;Ljava/lang/Throwable;)V
54+
public final fun getMissingFields ()Ljava/util/List;
5155
}
5256

5357
public abstract interface annotation class kotlinx/serialization/Polymorphic : java/lang/annotation/Annotation {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ import kotlinx.serialization.encoding.*
6161
* For serializer implementations, it is recommended to throw subclasses of [SerializationException] for
6262
* any serialization-specific errors related to invalid or unsupported format of the data
6363
* and [IllegalStateException] for errors during validation of the data.
64-
*
6564
*/
6665
public interface KSerializer<T> : SerializationStrategy<T>, DeserializationStrategy<T> {
6766
/**
@@ -187,6 +186,7 @@ public interface DeserializationStrategy<T> {
187186
* }
188187
* ```
189188
*
189+
* @throws MissingFieldException if non-optional fields were not found during deserialization
190190
* @throws SerializationException in case of any deserialization-specific error
191191
* @throws IllegalArgumentException if the decoded input is not a valid instance of [T]
192192
* @see KSerializer for additional information about general contracts and exception specifics

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

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package kotlinx.serialization
66

7+
import kotlinx.serialization.encoding.*
8+
79
/**
810
* A generic exception indicating the problem in serialization or deserialization process.
911
*
@@ -57,15 +59,59 @@ public open class SerializationException : IllegalArgumentException {
5759
}
5860

5961
/**
60-
* Thrown when [KSerializer] did not receive property from [Decoder], and this property was not optional.
62+
* Thrown when [KSerializer] did not receive a non-optonal property from [CompositeDecoder] and [CompositeDecoder.decodeElementIndex]
63+
* had already returned [CompositeDecoder.DECODE_DONE].
64+
*
65+
* [MissingFieldException] is thrown on missing field from all [auto-generated][Serializable] serializers and it
66+
* is recommended to throw this exception from user-defined serializers.
67+
*
68+
* @see SerializationException
69+
* @see KSerializer
6170
*/
62-
@PublishedApi
63-
internal class MissingFieldException
64-
// This constructor is used by coroutines exception recovery
65-
internal constructor(message: String?, cause: Throwable?) : SerializationException(message, cause) {
66-
// This constructor is used by the generated serializers
67-
constructor(fieldName: String) : this("Field '$fieldName' is required, but it was missing", null)
68-
internal constructor(fieldNames: List<String>, serialName: String) : this(if (fieldNames.size == 1) "Field '${fieldNames[0]}' is required for type with serial name '$serialName', but it was missing" else "Fields $fieldNames are required for type with serial name '$serialName', but they were missing", null)
71+
@ExperimentalSerializationApi
72+
public class MissingFieldException(
73+
missingFields: List<String>, message: String?, cause: Throwable?
74+
) : SerializationException(message, cause) {
75+
76+
/**
77+
* List of fields that were required but not found during deserialization.
78+
* Contains at least one element.
79+
*/
80+
public val missingFields: List<String> = missingFields
81+
82+
/**
83+
* Creates an instance of [MissingFieldException] for the given [missingFields] and [serialName] of
84+
* the corresponding serializer.
85+
*/
86+
public constructor(
87+
missingFields: List<String>,
88+
serialName: String
89+
) : this(
90+
missingFields,
91+
if (missingFields.size == 1) "Field '${missingFields[0]}' is required for type with serial name '$serialName', but it was missing"
92+
else "Fields $missingFields are required for type with serial name '$serialName', but they were missing",
93+
null
94+
)
95+
96+
/**
97+
* Creates an instance of [MissingFieldException] for the given [missingField] and [serialName] of
98+
* the corresponding serializer.
99+
*/
100+
public constructor(
101+
missingField: String,
102+
serialName: String
103+
) : this(
104+
listOf(missingField),
105+
"Field '$missingField' is required for type with serial name '$serialName', but it was missing",
106+
null
107+
)
108+
109+
@PublishedApi // Constructor used by the generated serializers
110+
internal constructor(missingField: String) : this(
111+
listOf(missingField),
112+
"Field '$missingField' is required, but it was missing",
113+
null
114+
)
69115
}
70116

71117
/**

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

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class JvmMissingFieldsExceptionTest {
8181

8282
@Test
8383
fun testBigPlaneClass() {
84-
val missedFields = MutableList(35) { "f$it" }
84+
val missedFields = MutableList(36) { "f$it" }
8585
val definedInJsonFields = arrayOf("f1", "f15", "f34")
8686
val optionalFields = arrayOf("f3", "f5", "f7")
8787
missedFields.removeAll(definedInJsonFields)
@@ -102,7 +102,6 @@ class JvmMissingFieldsExceptionTest {
102102
assertFailsWithMessages(listOf("p2", "c3")) {
103103
Json {
104104
serializersModule = module
105-
useArrayPolymorphism = false
106105
}.decodeFromString<PolymorphicWrapper>("""{"nested": {"type": "a", "p1": 1, "c1": 11}}""")
107106
}
108107
}
@@ -111,16 +110,14 @@ class JvmMissingFieldsExceptionTest {
111110
@Test
112111
fun testSealed() {
113112
assertFailsWithMessages(listOf("p3", "c2")) {
114-
Json { useArrayPolymorphism = false }
115-
.decodeFromString<Parent>("""{"type": "child", "p1":1, "c1": 11}""")
113+
Json.decodeFromString<Parent>("""{"type": "child", "p1":1, "c1": 11}""")
116114
}
117115
}
118116

119117
@Test
120118
fun testTransient() {
121119
assertFailsWithMessages(listOf("f3", "f4")) {
122-
Json { useArrayPolymorphism = false }
123-
.decodeFromString<WithTransient>("""{"f1":1}""")
120+
Json.decodeFromString<WithTransient>("""{"f1":1}""")
124121
}
125122
}
126123

@@ -132,10 +129,10 @@ class JvmMissingFieldsExceptionTest {
132129
}
133130

134131

135-
private inline fun assertFailsWithMessages(messages: List<String>, block: () -> Unit) {
136-
val exception = assertFailsWith(SerializationException::class, null, block)
137-
assertEquals("kotlinx.serialization.MissingFieldException", exception::class.qualifiedName)
138-
val missedMessages = messages.filter { !exception.message!!.contains(it) }
139-
assertTrue(missedMessages.isEmpty(), "Expected message '${exception.message}' to contain substrings $missedMessages")
132+
private inline fun assertFailsWithMessages(fields: List<String>, block: () -> Unit) {
133+
val exception = assertFailsWith(MissingFieldException::class, null, block)
134+
val missedMessages = fields.filter { !exception.message!!.contains(it) }
135+
assertEquals(exception.missingFields.sorted(), fields.sorted())
136+
assertTrue(missedMessages.isEmpty(), "Expected message '${exception.message}' to contain substrings $fields")
140137
}
141138
}

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,6 @@ class StacktraceRecoveryTest {
3838
serializer.deserialize(BadDecoder())
3939
}
4040

41-
@Test
42-
// checks simple name because MFE is internal class
43-
fun testMissingFieldException() = checkRecovered("MissingFieldException") {
44-
Json.decodeFromString<Data>("{}")
45-
}
46-
4741
private fun checkRecovered(exceptionClassSimpleName: String, block: () -> Unit) = runBlocking {
4842
val result = runCatching {
4943
callBlockWithRecovery(block)

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ internal open class StreamingJsonDecoder(
8787
return result
8888

8989
} catch (e: MissingFieldException) {
90-
throw MissingFieldException(e.message + " at path: " + lexer.path.getPath(), e)
90+
throw MissingFieldException(e.missingFields, e.message + " at path: " + lexer.path.getPath(), e)
9191
}
9292
}
9393

@@ -213,7 +213,6 @@ internal open class StreamingJsonDecoder(
213213
{ lexer.consumeString() /* skip unknown enum string*/ }
214214
)
215215

216-
@Suppress("INVISIBLE_MEMBER")
217216
private fun decodeObjectIndex(descriptor: SerialDescriptor): Int {
218217
// hasComma checks are required to properly react on trailing commas
219218
var hasComma = lexer.tryConsumeComma()

0 commit comments

Comments
 (0)