All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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 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 objectJsonAdapter = moshi.adapter(Object.class); return new PolymorphicJsonAdapter( labelKey, labels, subtypes, jsonAdapters, objectJsonAdapter).nullSafe(); } static final class PolymorphicJsonAdapter extends JsonAdapter { final String labelKey; final List labels; final List subtypes; final List> jsonAdapters; final JsonAdapter objectJsonAdapter; /** * Single-element options containing the label's key only. */ final JsonReader.Options labelKeyOptions; /** * Corresponds to subtypes. */ final JsonReader.Options labelOptions; PolymorphicJsonAdapter(String labelKey, List labels, List subtypes, List> jsonAdapters, JsonAdapter objectJsonAdapter) { this.labelKey = labelKey; this.labels = labels; this.subtypes = subtypes; this.jsonAdapters = jsonAdapters; this.objectJsonAdapter = objectJsonAdapter; this.labelKeyOptions = JsonReader.Options.of(labelKey); this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0])); } @Override public Object fromJson(JsonReader reader) throws IOException { int labelIndex = labelIndex(reader.peekJson()); return jsonAdapters.get(labelIndex).fromJson(reader); } private int labelIndex(JsonReader reader) throws IOException { reader.beginObject(); while (reader.hasNext()) { if (reader.selectName(labelKeyOptions) == -1) { reader.skipName(); reader.skipValue(); continue; } int labelIndex = reader.selectString(labelOptions); if (labelIndex == -1) { throw new JsonDataException("Expected one of " + labels + " for key '" + labelKey + "' but found '" + reader.nextString() + "'. Register a subtype for this label."); } reader.close(); return labelIndex; } throw new JsonDataException("Missing label for " + labelKey); } @Override public void toJson(JsonWriter writer, Object value) throws IOException { Class type = value.getClass(); int labelIndex = subtypes.indexOf(type); if (labelIndex == -1) { throw new IllegalArgumentException("Expected one of " + subtypes + " but found " + value + ", a " + value.getClass() + ". Register this subtype."); } JsonAdapter adapter = jsonAdapters.get(labelIndex); writer.beginObject(); writer.name(labelKey).value(labels.get(labelIndex)); int flattenToken = writer.beginFlatten(); adapter.toJson(writer, value); writer.endFlatten(flattenToken); writer.endObject(); } @Override public String toString() { return "PolymorphicJsonAdapter(" + labelKey + ")"; } } }