diff options
author | Vsevolod Tolstopyatov <qwwdfsad@gmail.com> | 2022-07-11 13:10:32 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-11 14:10:32 +0300 |
commit | 4524b654da77ba1b120381e3b4c13482f2f9af1a (patch) | |
tree | a315cb646b6d51157f9c5428ce4e1078155e1920 | |
parent | c2327723a8f98fe717c0ec143cff6421b469173e (diff) | |
download | kotlinx.serialization-4524b654da77ba1b120381e3b4c13482f2f9af1a.tar.gz |
Make MissingFieldException public (#1983)
Fixes #1266
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() |