question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Decoding nested generics data class.

See original GitHub issue

Hi 👋

I’m trying to decode the following Json using kotlinx.serialization and I am running into some issues.

The JSON:

{
    "fill": 0.0
}

Can also be represented as:

{
    "fill": [0.0, 0.0, 0.0, 0.0]
}

The data classes:

/// Fill.kt

@Serializable(FillSerializer::class)
public data class Fill<T>(@SerialName("fill") val insets: Inset<T>) {
    companion object
}

@Serializer(forClass = Fill::class)
class FillSerializer<T>(private val fillSerializer: KSerializer<T>): KSerializer<Fill<T>> {

    override val descriptor: SerialDescriptor = object : SerialClassDescImpl("FillSerializer") {
        init {
            addElement("fill")
        }
    }

    override fun deserialize(decoder: Decoder): Fill<T> {
        val inp = decoder.beginStructure(descriptor)
        val key = inp.decodeStringElement(descriptor, 0)
        val insets = inp.decodeSerializableElement(descriptor, 0, fillSerializer)
        inp.endStructure(descriptor)
        return Fill(insets)
    }

    override fun serialize(encoder: Encoder, obj: Fill<T>) {
        print(obj)
    }
}

/// Inset.kt

@Serializable(InsetSerializer::class)
data class Inset<T>(val start: T,
                    val end: T,
                    val top: T,
                    val bottom: T): WithDefault {

    constructor(array: Array<T>) : this(array[0], array[1], array[2], array[3])

}

@Serializer(forClass = Inset::class)
class InsetSerializer<T>(private val serializer: KSerializer<T>): KSerializer<Inset<T>> {

    override val descriptor: SerialDescriptor
        get() = StringDescriptor.withName("InsetSerializer")

    override fun deserialize(decoder: Decoder): Inset<T> {

        val decoded = decoder.decode(serializer)
            .guard {
                throw Exception("Some exception sonny.")
            }

        return when (decoded) {
            is List<*> -> {
                Inset(decoded.toTypedArray() as Array<T>)
            }
            is Float -> {
                Inset(decoded, decoded, decoded, decoded)
            }
            else -> throw Exception("WTF.")
        }
    }

    override fun serialize(encoder: Encoder, obj: Inset<T>) {

    }
}

I wrote the above following code to get something workable, Inset<T> by itself parses fine. The following tests pass:

final class InsetTests {

    val insetString = "0.0"
    val insetArrayString = "[0.0, 0.0, 0.0, 0.0]"
    val variedInsetArrayString = "[0.0, 12.0, 24.0, 18]"

    @Test
    fun testInsetString() {
        val json = Json(JsonConfiguration.Stable)
        val insets = json.parse(Inset.serializer(FloatSerializer), insetString)
        assert(insets == Inset(0.0f, 0.0f, 0.0f, 0.0f))
    }

    @Test
    fun testInsetArrayString() {
        val json = Json(JsonConfiguration.Stable)
        val insets = json.parse(Inset.serializer(ArrayListSerializer(FloatSerializer)), insetArrayString)
        assert(insets == Inset(0.0f, 0.0f, 0.0f, 0.0f))
    }

    @Test
    fun testVariedInsetArrayString() {
        val json = Json(JsonConfiguration.Stable)
        val insets = json.parse(Inset.serializer(ArrayListSerializer(FloatSerializer)), variedInsetArrayString)
        assert(insets == Inset(0.0f, 12.0f, 24.0f, 18.0f))
    }

}

Tests for Fill fail however:

final class FillTests {

    val fillJSONExample1: String = """
        {
            "fill": 0.0
        }
    """.trimIndent()

    val fillJSONExample2: String = """
        {
            "fill": [0.0, 0.0, 0.0, 0.0] 
        }
    """.trimIndent()


    @Test
    fun testParsingExample1() {
        val json = Json(JsonConfiguration.Stable)
        val parsedObject = json.parse(
            Fill.serializer(
                Inset.serializer(FloatSerializer)
            ),
            fillJSONExample1
        )
        print(parsedObject)
    }

}

With the following exception:

kotlinx.serialization.json.JsonParsingException: Invalid JSON at 12: Expected string or non-null literal

	at kotlinx.serialization.json.internal.JsonReader.takeString(JsonReader.kt:337)
	at kotlinx.serialization.json.internal.StreamingJsonInput.decodeString(StreamingJsonInput.kt:111)
	at kotlinx.serialization.ElementValueDecoder.decodeStringElement(ElementWise.kt:139)
	at com.ancestry.layoutcomposer.values.FillSerializer.deserialize(Layout.Fill.kt:48)
	at com.ancestry.layoutcomposer.values.FillSerializer.deserialize(Layout.Fill.kt:36)
	at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:33)
	at kotlinx.serialization.json.internal.StreamingJsonInput.decodeSerializableValue(StreamingJsonInput.kt:29)
	at kotlinx.serialization.CoreKt.decode(Core.kt:79)
	at kotlinx.serialization.json.Json.parse(Json.kt:152)
	at com.ancestry.layoutcomposer.FillTests.testParsingExample1(Fill.Tests.kt:29)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

What is the correct way to parse that dynamic json?

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:7 (3 by maintainers)

github_iconTop GitHub Comments

2reactions
sandwwraithcommented, Sep 23, 2019

I believe it would easier to use cast in deserializer to Json-specific JsonInput to accomplish you goal. Because, as @pdvrieze correctly mentioned, kotlinx.serialization is a format-agnostic framework and catching exceptions may break the internal state of some parsers. JsonInput has specific decodeJson method to read json into abstract tree. See this example with Either: https://github.com/kotlin/kotlinx.serialization/blob/45aa7c7ab97f25f205408b46a7069886408bce6b/runtime/commonTest/src/kotlinx/serialization/json/JsonTreeAndMapperTest.kt#L36

So, your deserializer will look like this:

val input = decoder as? JsonInput ?: throw SerializationException("This class can be loaded only by Json")
val tree = input.decodeJson()
when(tree) {
  is JsonPrimitive -> // work with tree.content: String
  is JsonArray -> // do other stuff
}
0reactions
qoomoncommented, Jun 10, 2020

That is my implementation

import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonInput

@Serializer(forClass = List::class)
class SingleElementListSerializer<T : Any>(private val dataSerializer: KSerializer<T>) : KSerializer<List<T>> {

    override val descriptor: SerialDescriptor = SerialDescriptor(serialName = "SingleElementListSerializer")

    private val dataListSerializer = ListSerializer(dataSerializer)

    override fun deserialize(decoder: Decoder): List<T> {
        decoder as? JsonInput ?: throw IllegalStateException(
                "This serializer can be used only with Json format." +
                    "Expected Decoder to be JsonInput, got ${this::class}"
            )
        return when (val jsonElement = decoder.decodeJson()) {
            is JsonArray -> decoder.json.fromJson(dataListSerializer, jsonElement)
            else -> listOf(decoder.json.fromJson(dataSerializer, jsonElement))
        }
    }

    override fun serialize(encoder: Encoder, value: List<T>) {
        return when (value.size) {
            1 -> encoder.encode(dataSerializer, value.single())
            else -> encoder.encode(dataListSerializer, value)
        }
    }
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Swift - JSONDecoder - decoding generic nested classes
I want to use JSONEncoder and JSONDecoder to send and receive objects. I have an issue when decoding generic classes inside another object...
Read more >
Using generics to load any kind of Codable data
Using generics to load any kind of Codable data ... This is called a nested struct, and is simply one struct placed inside...
Read more >
Decoding nested values with property wrappers - Ilya Puchka
So let's see how property wrappers can be used to solve one decoding edge case as an example - decoding deeply nested values....
Read more >
Using generics to load any kind of Codable data - YouTube
Download the completed project here: https://github.com/twostraws/hackingwithswiftOther parts in Project 8:Introduction: ...
Read more >
Encoding and decoding - circe
It also provides instances for List[A] , Option[A] , and other generic types, but only if A has an Encoder instance. Encoding data...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found