summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVsevolod Tolstopyatov <qwwdfsad@gmail.com>2022-07-11 13:10:32 +0200
committerGitHub <noreply@github.com>2022-07-11 14:10:32 +0300
commit4524b654da77ba1b120381e3b4c13482f2f9af1a (patch)
treea315cb646b6d51157f9c5428ce4e1078155e1920
parentc2327723a8f98fe717c0ec143cff6421b469173e (diff)
downloadkotlinx.serialization-4524b654da77ba1b120381e3b4c13482f2f9af1a.tar.gz
Make MissingFieldException public (#1983)
Fixes #1266
-rw-r--r--core/api/kotlinx-serialization-core.api4
-rw-r--r--core/commonMain/src/kotlinx/serialization/KSerializer.kt2
-rw-r--r--core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt62
-rw-r--r--formats/json-tests/jvmTest/src/kotlinx/serialization/JvmMissingFieldsExceptionTest.kt19
-rw-r--r--formats/json-tests/jvmTest/src/kotlinx/serialization/StacktraceRecoveryTest.kt6
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt3
6 files changed, 68 insertions, 28 deletions
diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api
index 63c7dfa6..bc7bbced 100644
--- a/core/api/kotlinx-serialization-core.api
+++ b/core/api/kotlinx-serialization-core.api
@@ -48,6 +48,10 @@ public abstract interface annotation class kotlinx/serialization/MetaSerializabl
public final class kotlinx/serialization/MissingFieldException : kotlinx/serialization/SerializationException {
public fun <init> (Ljava/lang/String;)V
+ public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
+ public fun <init> (Ljava/util/List;Ljava/lang/String;)V
+ public fun <init> (Ljava/util/List;Ljava/lang/String;Ljava/lang/Throwable;)V
+ public final fun getMissingFields ()Ljava/util/List;
}
public abstract interface annotation class kotlinx/serialization/Polymorphic : java/lang/annotation/Annotation {
diff --git a/core/commonMain/src/kotlinx/serialization/KSerializer.kt b/core/commonMain/src/kotlinx/serialization/KSerializer.kt
index 2d9def18..69f0a0c9 100644
--- a/core/commonMain/src/kotlinx/serialization/KSerializer.kt
+++ b/core/commonMain/src/kotlinx/serialization/KSerializer.kt
@@ -61,7 +61,6 @@ import kotlinx.serialization.encoding.*
* For serializer implementations, it is recommended to throw subclasses of [SerializationException] for
* any serialization-specific errors related to invalid or unsupported format of the data
* and [IllegalStateException] for errors during validation of the data.
- *
*/
public interface KSerializer<T> : SerializationStrategy<T>, DeserializationStrategy<T> {
/**
@@ -187,6 +186,7 @@ public interface DeserializationStrategy<T> {
* }
* ```
*
+ * @throws MissingFieldException if non-optional fields were not found during deserialization
* @throws SerializationException in case of any deserialization-specific error
* @throws IllegalArgumentException if the decoded input is not a valid instance of [T]
* @see KSerializer for additional information about general contracts and exception specifics
diff --git a/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt b/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt
index 9b690509..c4c72aa9 100644
--- a/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt
+++ b/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt
@@ -4,6 +4,8 @@
package kotlinx.serialization
+import kotlinx.serialization.encoding.*
+
/**
* A generic exception indicating the problem in serialization or deserialization process.
*
@@ -57,15 +59,59 @@ public open class SerializationException : IllegalArgumentException {
}
/**
- * Thrown when [KSerializer] did not receive property from [Decoder], and this property was not optional.
+ * Thrown when [KSerializer] did not receive a non-optonal property from [CompositeDecoder] and [CompositeDecoder.decodeElementIndex]
+ * had already returned [CompositeDecoder.DECODE_DONE].
+ *
+ * [MissingFieldException] is thrown on missing field from all [auto-generated][Serializable] serializers and it
+ * is recommended to throw this exception from user-defined serializers.
+ *
+ * @see SerializationException
+ * @see KSerializer
*/
-@PublishedApi
-internal class MissingFieldException
-// This constructor is used by coroutines exception recovery
-internal constructor(message: String?, cause: Throwable?) : SerializationException(message, cause) {
- // This constructor is used by the generated serializers
- constructor(fieldName: String) : this("Field '$fieldName' is required, but it was missing", null)
- 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)
+@ExperimentalSerializationApi
+public class MissingFieldException(
+ missingFields: List<String>, message: String?, cause: Throwable?
+) : SerializationException(message, cause) {
+
+ /**
+ * List of fields that were required but not found during deserialization.
+ * Contains at least one element.
+ */
+ public val missingFields: List<String> = missingFields
+
+ /**
+ * Creates an instance of [MissingFieldException] for the given [missingFields] and [serialName] of
+ * the corresponding serializer.
+ */
+ public constructor(
+ missingFields: List<String>,
+ serialName: String
+ ) : this(
+ missingFields,
+ if (missingFields.size == 1) "Field '${missingFields[0]}' is required for type with serial name '$serialName', but it was missing"
+ else "Fields $missingFields are required for type with serial name '$serialName', but they were missing",
+ null
+ )
+
+ /**
+ * Creates an instance of [MissingFieldException] for the given [missingField] and [serialName] of
+ * the corresponding serializer.
+ */
+ public constructor(
+ missingField: String,
+ serialName: String
+ ) : this(
+ listOf(missingField),
+ "Field '$missingField' is required for type with serial name '$serialName', but it was missing",
+ null
+ )
+
+ @PublishedApi // Constructor used by the generated serializers
+ internal constructor(missingField: String) : this(
+ listOf(missingField),
+ "Field '$missingField' is required, but it was missing",
+ null
+ )
}
/**
diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/JvmMissingFieldsExceptionTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/JvmMissingFieldsExceptionTest.kt
index 5fc366a9..a423bc84 100644
--- a/formats/json-tests/jvmTest/src/kotlinx/serialization/JvmMissingFieldsExceptionTest.kt
+++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/JvmMissingFieldsExceptionTest.kt
@@ -81,7 +81,7 @@ class JvmMissingFieldsExceptionTest {
@Test
fun testBigPlaneClass() {
- val missedFields = MutableList(35) { "f$it" }
+ val missedFields = MutableList(36) { "f$it" }
val definedInJsonFields = arrayOf("f1", "f15", "f34")
val optionalFields = arrayOf("f3", "f5", "f7")
missedFields.removeAll(definedInJsonFields)
@@ -102,7 +102,6 @@ class JvmMissingFieldsExceptionTest {
assertFailsWithMessages(listOf("p2", "c3")) {
Json {
serializersModule = module
- useArrayPolymorphism = false
}.decodeFromString<PolymorphicWrapper>("""{"nested": {"type": "a", "p1": 1, "c1": 11}}""")
}
}
@@ -111,16 +110,14 @@ class JvmMissingFieldsExceptionTest {
@Test
fun testSealed() {
assertFailsWithMessages(listOf("p3", "c2")) {
- Json { useArrayPolymorphism = false }
- .decodeFromString<Parent>("""{"type": "child", "p1":1, "c1": 11}""")
+ Json.decodeFromString<Parent>("""{"type": "child", "p1":1, "c1": 11}""")
}
}
@Test
fun testTransient() {
assertFailsWithMessages(listOf("f3", "f4")) {
- Json { useArrayPolymorphism = false }
- .decodeFromString<WithTransient>("""{"f1":1}""")
+ Json.decodeFromString<WithTransient>("""{"f1":1}""")
}
}
@@ -132,10 +129,10 @@ class JvmMissingFieldsExceptionTest {
}
- private inline fun assertFailsWithMessages(messages: List<String>, block: () -> Unit) {
- val exception = assertFailsWith(SerializationException::class, null, block)
- assertEquals("kotlinx.serialization.MissingFieldException", exception::class.qualifiedName)
- val missedMessages = messages.filter { !exception.message!!.contains(it) }
- assertTrue(missedMessages.isEmpty(), "Expected message '${exception.message}' to contain substrings $missedMessages")
+ private inline fun assertFailsWithMessages(fields: List<String>, block: () -> Unit) {
+ val exception = assertFailsWith(MissingFieldException::class, null, block)
+ val missedMessages = fields.filter { !exception.message!!.contains(it) }
+ assertEquals(exception.missingFields.sorted(), fields.sorted())
+ assertTrue(missedMessages.isEmpty(), "Expected message '${exception.message}' to contain substrings $fields")
}
}
diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/StacktraceRecoveryTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/StacktraceRecoveryTest.kt
index e0df4220..dcf3e959 100644
--- a/formats/json-tests/jvmTest/src/kotlinx/serialization/StacktraceRecoveryTest.kt
+++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/StacktraceRecoveryTest.kt
@@ -38,12 +38,6 @@ class StacktraceRecoveryTest {
serializer.deserialize(BadDecoder())
}
- @Test
- // checks simple name because MFE is internal class
- fun testMissingFieldException() = checkRecovered("MissingFieldException") {
- Json.decodeFromString<Data>("{}")
- }
-
private fun checkRecovered(exceptionClassSimpleName: String, block: () -> Unit) = runBlocking {
val result = runCatching {
callBlockWithRecovery(block)
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
index 3505a9c5..f4760bdd 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt
@@ -87,7 +87,7 @@ internal open class StreamingJsonDecoder(
return result
} catch (e: MissingFieldException) {
- throw MissingFieldException(e.message + " at path: " + lexer.path.getPath(), e)
+ throw MissingFieldException(e.missingFields, e.message + " at path: " + lexer.path.getPath(), e)
}
}
@@ -213,7 +213,6 @@ internal open class StreamingJsonDecoder(
{ lexer.consumeString() /* skip unknown enum string*/ }
)
- @Suppress("INVISIBLE_MEMBER")
private fun decodeObjectIndex(descriptor: SerialDescriptor): Int {
// hasComma checks are required to properly react on trailing commas
var hasComma = lexer.tryConsumeComma()