I am trying to consume and emit JSON which contains a polymorphic list of items. The problem is: the items contain type
key with integer values (not strings). The API endpoint produces and expects JSON similar to this:
{
"startTime": "2022-07-27T13:32:57.379Z",
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
},
{
"type": 2,
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
}
],
"results": "string"
}
],
"status": 0,
"clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
As described in the article on polymorphism, I created an hierarchy of classes. I also try to convert type
value before deserialization.
object MyTransformingDeserializer : JsonTransformingSerializer<BaseItem>(PolymorphicSerializer(BaseItem::class)) {
override fun transformDeserialize(element: JsonElement): JsonElement {
val type = element.jsonObject["type"]!!
val newType = JsonPrimitive(value = type.toString())
return JsonObject(element.jsonObject.toMutableMap().also { it["type"] = newType })
}
}
@Serializable(with = MyTransformingDeserializer::class)
sealed class BaseItem {
abstract val type: String
}
@Serializable
@SerialName("0")
class ItemType0(
override val type: String,
// ...
) : BaseItem()
@Serializable
@SerialName("1")
class ItemType1(
override val type: String,
// ...
) : BaseItem()
@Serializable
@SerialName("2")
class ItemType2(
override val type: String,
// ...
) : BaseItem()
But all I get is this error:
kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for class discriminator '0'
Given that I can not change the format of the JSON, what can be done to successfully serialize/desereialize it?
CodePudding user response:
Handling polymorphism in Kotlinx Serialization is difficult, especially when you don't have control over the format of the source. But KxS does give a lot of low-level tools to manually handle almost anything.
You were close in choosing JsonTransformingSerializer
! It seems that it doesn't transform the JSON before KxS selects a serializer. Because discriminators can only be strings, and so deserialization fails.
JsonContentPolymorphicSerializer
Instead of JsonTransformingSerializer
, you can use JsonContentPolymorphicSerializer
.
Kotlinx Serialization will first deserialize the JSON to a JsonObject
. It will then provide that object to the serializer for BaseItem
, and you can parse and select the correct subclass.
import kotlinx.serialization.*
import kotlinx.serialization.json.*
object BaseItemSerializer : JsonContentPolymorphicSerializer<BaseItem>(BaseItem::class) {
override fun selectDeserializer(
element: JsonElement
): DeserializationStrategy<out BaseItem> {
return when (val type = element.jsonObject["type"]?.jsonPrimitive?.intOrNull) {
0 -> ItemType0.serializer()
1 -> ItemType1.serializer()
2 -> ItemType2.serializer()
else -> error("unknown Item type $type")
}
}
}
Including type
Since this is manually performing polymorphic discrimination, there's no need to include type
in your classes.
import kotlinx.serialization.Serializable
@Serializable(with = BaseItemSerializer::class)
sealed class BaseItem
@Serializable
data class ItemType0(
// ...
) : BaseItem()
@Serializable
class ItemType1(
// ...
) : BaseItem()
@Serializable
class ItemType2(
// ...
) : BaseItem()
However you might like to include it, for completeness, and so it's included when serializing. For that, you must use @EncodeDefault
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable
@Serializable(with = BaseItemSerializer::class)
sealed class BaseItem {
abstract val type: Int
}
@Serializable
class ItemType0(
// ...
) : BaseItem() {
@EncodeDefault
override val type: Int = 0
}
// ...
Complete example
Bringing it all together, here's a complete example.
import kotlinx.serialization.*
import kotlinx.serialization.json.*
val mapper = Json {
prettyPrint = true
prettyPrintIndent = " "
}
fun main() {
val json = """
{
"startTime": "2022-07-27T13:32:57.379Z",
"status": 0,
"clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
},
{
"type": 2,
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
}
],
"results": "string"
}
]
}
""".trimIndent()
val itemHolder: ItemHolder = mapper.decodeFromString(json)
println(itemHolder)
println(mapper.encodeToString(itemHolder))
}
@Serializable
data class ItemHolder(
val startTime: String,
val clientId: String,
val status: Int,
val items: List<BaseItem>,
)
@Serializable(with = BaseItem.Serializer::class)
sealed class BaseItem {
abstract val type: Int
object Serializer : JsonContentPolymorphicSerializer<BaseItem>(BaseItem::class) {
override fun selectDeserializer(
element: JsonElement
): DeserializationStrategy<out BaseItem> {
return when (val type = element.jsonObject["type"]?.jsonPrimitive?.intOrNull) {
0 -> ItemType0.serializer()
1 -> ItemType1.serializer()
2 -> ItemType2.serializer()
else -> error("unknown Item type $type")
}
}
}
}
@Serializable
data class ItemType0(
val results: String,
val restBetweenRounds: String,
) : BaseItem() {
@EncodeDefault
override val type: Int = 0
}
@Serializable
data class ItemType1(
val results: String,
val setCount: Int,
) : BaseItem() {
@EncodeDefault
override val type: Int = 1
}
@Serializable
data class ItemType2(
val results: String,
val items: List<BaseItem>,
) : BaseItem() {
@EncodeDefault
override val type: Int = 2
}
This prints
ItemHolder(
startTime=2022-07-27T13:32:57.379Z,
clientId=3fa85f64-5717-4562-b3fc-2c963f66afa6,
status=0,
items=[
ItemType0(results=string, restBetweenRounds=string),
ItemType1(results=string, setCount=0),
ItemType2(
results=string,
items=[
ItemType0(results=string, restBetweenRounds=string),
ItemType1(results=string, setCount=0)
]
)
]
)
and
{
"startTime": "2022-07-27T13:32:57.379Z",
"clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": 0,
"items": [
{
"results": "string",
"restBetweenRounds": "string",
"type": 0
},
{
"results": "string",
"setCount": 0,
"type": 1
},
{
"results": "string",
"items": [
{
"results": "string",
"restBetweenRounds": "string",
"type": 0
},
{
"results": "string",
"setCount": 0,
"type": 1
}
],
"type": 2
}
]
}
which matches the input!
Versions
- Kotlin 1.7.10
- Kotlinx Serialization 1.3.4