Kotlinx.serialization: Polymorphic serialization: properties named "type" are not serialized with a unique key

Created on 16 May 2019  路  6Comments  路  Source: Kotlin/kotlinx.serialization

Describe the bug
When using the polymorphic serializer, if a class has a property called type, it will be serialized with two keys called type -- one with the class name, and one with the value of the class property.

To Reproduce

class SampleTests {
    @Serializable
    abstract class Base

    @Serializable
    data class Derived(val type: String) : Base()

    @Test
    fun test() {
        val j = Json {
            serialModule = SerializersModule {
                polymorphic(Base::class) {
                    addSubclass(Derived.serializer())
                }
            }
        }

        val derived = Derived("string")
        val stringified = j.stringify(Base.serializer(), derived)
        val parsed = j.parse(Base.serializer(), stringified) // throws

        assertEquals(
            derived,
            parsed
        )
    }
}

Expected behavior
An error should be emitted that the key type is reserved, with instructions to use SerialName. It could also be useful to allow the discriminator property name to be configurable

Environment

  • Kotlin version: [1.3.30]
  • Library version: [0.11.0]
  • Kotlin platforms: [e.g. JVM, JS, and Native]
  • Gradle version: [e.g. 5.4]
question

Most helpful comment

Discriminator name is configurable, see in JsonConfiguration class

All 6 comments

Discriminator name is configurable, see in JsonConfiguration class

The system that receives my json output runs validation and rejects unknown fields - so I can't use the discriminator to simply rename the default "type" field to something else.

Is there a way to configure the polymorphic serializer to use my "type" field, instead of the auto generated fully-qualified class name?

Value for the discriminator field is assigned from @SerialName on class (which is FQ name by default). You can reassign them individually, but unfortunately, there is no global policy yet.

So as an example I"ll reference use case in 380 with an added "type" field

with 0.11, we can now do the following:

@Serializable
sealed class Shape(val type: String)
@Serializable
data class Circle(val radius: Double) : Shape("CIRCLE")
@Serializable
data class Rectangle(val width: Double) : Shape("RECTANGLE")

@Serializable
data class Box(val shapes: List<@Polymorphic Shape>)

private val shapeModule = SerializersModule {
    polymorphic(Shape::class) {
        Circle::class with Circle.serializer()
        Rectangle::class with Rectangle.serializer()
    }
}

val box = Box(listOf(Circle(3.5), Rectangle(4.2)))
val json = Json(context = shapeModule).stringify(Box.serializer(), box)

this yields the following output:

{
  "shapes": [
    {
      "type":"com.example.shape.Circle",
      "type": "CIRCLE",
      "radius": 3.5
    },
    {
      "type":"com.example.shape.Rectangle",
      "type": "RECTANGLE",
      "width": 4.2
    }
  ]
}

I understand that we can rename the class discriminator field to something else, so that the output would instead be "something_else": "com.example.shape.Circle", but is there way to tell the kotlin serializer how to use our existing "type" field? Something like:

private val shapeModule = SerializersModule {
    polymorphic(Shape::class) {
        Circle::class with Circle.serializer() on "CIRCLE"
        Rectangle::class with Rectangle.serializer() on "RECTANGLE"
    }
}

the goal being to have the output:

{
  "shapes": [
    {
      "type": "CIRCLE",
      "radius": 3.5
    },
    {
      "type": "RECTANGLE",
      "width": 4.2
    }
  ]
}

Nevermind, I just understood what you meant.

The following yields exactly what I need. Awesome!

@Serializable
sealed class Shape

@Serializable
@SerialName("CIRCLE")
data class Circle(val radius: Double) : Shape()

@Serializable
@SerialName("RECTANGLE")
data class Rectangle(val width: Double) : Shape()

Now sealed classes are supported automatically and there is the same problem:

package com

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration

@Serializable
sealed class Base

@Serializable
data class Child(val type: String) : Base()

object Tmp {

  @JvmStatic
  fun main(args: Array<String>) {
    val json = Json(JsonConfiguration.Stable)

    val child = Child("1")

    val string = json.stringify(Base.serializer(), child)
    println(string)

    val parsed = json.parse(Base.serializer(), string)

    println(parsed)
  }
}

The output:

{"type":"com.Child","type":"1"}
Exception in thread "main" kotlinx.serialization.SerializationException: 1 is not registered for polymorphic serialization in the scope of class com.Base
    at kotlinx.serialization.internal.AbstractPolymorphicSerializerKt.throwSubtypeNotRegistered(AbstractPolymorphicSerializer.kt:99)
    at kotlinx.serialization.internal.AbstractPolymorphicSerializerKt.access$throwSubtypeNotRegistered(AbstractPolymorphicSerializer.kt:1)
    at kotlinx.serialization.internal.AbstractPolymorphicSerializer.findPolymorphicSerializer(AbstractPolymorphicSerializer.kt:83)
    at kotlinx.serialization.SealedClassSerializer.findPolymorphicSerializer(SealedSerializer.kt:92)
    at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:41)
    at kotlinx.serialization.json.internal.StreamingJsonInput.decodeSerializableValue(StreamingJsonInput.kt:33)
    at kotlinx.serialization.CoreKt.decode(Core.kt:80)
    at kotlinx.serialization.json.Json.parse(Json.kt:126)
    at com.Tmp.main(Tmp.kt:24)
Was this page helpful?
0 / 5 - 0 ratings