Skip to content

onenowy/moshi-polymorphic-adapter

Repository files navigation

Moshi Polymorphic Adapter

Moshi Polymorphic Adapter is a library written by Kotlin, which provides polymorphic Adapters for Moshi . It's based on thePolymorphicJsonAdapterFactory of Moshi, but more flexible.

ValuePolymorphicAdapterFactory

A polymorphic adapter factory creates an adapter that uses the unique value to determine which type to decode to. The ValuePolymorphicAdapterFactory is almost same as the PolymorphicJsonAdapterFactory, but it supports Int , Long, Double and Boolean, not only String.

Suppose we want to decode a JSON object to a Kotlin or a Java type, and the JSON object is like:

[
  {
    "type": 1,
    "typeOneData": "test"
  },
  {
    "type": 2,
    "typeTwoData": 1
  }
]

and the type is like:

sealed class Parent(val type: Int)
data class FirstChild(val typeOneData: String) : Parent(1)
data class SecondChild(val typeTwoData: Int) : Parent(2)

or

interface Parent {
}

class FirstChild implements Parent {
    String typeOneData;

    public FirstChild(String typeOneData) {
        this.typeOneData = typeOneData;
    }
}

class SecondChild implements Parent {
    int typeTwoData;

    public SecondChild(int typeTwoData) {
        this.typeTwoData = typeTwoData;
    }
}

Base types may be classes or interfaces. Subtypes are encoded as JSON objects. Base types don't have to include a type label like type, which is optional.

Configure the adapter factory:

Kotlin
val valuePolymorphicAdapterFactory = ValuePolymorphicAdapterFactory.of(Parent::class.java, "type", Int::class.java)
    .withSubtype(FirstChild::class.java, 1)
    .withSubtype(SecondChild::class.java, 2)
val moshi = Moshi.Builder().add(valuePolymorphicAdapterFactory).build()
Java
ValuePolymorphicAdapterFactory<Parent, Integer> valuePolymorphicAdapterFactory=ValuePolymorphicAdapterFactory.of(Parent.class,"type",int.class)
        .withSubtype(FirstChild.class,1)
        .withSubtype(SecondChild.class,2);
        Moshi moshi=new Moshi.Builder().add(valuePolymorphicAdapterFactory).build();

NamePolymorphicAdapterFactory

This adapter factory is also similar to PolymorphicJsonAdapterFactory, but it creates an adapter that determines which type to decode to by the JSON field name, not value.

For example, We have a JSON object that doesn't have a type label field:

 [
  {
    "unique name": 1,
    "commonData": "data",
    "data": "data"
  },
  {
    "uniqueSecondName": 1,
    "commonData": "data",
    "data": 1
  }
]

and the type is like:

sealed class Parent
data class FirstChild(@Json(name = "unique name") val uniqueName: Int, val commonData: Sting, val data: String) : Parent()
data class SecondChild(val uniqueSecondName: Int, val commonData: Sting, val data: Int) : Parent()

or

interface Parent {
}

class FirstChild implements Parent {
    @Json(name = "unique name")
    int uniqueName;
    String commonData;
    String data;

    public FirstChild(int uniqueName, String commonData, String data) {
        this.uniqueName = uniqueName;
        this.commonData = commonData;
        this.data = data;
    }
}

class SecondChild implements Parent {
    int uniqueSecondName;
    String commonData;
    int data;

    public SecondChild(int uniqueSecondName, String commonData, int data) {
        this.uniqueSecondName = uniqueSecondName;
        this.commonData = commonData;
        this.data = data;
    }
}

Configure the adapter factory:

Kotlin
val namePolymorphicAdapterFactory = NamePolymorphicAdapterFactory.of(Parent::class.java)
    .withSubtype(FirstChild::class.java, "unique name")
    .withSubtype(SecondChild::class.java, "uniqueSecondName")
val moshi = Moshi.Builder().add(namePolymorphicAdapterFactory).build()
Java
NamePolymorphicAdapterFactory<Parent> namePolymorphicAdapterFactory = NamePolymorphicAdapterFactory.of(Parent.class)
        .withSubtype(FirstChild.class,"unique name")
        .withSubtype(SecondChild.class,"uniqueSecondName");
        Moshi moshi=new Moshi.Builder().add(namePolymorphicAdapterFactory).build();

Set the default value or the fallback Adapter

Moshi Polymorphic adapter can set the default value or the fallback adapter using the withDefaultValue and withFallbackJsonAdapter methods, which are derived from the PolymorphicJsonAdapterFactory. Please refer to the PolymorphicJsonAdapterFactory for more details.

Installation

Depend via Maven:

<dependency>
    <groupId>dev.onenowy.moshipolymorphicadapter</groupId>
    <artifactId>moshi-polymorphic-adapter</artifactId>
    <version>{version}</version>
</dependency>

or Gradle:

implementation("dev.onenowy.moshipolymorphicadapter:moshi-polymorphic-adapter:{version}")

Kotlin-Sealed

Moshi Polymorphic Adapter provides convenience ways to create adapters for Kotlin sealed classes. It uses @JsonClass of Moshi and generator tag in the annotation to configure the adapter type. PolymorphicAdapterType has constant values that represent the type of the adapter.

For ValuePolymorphicAdapter:

@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.VALUE_POLYMORPHIC_ADAPTER_INT + ":" + "type")
sealed class Parent

@ValueLabel(1.toString())
data class FirstChild(val type: Int, val typeOneData: String) : Parent()

@ValueLabel(2.toString())
class SecondChild(val typeTwoData: Int) : Parent()

thegenerator tag value must be set as PolymorphicAdapterType.VALUE_POLYMORPHIC_ADAPTER_{value type}:{type label}.

and for NamePolymorphicAdapter:

@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
sealed class Parent

@NameLabel("unique Name")
data class FirstChild(@Json(name = "unique name") val uniqueName: Int, val commonData: Sting, val data: String) :
    Parent()

@NameLabel("uniqueSecondName")
data class SecondChild(val uniqueSecondName: Int, val commonData: Sting, val data: Int) : Parent()

It supports null as a default value using @DefaultNull.

@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_ADAPTER)
@DefaultNull
sealed class Parent()

You can use this feature with reflection or codegen.

Reflection

To use the reflection feature, you need to add the KotlinSealedPolymorphicAdapterFactory. If you use KotlinJsonAdapterFactory of Moshi, the KotlinSealedPolymorphicAdapterFactory must be added before it.

val moshi = Moshi.Builder()
    .add(KotlinSealedPolymorphicAdapterFactory())
    .add(KotlinJsonAdapterFactory())
    .build()

The reflection feature requires the following additional dependency:

<dependency>
    <groupId>dev.onenowy.moshipolymorphicadapter</groupId>
    <artifactId>kotlin-sealed-reflect</artifactId>
    <version>{version}</version>
</dependency>
implementation("dev.onenowy.moshipolymorphicadapter:kotlin-sealed-reflect:{version}")

Codegen

The codgen feature requires kapt plugin and the following additional dependency:

<dependency>
    <groupId>dev.onenowy.moshipolymorphicadapter</groupId>
    <artifactId>kotlin-sealed-codegen</artifactId>
    <version>{version}</version>
</dependency>
kapt("dev.onenowy.moshipolymorphicadapter:kotlin-sealed-codegen:{version}")