简体   繁体   中英

Serialize sealed class with Moshi

The following will produce an IllegalArgumentException because you "Cannot serialize abstract class"

sealed class Animal {
    data class Dog(val isGoodBoy: Boolean) : Animal()
    data class Cat(val remainingLives: Int) : Animal()
}

private val moshi = Moshi.Builder()
    .build()

@Test
fun test() {
    val animal: Animal = Animal.Dog(true)
    println(moshi.adapter(Animal::class.java).toJson(animal))
}

I have tried solving this using a custom adapter, but the only solution I could figure out involves explicitly writing all of the property names for each subclass. eg:

class AnimalAdapter {
    @ToJson
    fun toJson(jsonWriter: JsonWriter, animal: Animal) {
        jsonWriter.beginObject()
        jsonWriter.name("type")
        when (animal) {
            is Animal.Dog -> jsonWriter.value("dog")
            is Animal.Cat -> jsonWriter.value("cat")
        }

        jsonWriter.name("properties").beginObject()
        when (animal) {
            is Animal.Dog -> jsonWriter.name("isGoodBoy").value(animal.isGoodBoy)
            is Animal.Cat -> jsonWriter.name("remainingLives").value(animal.remainingLives)
        }
        jsonWriter.endObject().endObject()
    }

    ....
}

Ultimately I'm looking to produce JSON that looks like this:

{
    "type" : "cat",
    "properties" : {
        "remainingLives" : 6
    }
}
{
    "type" : "dog",
    "properties" : {
        "isGoodBoy" : true
    }
}

I'm happy with having to use the custom adapter to write the name of each type, but I need a solution that will automatically serialize the properties for each type rather than having to write them all manually.

This can be done with PolymorphicJsonAdapterFactory and including an extra property in the json to specify the type.

For example:

This JSON

{
  "animals": [
    { 
        "type": "dog",
        "isGoodBoy": true
    },
    {
        "type": "cat",
        "remainingLives": 9
    }    
  ]
}

Can be mapped to the following classes

sealed class Animal {
    @JsonClass(generateAdapter = true)
    data class Dog(val isGoodBoy: Boolean) : Animal()

    @JsonClass(generateAdapter = true)
    data class Cat(val remainingLives: Int) : Animal()

    object Unknown : Animal()
}

With the following Moshi config

Moshi.Builder()
    .add(
        PolymorphicJsonAdapterFactory.of(Animal::class.java, "type")
            .withSubtype(Animal.Dog::class.java, "dog")
            .withSubtype(Animal.Cat::class.java, "cat")
            .withDefaultValue(Animal.Unknown)
    )

I think you need the polymorphic adapter to achieve this which requires the moshi-adapters artifact. This will enable serialization of sealed classes with different properties. More details are in this article here: https://proandroiddev.com/moshi-polymorphic-adapter-is-d25deebbd7c5

I have solved this by creating a Factory, an enclosing class, and an enum that can provide the classes for each item type. However this feels rather clunky and I would love a more straight forward solution.

data class AnimalObject(val type: AnimalType, val properties: Animal)

enum class AnimalType(val derivedClass: Class<out Animal>) {
    DOG(Animal.Dog::class.java),
    CAT(Animal.Cat::class.java)
}

class AnimalFactory : JsonAdapter.Factory {
    override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<AnimalObject>? {
        if (!Types.getRawType(type).isAssignableFrom(AnimalObject::class.java)) {
            return null
        }

        return object : JsonAdapter<AnimalObject>() {
            private val animalTypeAdapter = moshi.adapter<AnimalType>(AnimalType::class.java)

            override fun fromJson(reader: JsonReader): AnimalObject? {
                TODO()
            }

            override fun toJson(writer: JsonWriter, value: AnimalObject?) {
                writer.beginObject()
                writer.name("type")
                animalTypeAdapter.toJson(writer, value!!.type)
                writer.name("properties")
                moshi.adapter<Animal>(value.type.derivedClass).toJson(writer, value.properties)
                writer.endObject()
            }
        }
    }
}

Answer is taken from: github.com/square/moshi/issues/813

You should be able to create your own JsonAdapter.Factory and provide custom adapter whenever an Animal need to be serialized/deserialized:

sealed class Animal {
    @JsonClass(generateAdapter = true)
    data class Dog(val isGoodBoy: Boolean) : Animal()

    @JsonClass(generateAdapter = true)
    data class Cat(val remainingLives: Int) : Animal()
}

object AnimalAdapterFactory : JsonAdapter.Factory {
    override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? =
        when (type) {
            Animal::class.java -> AnimalAdapter(moshi)
            else -> null
        }

    private class AnimalAdapter(moshi: Moshi) : JsonAdapter<Animal>() {

        private val mapAdapter: JsonAdapter<MutableMap<String, Any?>> =
            moshi.adapter(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java))
        private val dogAdapter = moshi.adapter(Animal.Dog::class.java)
        private val catAdapter = moshi.adapter(Animal.Cat::class.java)

        override fun fromJson(reader: JsonReader): Animal? {
            val mapValues = mapAdapter.fromJson(reader)
            val type = mapValues?.get("type") ?: throw Util.missingProperty("type", "type", reader)
            val properties = mapValues["properties"] ?: throw Util.missingProperty("properties", "properties", reader)
            return when (type) {
                "dog" -> dogAdapter.fromJsonValue(properties)
                "cat" -> catAdapter.fromJsonValue(properties)
                else -> null
            }
        }

        override fun toJson(writer: JsonWriter, value: Animal?) {
            writer.beginObject()
            writer.name("type")
            when (value) {
                is Animal.Dog -> writer.value("dog")
                is Animal.Cat -> writer.value("cat")
            }

            writer.name("properties")
            when (value) {
                is Animal.Dog -> dogAdapter.toJson(writer, value)
                is Animal.Cat -> catAdapter.toJson(writer, value)
            }
            writer.endObject()
        }
    }
}

private val moshi = Moshi.Builder()
    .add(AnimalAdapterFactory)
    .build()

@Test
fun test() {
    val dog: Animal = Animal.Dog(true)
    val cat: Animal = Animal.Cat(7)
    println(moshi.adapter(Animal::class.java).toJson(dog))
    println(moshi.adapter(Animal::class.java).toJson(cat))
    val shouldBeDog: Animal? = moshi.adapter(Animal::class.java).fromJson(moshi.adapter(Animal::class.java).toJson(dog))
    val shouldBeCat: Animal? = moshi.adapter(Animal::class.java).fromJson(moshi.adapter(Animal::class.java).toJson(cat))
    println(shouldBeDog)
    println(shouldBeCat)
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM