Serialize Sealed class within a data class using Gson in kotlin
Asked Answered
I

3

8

I have created a sealed class for the json field Value under CustomAttribute data class. This field can return String or Array of Strings.

How can we deserialize this sealed class from json?

data class CustomAttribute (
     val attributeCode: String,
     val value: Value 
)

sealed class Value {
      class StringArrayValue(val value: List<String>) : Value()
      class StringValue(val value: String)            : Value()
}
Inattentive answered 17/2, 2020 at 10:8 Comment(2)
Can i ask why do you need a value parameter that can be or a list or a single string value? I asked because im curios not to critizied, btw can this help or you already look at it? github.com/Kotlin/kotlinx.serialization/issues/103Shroudlaid
@Shroudlaid It was required as the API can return any of these values at a time. Thanks for the link but I still not able to solve this, can you please help in this..Inattentive
D
8

I created a TypeAdapterFactory implementation specifically to support sealed classes and their subtypes. This works similarly to the RuntimeTypeAdapterFactory (and I used it as a guide to write my class), but will specifically only support sealed types, and will deserialize using object instances of objects with a sealed class supertype (RuntimeTypeAdapterFactory will create a new instance of object types, which breaks equality checks when a single instance is the expectation).

private class SealedTypeAdapterFactory<T : Any> private constructor(
    private val baseType: KClass<T>,
    private val typeFieldName: String
) : TypeAdapterFactory {

    private val subclasses = baseType.sealedSubclasses
    private val nameToSubclass = subclasses.associateBy { it.simpleName!! }

    init {
        if (!baseType.isSealed) throw IllegalArgumentException("$baseType is not a sealed class")
    }

    override fun <R : Any> create(gson: Gson, type: TypeToken<R>?): TypeAdapter<R>? {
        if (type == null || subclasses.isEmpty() || subclasses.none { type.rawType.isAssignableFrom(it.java) }) return null

        val elementTypeAdapter = gson.getAdapter(JsonElement::class.java)
        val subclassToDelegate: Map<KClass<*>, TypeAdapter<*>> = subclasses.associateWith {
            gson.getDelegateAdapter(this, TypeToken.get(it.java))
        }
        return object : TypeAdapter<R>() {
            override fun write(writer: JsonWriter, value: R) {
                val srcType = value::class
                val label = srcType.simpleName!!
                @Suppress("UNCHECKED_CAST") val delegate = subclassToDelegate[srcType] as TypeAdapter<R>
                val jsonObject = delegate.toJsonTree(value).asJsonObject

                if (jsonObject.has(typeFieldName)) {
                    throw JsonParseException("cannot serialize $label because it already defines a field named $typeFieldName")
                }
                val clone = JsonObject()
                clone.add(typeFieldName, JsonPrimitive(label))
                jsonObject.entrySet().forEach {
                    clone.add(it.key, it.value)
                }
                elementTypeAdapter.write(writer, clone)
            }

            override fun read(reader: JsonReader): R {
                val element = elementTypeAdapter.read(reader)
                val labelElement = element.asJsonObject.remove(typeFieldName) ?: throw JsonParseException(
                    "cannot deserialize $baseType because it does not define a field named $typeFieldName"
                )
                val name = labelElement.asString
                val subclass = nameToSubclass[name] ?: throw JsonParseException("cannot find $name subclass of $baseType")
                @Suppress("UNCHECKED_CAST")
                return (subclass.objectInstance as? R) ?: (subclassToDelegate[subclass]!!.fromJsonTree(element) as R)
            }
        }
    }

    companion object {
        fun <T : Any> of(clz: KClass<T>) = SealedTypeAdapterFactory(clz, "type")
    }
}

Usage:

GsonBuilder().registerTypeAdapter(SealedTypeAdapterFactory.of(Value::class)).create()
Desiccate answered 7/2, 2023 at 19:36 Comment(0)
C
1

If the goal is just to serialize, the following code will do it. If you are stuck with an older Gson version and want to deserialize too, Jasons SealedTypeAdapterFactory works well.

private val serializer = JsonSerializer<MySuperclass> { src, _, context ->
   context!!.serialize(src)
      .also { it.asJsonObject.addProperty("type", src?.javaClass?.simpleName) }


val GSON: Gson = GsonBuilder()
    .registerTypeAdapter(MySuperclass::class.java, serializer)
    .create()
Chin answered 27/6, 2023 at 9:1 Comment(0)
V
-4

I have successfully serialized and de-serialized a sealed class in the past, with a disclaimer of using Jackson, not Gson as my serialization engine.

My sealed class has been defined as:

@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS, include = JsonTypeInfo.As.PROPERTY, visible = true)
sealed class FlexibleResponseModel
    class SnapshotResponse(val collection: List<EntityModel>): FlexibleResponseModel()
    class DifferentialResponse(val collection: List<EntityModel>): FlexibleResponseModel()
    class EventDrivenResponse(val collection: List<EntityEventModel>): FlexibleResponseModel()
    class ErrorResponse(val error: String): FlexibleResponseModel()

With the annotations used, it required no further configuration for the Jackson instance to properly serialize and de-serialize instances of this sealed class granted that both sides of the communication possessed a uniform definition of the sealed class.

While I recognise that JsonTypeInfo is a Jackson-specific annotation, perhaps you might consider switching over from Gson if this feature is a must - or you might be able to find an equivalent configuration for Gson which would also include the class identifier in your serialized data.

Ventral answered 17/2, 2020 at 23:17 Comment(3)
I am currently using Gson in the application so at this stage cannot change to Jackson. I wiill try to find its equivalent in gson.Inattentive
@AkashBisariya and did you find it?Allcot
No,i just made it nullable and check at runtime the type of value @AllcotInattentive

© 2022 - 2024 — McMap. All rights reserved.