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)