Reputation: 1396
I'm using the 1.0.0 version of kotlin serialization but I'm stuck when I try to deserialize a "flexible" array.
From the Backend API that I don't control I get back an JSON Array that holds different types of objects. How would you deserialize them using kotlin serialization?
Example
This is the API's response
[
{
"id": "test",
"person": "person",
"lastTime": "lastTime",
"expert": "pro"
},
{
"id": "test",
"person": "person",
"period": "period",
"value": 1
}
]
@Serializable
sealed class Base {
@SerialName("id")
abstract val id: String
@SerialName("person")
abstract val person: String
}
@Serializable
data class ObjectA (
@SerialName("id") override val id: String,
@SerialName("title") override val title: String,
@SerialName("lastTime") val lastTime: String,
@SerialName("expert") val expert: String
) : Base()
@Serializable
data class ObjectB (
@SerialName("id") override val id: String,
@SerialName("title") override val title: String,
@SerialName("period") val period: String,
@SerialName("value") val value: Int
) : Base()
Performing the following code result in an error
println(Json.decodeFromString<List<Base>>(json))
error Polymorphic serializer was not found for class discriminator
Upvotes: 1
Views: 3178
Reputation: 3194
In case it's not clear to you even after reading the answers multiple times, I'd like to add a little more explanation.
Suppose you have a JSON string as the following.
val s1 = """
[
{
"type": "string_class",
"id": 1,
"string": "hello"
},
{
"type": "number_class",
"id": 1,
"number": 100
}
]
""".trimIndent()
Each item in the list looks different (i.e. one has "string" and the other has "number"), and "type" is what distinguishes them.
val json = Json {
classDiscriminator = "type"
}
@Serializable
sealed class Base {
abstract val id: Int
@Serializable
@SerialName("string_class")
data class StringClass(
override val id: Int,
val string: String,
) : Base()
@Serializable
@SerialName("number_class")
data class NumberClass(
override val id: Int,
val number: Int,
) : Base()
}
This works well, as long as each item has "type" that we can use to distinguish one class from the other. But if there is no "type" you can leverage, you'd need to create a custom Serializer
.
val string1 = """
[
{
"id": 1,
"string": "Hello"
},
{
"id": 1,
"number": 100
}
]
""".trimIndent()
You'd need to check what fields are available, and choose the right Serializer.
object BaseSerializer : JsonContentPolymorphicSerializer<Base>(Base::class) {
override fun selectDeserializer(
element: JsonElement,
): DeserializationStrategy<Base> {
val jsonObject = element.jsonObject
return when {
jsonObject.containsKey("string") -> Base.StringClass.serializer()
jsonObject.containsKey("number") -> Base.NumberClass.serializer()
else -> throw SerializationException("not supported")
}
}
}
@Serializable(BaseSerializer::class)
sealed class Base {
abstract val id: Int
@Serializable
data class StringClass(
override val id: Int,
val string: String,
) : Base()
@Serializable
data class NumberClass(
override val id: Int,
val number: Int,
) : Base()
}
Upvotes: 0
Reputation: 1396
@cactustictacs solution came very close. He said that "By default sealed classes are handled by adding a type field to the JSON"
But because I didn't had a type
property I needed a other field that decides which subclass it should be.
In Kotlin Serializer you can do that by
val format = Json {
classDiscriminator = "PROPERTY_THAT_DEFINES_THE_SUBCLASS"
}
val contentType = MediaType.get("application/json")
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(format.asConverterFactory(contentType))
.build()
where in classDiscriminator
you can enter the property that you want. Hope this helps other people in the future.
Upvotes: 1
Reputation: 19524
When you say you don't control the API, is that JSON being generated from your code by the Kotlin serialization library? Or is it something else you want to wrangle into your own types?
By default sealed classes are handled by adding a type
field to the JSON, which you have in your objects, but it's a property in your Base
class. In the next example it shows you how you can add a @SerialName("owned")
annotation to say what type
value each class corresponds to, which might help you if you can add the right one to your classes? Although in your JSON example both objects have "type" as their type
...
If you can't nudge the API response into the right places, you might have to write a custom serializer (it's the deserialize part you care about) to parse things and identify what each object looks like, and construct the appropriate one.
(I don't know a huge amount about the library or anything, just trying to give you some stuff to look at, see if it helps!)
Upvotes: 1