zt

    zt

    4 months ago
    How would I write a custom serializer for json input that looks like this
    [ { "somekey": { } }, { "someotherkey": { } } ]
    I wanna be able to check the name of the first key in the object and then serialize to a specific class depending on what it is
    Landry Norris

    Landry Norris

    4 months ago
    If you have control over the input, you can create a sealed class and subclass it, marking each subclass with SerialName. This will create a ‘type’ field that will tell the serializer which subclass to deserialize. It won’t require a custom serializer.
    Your json would look like
    [ { "type": "MySubclass", "field1": "value1" }, { "type": "MyOtherClass", "anotherField": "value2" }, { "type": "ClassWithNoFields" } ]
    zt

    zt

    4 months ago
    unfortunately i dont have control over the input
    e

    ephemient

    4 months ago
    @Serializable(with = Foo.Serializer::class)
    sealed class Foo {
        @Serializable
        object Bar : Foo()
        @Serializable
        object Baz : Foo()
    
        object Serializer : KSerializer<Foo> {
            override val descriptor: SerialDescriptor = SerialDescriptor("Foo", Surrogate.serializer().descriptor)
            override fun serialize(encoder: Encoder, value: Foo) {
                encoder.encodeSerializableValue(
                    Surrogate.serializer(),
                    Surrogate(
                        bar = value as? Bar,
                        baz = value as? Baz,
                    )
                )
            }
            override fun deserialize(decoder: Decoder): Foo {
                val surrogate = decoder.decodeSerializableValue(Surrogate.serializer())
                return surrogate.bar ?: surrogate.baz ?: throw SerializationException("missing key")
            }
        }
    
        @Serializable
        private data class Surrogate(
            val bar: Bar? = null,
            val baz: Baz? = null,
        )
    }
    
    Json.decodeFromString<List<Foo>>("""[{"bar":{}},{"baz":{}}]""") == listOf(Foo.Bar, Foo.Baz)
    r

    ribesg

    4 months ago
    Basically deserialize to JsonObject, check for your field in the JsonObject, then deserialize the JsonObject to whatever model matches
    But please note that there is no such thing as a “first key” in a json object.
    In json
    { "a": 0, "b": 1 }
    and
    { "b": 1, "a": 0 }
    are strictly identical
    e

    ephemient

    4 months ago
    no need to go through JsonObject, the serializer I wrote above will work for all formats
    r

    ribesg

    4 months ago
    I don’t like how your solution scales if you have 50 keys though 😄
    e

    ephemient

    4 months ago
    yeah, that would be a pain. at that point you would either do some tricks to construct a delegate based on the PolymorphicSerializer directly, or use a JsonTransformingSerializer; parsing it directly from Json is not going to be better
    e.g.
    object FooJsonSerializer : JsonTransformingSerializer<Foo>(Foo.serializer()) {
        private val discriminator = Foo::class.findAnnotation<JsonClassDiscriminator>()?.discriminator ?: "type"
    
        override fun transformSerialize(element: JsonElement): JsonElement = buildJsonObject {
            put(element.jsonObject[discriminator]!!.jsonPrimitive.content, JsonObject(element.jsonObject - discriminator))
        }
    
        override fun transformDeserialize(element: JsonElement): JsonElement = buildJsonObject {
            val (type, value) = element.jsonObject.entries.single()
            put(discriminator, type)
            for ((key, subvalue) in value.jsonObject) put(key, subvalue)
        }
    }
    (error-checking etc. omitted)