Kotlin / kotlinx.serialization

Kotlin multiplatform / multi-format serialization
Apache License 2.0
5.44k stars 623 forks source link

`kotlinx.serialization.MissingFieldException` crash when using `decodeFromJsonElement()` #2448

Closed RabieJradi closed 1 year ago

RabieJradi commented 1 year ago

Describe the bug given the same json as input, when trying to deserialize to the following class structure, I get a kotlinx.serialization.MissingFieldException crash when using decodeFromJsonElement() as seen in the code example.

Here are the relevant data structures:

@Serializable
abstract class Resource {
    abstract val id: String
}

@Serializable
@SerialName("typeClassWithCyclicRelationship")
data class ClassWithCyclicRelationship(
    override val id: String,
    val field: Int,
    val parentRelationship: ClassWithCyclicRelationship?,
) : Resource()

global method

fun jsonFormat(
    setupSerializerModule: (SerializersModuleBuilder.() -> Unit)? = null,
) = Json {
    ignoreUnknownKeys = true
    explicitNulls = false
    serializersModule = SerializersModule {
        setupSerializerModule?.let { it() }
    }
}

test class that highlights the two cases

class JsonAPIDocumentTest {
private lateinit var json: Json

    @BeforeTest
    fun setup() {
        json = jsonFormat {
            polymorphic(Resource::class) {
                subclass(ClassWithCyclicRelationship::class)
            }
        }
    }

    @Test
    fun `decodingFromJsonElement crashes if there is a null parent reference`() {
        val transformedJSONDataObject = JsonObject(mapOf(
            "type" to JsonPrimitive("typeClassWithCyclicRelationship"),
            "id" to JsonPrimitive("1"),
            "field" to JsonPrimitive("123"),
            "parentRelationship" to JsonObject(mapOf(
                "type" to JsonPrimitive("typeClassWithCyclicRelationship"),
                "id" to JsonPrimitive("2"),
                "field" to JsonPrimitive("456"),
                "parentRelationship" to JsonNull
            )),

        ))

        val output = json.decodeFromJsonElement(Resource.serializer(), transformedJSONDataObject)

        assertEquals(
            ClassWithCyclicRelationship(
                id = "1",
                field = 123,
                parentRelationship = ClassWithCyclicRelationship(
                    id = "2",
                    field = 456,
                    parentRelationship = null,
                ),
            ),
            output,
        )
    }

    @Test
    fun `decodingFromJsonString works just fine given the exact same structure!`() {
        val transformedJSONDataObject = JsonObject(mapOf(
            "type" to JsonPrimitive("typeClassWithCyclicRelationship"),
            "id" to JsonPrimitive("1"),
            "field" to JsonPrimitive("123"),
            "parentRelationship" to JsonObject(mapOf(
                "type" to JsonPrimitive("typeClassWithCyclicRelationship"),
                "id" to JsonPrimitive("2"),
                "field" to JsonPrimitive("456"),
                "parentRelationship" to JsonNull
            )),

            ))

        val output = json.decodeFromString(Resource.serializer(),transformedJSONDataObject.toString())

        assertEquals(
            ClassWithCyclicRelationship(
                id = "1",
                field = 123,
                parentRelationship = ClassWithCyclicRelationship(
                    id = "2",
                    field = 456,
                    parentRelationship = null,
                ),
            ),
            output,
        )
    }
}

Full stacktrace

Fields [id, field, parentRelationship] are required for type with serial name 'typeClassWithCyclicRelationship', but they were missing
kotlinx.serialization.MissingFieldException: Fields [id, field, parentRelationship] are required for type with serial name 'typeClassWithCyclicRelationship', but they were missing
    at app//kotlinx.serialization.internal.PluginExceptionsKt.throwMissingFieldException(PluginExceptions.kt:20)
    at app//com.peakon.network.jsonapi.ClassWithCyclicRelationship.<init>(TestResources.kt:135)
    at app//com.peakon.network.jsonapi.ClassWithCyclicRelationship$$serializer.deserialize(TestResources.kt:135)
    at app//com.peakon.network.jsonapi.ClassWithCyclicRelationship$$serializer.deserialize(TestResources.kt:135)
    at app//kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:61)
    at app//kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:52)
    at app//kotlinx.serialization.internal.TaggedDecoder.decodeSerializableValue(Tagged.kt:207)
    at app//kotlinx.serialization.internal.TaggedDecoder$decodeNullableSerializableElement$1.invoke(Tagged.kt:289)
    at app//kotlinx.serialization.internal.TaggedDecoder.tagBlock(Tagged.kt:297)
    at app//kotlinx.serialization.internal.TaggedDecoder.decodeNullableSerializableElement(Tagged.kt:288)
    at app//com.peakon.network.jsonapi.ClassWithCyclicRelationship$$serializer.deserialize(TestResources.kt:135)
    at app//com.peakon.network.jsonapi.ClassWithCyclicRelationship$$serializer.deserialize(TestResources.kt:135)
    at app//kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:61)
    at app//kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:52)
    at app//kotlinx.serialization.json.internal.TreeJsonDecoderKt.readPolymorphicJson(TreeJsonDecoder.kt:33)
    at app//kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:71)
    at app//kotlinx.serialization.json.internal.AbstractJsonTreeDecoder.decodeSerializableValue(TreeJsonDecoder.kt:52)
    at app//kotlinx.serialization.json.internal.TreeJsonDecoderKt.readJson(TreeJsonDecoder.kt:25)
    at app//kotlinx.serialization.json.Json.decodeFromJsonElement(Json.kt:117)
    at app//com.peakon.network.jsonapi.JsonAPIDocumentTest.decodingFromJsonElement crashes if there is a null parent reference(JsonAPIDocumentTest.kt:201)
    at java.base@17.0.6/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base@17.0.6/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at java.base@17.0.6/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.base@17.0.6/java.lang.reflect.Method.invoke(Unknown Source)
    at app//org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at app//org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at app//org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at app//org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at app//org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at app//org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
    at app//org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
    at app//org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
    at app//org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    at app//org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    at app//org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    at app//org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    at app//org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    at app//org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at app//org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:108)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:40)
    at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:60)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:52)
    at java.base@17.0.6/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base@17.0.6/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at java.base@17.0.6/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.base@17.0.6/java.lang.reflect.Method.invoke(Unknown Source)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
    at jdk.proxy2/jdk.proxy2.$Proxy5.processTestClass(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
    at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)
    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)
    at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
    at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

Expected behavior That decodeFromJsonElement() behaves the same as decodeFromString().

Environment

RabieJradi commented 1 year ago

Never mind, seems like the bug is fixed in version 1.6.0! The above test cases are working as expected without the crash after updating to the latest library version