用 Moshi 序列化 sealed class

Serialize sealed class with Moshi

以下将产生一个 IllegalArgumentException 因为你 "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))
}

我曾尝试使用自定义适配器解决此问题,但我能想出的唯一解决方案涉及为每个子类显式编写所有 属性 名称。例如:

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()
    }

    ....
}

最终我希望生成如下所示的 JSON:

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

我很高兴必须使用自定义适配器来编写每种类型的名称,但我需要一个解决方案来自动序列化每种类型的属性,而不必手动编写它们。

我认为您需要多态适配器来实现此目的,这需要 moshi-adapters 工件。这将启用具有不同属性的 sealed 类 的序列化。更多详细信息在本文中:https://proandroiddev.com/moshi-polymorphic-adapter-is-d25deebbd7c5

我通过创建一个工厂、一个封闭的 class 和一个可以为每个项目类型提供 classes 的枚举解决了这个问题。然而,这感觉相当笨拙,我希望有一个更直接的解决方案。

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()
            }
        }
    }
}

答案摘自:github.com/square/moshi/issues/813

您应该能够创建自己的 JsonAdapter.Factory 并在需要 Animal 时提供自定义适配器 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)
}

这可以通过 PolymorphicJsonAdapterFactory 并在 json 中包含一个额外的 属性 来指定类型。

例如:

这个JSON

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

可以映射到下面类

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()
}

使用以下 Moshi 配置

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