Handling Polymorphic Types

A common challenge in JSON serialization is handling collections of different but related types, such as a list of shapes where each shape can be a Circle or a Rectangle.

Moshi provides PolymorphicJsonAdapterFactory in the moshi-adapters artifact to solve this problem.

The Problem

Consider these classes:

interface Shape

data class Circle(val radius: Double) : Shape
data class Rectangle(val width: Double, val height: Double) : Shape

data class Drawing(val shapes: List<Shape>)

Without special configuration, Moshi doesn't know how to deserialize a Shape because it's an interface. It needs to be told which concrete class (Circle or Rectangle) to use based on the JSON content.

The Solution: A Type Label

PolymorphicJsonAdapterFactory works by requiring a special field in the JSON object, called a type label, that identifies which subtype to use.

Here's how the JSON should look:

{
  "shapes": [
    {
      "type": "circle",
      "radius": 10.0
    },
    {
      "type": "rectangle",
      "width": 5.0,
      "height": 8.0
    }
  ]
}

In this example, "type" is the label key, and "circle" and "rectangle" are the labels.

Configuration

You configure the factory by mapping each label to its corresponding class.

import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory

val moshi = Moshi.Builder()
    .add(
        PolymorphicJsonAdapterFactory.of(Shape::class.java, "type")
            .withSubtype(Circle::class.java, "circle")
            .withSubtype(Rectangle::class.java, "rectangle")
    )
    .addLast(KotlinJsonAdapterFactory())
    .build()

val adapter = moshi.adapter(Drawing::class.java)
  • of(Shape::class.java, "type"): Specifies the base type (Shape) and the JSON key for the type label ("type").
  • .withSubtype(...): Maps a concrete class to its unique string label.

When serializing, Moshi will automatically add the correct label to the JSON object. When deserializing, it will read the label first to decide which adapter to use for the rest of the object.

Handling Unknown Subtypes

What happens if the JSON contains a type label that you haven't configured?

Default Behavior

By default, Moshi will throw a JsonDataException.

withDefaultValue()

You can provide a default object to return if an unknown label is encountered. This is useful for maintaining forward compatibility.

// A fallback shape
object UnknownShape : Shape

val factory = PolymorphicJsonAdapterFactory.of(Shape::class.java, "type")
    .withSubtype(Circle::class.java, "circle")
    .withDefaultValue(UnknownShape)

Now, if a "triangle" shape appears in the JSON, it will be deserialized as an UnknownShape instance.

withFallbackJsonAdapter()

For more complex fallback logic, you can provide a custom JsonAdapter. This adapter will be invoked for any unrecognized subtype. This gives you full control to parse the unknown object, log a warning, or return a specific object.

val factory = PolymorphicJsonAdapterFactory.of(Shape::class.java, "type")
    .withSubtype(Circle::class.java, "circle")
    .withFallbackJsonAdapter(myCustomFallbackAdapter)