can I use kotlinx serializer with multiple sealed class levels as parents and a nested invocation? - kotlin

I am trying to use kotlinx #Serializable and Ive faced this issue:
I have the following classes:
#Serializable
sealed class GrandParent
a second one:
#Serializable
sealed class Parent() : GrandParent() {
abstract val id: String
}
and a third one
#Serializable
data class Child(
override val id: String, ....
): Parent()
I'm needing of grandparent since I use it as a generic type in another class, which happen to also have a reference to the GrandParent class
#Serializable
data class MyContent(
override val id: String,
....
val data: GrandParent, <- so it has a self reference to hold nested levels
...): Parent()
Every time I try to run this I get an error...
Class 'MyContent' is not registered for polymorphic serialization in the scope of 'GrandParent'.
Mark the base class as 'sealed' or register the serializer explicitly.
I am using ktor as wrapper, kotlin 1.5.10. I did this based on https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#registered-subclasses
Any ideas?

You should serialize and deserialize using your sealed class in order for kotlin serialization to "know" to add a discriminator with the right implementation. By default it search for type in the json but you can change it with JsonBuilder:
Json {
classDiscriminator = "class"
}
Here is an example:
#Serializable
sealed class GrandParent
#Serializable
sealed class Parent : GrandParent() {
abstract val id: String,
}
#Serializable
data class Child(
override val id: String,
): Parent()
#Serializable
data class MyContent(
override val id: String,
val data: GrandParent,
): Parent()
fun main() {
val test = MyContent(id = "test", data = Child(id = "child"))
val jsonStr = Json.encodeToString(GrandParent.serializer(), test)
println("Json string: $jsonStr")
val decoded = Json.decodeFromString(GrandParent.serializer(), jsonStr)
println("Decoded object: $decoded")
}
Result in console:
Json string: {"type":"MyContent","id":"test","data":{"type":"Child","id":"child"}}
Decoded object: MyContent(id=test, data=Child(id=child))
encode and decode can also be written like this (but behind the scenes it will use reflections):
val jsonStr = Json.encodeToString<GrandParent>(test)
println("Json string: $jsonStr")
val decoded = Json.decodeFromString<GrandParent>(jsonStr)
println("Decoded object: $decoded")

Related

Serialization with sealed classes fails in Kotlin serialization

I'm having trouble with kotlin-serialization in the following use case:
#Serializable
sealed class NetworkAnswer {
#SerialName("answerId")
abstract val id: Int
}
#Serializable
data class NetworkYesNoAnswer(
override val id: Int,
#SerialName("isPositive")
val isPositive: Boolean
) : NetworkAnswer()
When I serialize this:
val json = Json { ignoreUnknownKeys = true; explicitNulls = false }
val result: NetworkYesNoAnswer = json.decodeFromString(NetworkYesNoAnswer.serializer(), """
{
"answerId": 1,
"isPositive": true
}
""".trimIndent()
)
I get the following error
Caused by: kotlinx.serialization.MissingFieldException: Fields [id] are required for type with serial name 'NetworkYesNoAnswer', but they were missing
The only way the serialization works is if I use the same name for both the member and "SerialName", like so:
#Serializable
sealed class NetworkAnswer {
#SerialName("answerId")
abstract val answerId: Int
}
#Serializable
data class NetworkYesNoAnswer(
override val answerId: Int,
#SerialName("isPositive")
val isPositive: Boolean
) : NetworkAnswer()
This kinda defeats the purpose of "SerialName", is there a way to solve that without using the same name?
Declaring a #SerialName on a base class has no effect on member declarations overridden by child classes.
Instead, you can declare #SerialName on the child class instead. There is no need to change the actual name of the field.
#Serializable
data class NetworkYesNoAnswer(
#SerialName("answerId")
override val id: Int,
#SerialName("isPositive")
val isPositive: Boolean
) : NetworkAnswer()
Declaring the #SerialName on the base class and applying it to all children seems NOT to be supported as of now, but is desired by other members of the community as well, e.g. here on GitHub.
OT: Most likely you could use a sealed interface, which was first introduced in Kotlin v1.5.0, instead of a sealed class.

How to use custom field in polymorphic Json deserialization using kotlinx.serialization

I have the following data structure that I want to deserialize:
#Serializable
data class SearchResponse(val results: List<SearchResultContainer>) {
#Serializable
data class SearchResultContainer(
val type: ResultType,
val result: SearchResult
)
#Serializable
enum class ResultType {
SERIES, SERIES_CRUMB, EPISODE, CHANNEL
}
#Serializable
sealed interface SearchResult
#Serializable
data class SeriesSearchResult(
val id: String,
val name: String,
val description: String,
val image: String
) : SearchResult
// ...
I want to deserialize concrete SearchResult based on enum - ResultType.
Do I need to register custom serializer for this?

Moshi PolymorphicJsonAdapter for nested sealed classes maybe not generating adapter correctly

When a subclass of a sealed class has a property that is a subclass of a different sealed class the generated adapter does not recognize the correct types. For example:
Given the following classes:
sealed class Operation(
val id: Long,
val params: Params?,
) {
sealed class Params()
}
sealed class SpecialOperation(
id: Long,
params: SpecialOperation.Params?,
) : Operation(id, params) {
class Params : Operation.Params()
}
The generated adapter for the SpecialOperation class constructs an instance of Operation.Params instead of an instance of SpecialOperation.Params which then throws an exception: Type mismatch: inferred type is Operation.Params? but SpecialOperation.Params? was expected.
I was able to work around this behavior by making the properties from the super sealed class Operation open and overriding them in the subclass SpecialOperation like the following:
sealed class Operation(
open val id: Long,
open val params: Params?,
) {
...
}
sealed class SpecialOperation(
override val id: Long,
override val params: SpecialOperation.Params?,
) : Operation(id, params) {
...
}
In this situation shouldn't the generated adapter check the type of the constructor argument instead of the type of the property on the superclass as one is a subtype of the other?

Proper way to serialize a sealed class with kotlinx-serialization

I am not sure if it is possible yet but i would like to serialize the following class.
#Serializable
sealed class RestResponseDTO<out T : Any>{
#Serializable
#SerialName("Success")
class Success<out T : Any>(val value: T) : RestResponseDTO<T>()
#Serializable
#SerialName("Failure")
class Error(val message: String) : RestResponseDTO<String>()
}
when i try and use it
route(buildRoute(BookDTO.restStub)) {
get {
call.respond(RestResponseDTO.Success(BookRepo.getAll()))
}
}
I get this error:
kotlinx.serialization.SerializationException: Serializer for class
'Success' is not found. Mark the class as #Serializable or provide the
serializer explicitly.
The repo mentioned in the get portion of the route returns a list of BookDTO
#Serializable
data class BookDTO(
override val id: Int,
override val dateCreated: Long,
override val dateUpdated: Long,
val title: String,
val isbn: String,
val description: String,
val publisher:DTOMin,
val authors:List<DTOMin>
):DTO {
override fun getDisplay() = title
companion object {
val restStub = "/books"
}
}
This problem is not a deal breaker but it would be great to use an exhaustive when on my ktor-client.
Serializing sealed classes works just fine. What is blocking you are the generic type parameters.
You probably want to remove those, and simply use value: DTO. Next, make sure to have all subtypes of DTO registered for polymorphic serialization in the SerializersModule.

How to serialize kotlin sealed class with open val using kotlinx serialization

import kotlinx.serialization.Serializable
#Serializable
sealed class Exercise(open val id: String) {
#Serializable
data class Theory(override val id: String) : Exercise(id)
}
I have such kind of sealed class in my code, and compiler says me:
Serializable class has duplicate serial name of property 'id', either in the class itself or its supertypes.
Is there way to have open val in serializable sealed class, which works correctly when overriding it?
This is Kotlin issue KT-38958. It seems to be a corner case of the Constructor properties requirement.
It can be solved by using the following implementation,
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
#Serializable
sealed class Exercise {
abstract val id: String
#Serializable
data class Theory(override val id: String) : Exercise()
}
fun main() {
val t1 = Exercise.Theory("t1")
val t1Json = Json.encodeToString(t1)
println(t1Json)
println(Json.decodeFromString<Exercise.Theory>(t1Json).toString())
}
which will output:
{"id":"t1"}
Theory(id=t1)
For details, see "Designing serializable hierarchy" in the Kotlin Serialization Guide.