com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory Maven / Gradle / Ivy
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.gnu.org/licenses/lgpl-3.0.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.adapters;
import com.squareup.moshi.*;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON
* Moshi uses this type information to determine which class to decode to. When encoding Moshi uses
* the object’s class to determine what type information to include.
*
*
Suppose we have an interface, its implementations, and a class that uses them:
*
*
{@code
*
* interface HandOfCards {
* }
*
* class BlackjackHand extends HandOfCards {
* Card hidden_card;
* List visible_cards;
* }
*
* class HoldemHand extends HandOfCards {
* Set hidden_cards;
* }
*
* class Player {
* String name;
* HandOfCards hand;
* }
* }
*
*
We want to decode the following JSON into the player model above:
*
*
{@code
*
* {
* "name": "Jesse",
* "hand": {
* "hand_type": "blackjack",
* "hidden_card": "9D",
* "visible_cards": ["8H", "4C"]
* }
* }
* }
*
*
Left unconfigured, Moshi would incorrectly attempt to decode the hand object to the abstract
* {@code HandOfCards} interface. We configure it to use the appropriate subtype instead:
*
*
{@code
*
* Moshi moshi = new Moshi.Builder()
* .add(PolymorphicJsonAdapterFactory.of(HandOfCards.class, "hand_type")
* .withSubtype(BlackjackHand.class, "blackjack")
* .withSubtype(HoldemHand.class, "holdem"))
* .build();
* }
*
*
This class imposes strict requirements on its use:
*
*
* - Base types may be classes or interfaces. You may not use {@code Object.class} as a base
* type.
*
- Subtypes must encode as JSON objects.
*
- Type information must be in the encoded object. Each message must have a type label like
* {@code hand_type} whose value is a string like {@code blackjack} that identifies which type
* to use.
*
- Each type identifier must be unique.
*
*
*
For best performance type information should be the first field in the object. Otherwise Moshi
* must reprocess the JSON stream once it knows the object's type.
*
*
If an unknown subtype is encountered when decoding, this will throw a {@link
* JsonDataException}. If an unknown type is encountered when encoding, this will throw an {@link
* IllegalArgumentException}.
*/
public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Factory {
final Class baseType;
final String labelKey;
final List labels;
final List subtypes;
PolymorphicJsonAdapterFactory(
Class baseType, String labelKey, List labels, List subtypes) {
this.baseType = baseType;
this.labelKey = labelKey;
this.labels = labels;
this.subtypes = subtypes;
}
/**
* @param baseType The base type for which this factory will create adapters. Cannot be Object.
* @param labelKey The key in the JSON object whose value determines the type to which to map the
* JSON object.
*/
public static PolymorphicJsonAdapterFactory of(Class baseType, String labelKey) {
if (baseType == null) {
throw new NullPointerException("baseType == null");
}
if (labelKey == null) {
throw new NullPointerException("labelKey == null");
}
if (baseType == Object.class) {
throw new IllegalArgumentException(
"The base type must not be Object. Consider using a marker interface.");
}
return new PolymorphicJsonAdapterFactory(
baseType, labelKey, Collections.emptyList(), Collections.emptyList());
}
/**
* Returns a new factory that decodes instances of {@code subtype}. When an unknown type is found
* during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label
* is found during decoding a {@linkplain JsonDataException} will be thrown.
*/
public PolymorphicJsonAdapterFactory withSubtype(Class extends T> subtype, String label) {
if (subtype == null) {
throw new NullPointerException("subtype == null");
}
if (label == null) {
throw new NullPointerException("label == null");
}
if (labels.contains(label) || subtypes.contains(subtype)) {
throw new IllegalArgumentException("Subtypes and labels must be unique.");
}
List newLabels = new ArrayList(labels);
newLabels.add(label);
List newSubtypes = new ArrayList(subtypes);
newSubtypes.add(subtype);
return new PolymorphicJsonAdapterFactory(baseType, labelKey, newLabels, newSubtypes);
}
@Override
public JsonAdapter> create(Type type, Set extends Annotation> annotations, Moshi moshi) {
if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
return null;
}
List> jsonAdapters = new ArrayList>(subtypes.size());
for (int i = 0, size = subtypes.size(); i < size; i++) {
jsonAdapters.add(moshi.adapter(subtypes.get(i)));
}
JsonAdapter