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

com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory Maven / Gradle / Ivy

There is a newer version: 1.15.1
Show newest version
/*
 * Copyright (C) 2011 Google Inc.
 *
 * Licensed under the Apache License, 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
 *
 *    https://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.JsonAdapter;
import com.squareup.moshi.JsonDataException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
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;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;

/**
 * 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 implements HandOfCards {
 *   Card hidden_card;
 *   List visible_cards;
 * }
 *
 * class HoldemHand implements 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. *
  • 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: * *

    *
  • If {@link #withDefaultValue(Object)} is used, then {@code defaultValue} will be returned. *
  • If {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the {@code * fallbackJsonAdapter.fromJson(reader)} result will be returned. *
  • Otherwise a {@link JsonDataException} will be thrown. *
* *

If an unknown type is encountered when encoding: * *

    *
  • If {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the {@code * fallbackJsonAdapter.toJson(writer, value)} result will be returned. *
  • Otherwise a {@link IllegalArgumentException} will be thrown. *
* *

If the same subtype has multiple labels the first one is used when encoding. */ public final class PolymorphicJsonAdapterFactory implements JsonAdapter.Factory { final Class baseType; final String labelKey; final List labels; final List subtypes; @Nullable final JsonAdapter fallbackJsonAdapter; PolymorphicJsonAdapterFactory( Class baseType, String labelKey, List labels, List subtypes, @Nullable JsonAdapter fallbackJsonAdapter) { this.baseType = baseType; this.labelKey = labelKey; this.labels = labels; this.subtypes = subtypes; this.fallbackJsonAdapter = fallbackJsonAdapter; } /** * @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. */ @CheckReturnValue public static PolymorphicJsonAdapterFactory of(Class baseType, String labelKey) { if (baseType == null) throw new NullPointerException("baseType == null"); if (labelKey == null) throw new NullPointerException("labelKey == null"); return new PolymorphicJsonAdapterFactory<>( baseType, labelKey, Collections.emptyList(), Collections.emptyList(), null); } /** Returns a new factory that decodes instances of {@code subtype}. */ 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)) { throw new IllegalArgumentException("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, fallbackJsonAdapter); } /** * Returns a new factory that with default to {@code fallbackJsonAdapter.fromJson(reader)} upon * decoding of unrecognized labels. * *

The {@link JsonReader} instance will not be automatically consumed, so make sure to consume * it within your implementation of {@link JsonAdapter#fromJson(JsonReader)} */ public PolymorphicJsonAdapterFactory withFallbackJsonAdapter( @Nullable JsonAdapter fallbackJsonAdapter) { return new PolymorphicJsonAdapterFactory<>( baseType, labelKey, labels, subtypes, fallbackJsonAdapter); } /** * Returns a new factory that will default to {@code defaultValue} upon decoding of unrecognized * labels. The default value should be immutable. */ public PolymorphicJsonAdapterFactory withDefaultValue(@Nullable T defaultValue) { return withFallbackJsonAdapter(buildFallbackJsonAdapter(defaultValue)); } private JsonAdapter buildFallbackJsonAdapter(final T defaultValue) { return new JsonAdapter() { @Override public @Nullable Object fromJson(JsonReader reader) throws IOException { reader.skipValue(); return defaultValue; } @Override public void toJson(JsonWriter writer, Object value) throws IOException { throw new IllegalArgumentException( "Expected one of " + subtypes + " but found " + value + ", a " + value.getClass() + ". Register this subtype."); } }; } @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))); } return new PolymorphicJsonAdapter(labelKey, labels, subtypes, jsonAdapters, fallbackJsonAdapter) .nullSafe(); } static final class PolymorphicJsonAdapter extends JsonAdapter { final String labelKey; final List labels; final List subtypes; final List> jsonAdapters; @Nullable final JsonAdapter fallbackJsonAdapter; /** 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, @Nullable JsonAdapter fallbackJsonAdapter) { this.labelKey = labelKey; this.labels = labels; this.subtypes = subtypes; this.jsonAdapters = jsonAdapters; this.fallbackJsonAdapter = fallbackJsonAdapter; this.labelKeyOptions = JsonReader.Options.of(labelKey); this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0])); } @Override public Object fromJson(JsonReader reader) throws IOException { JsonReader peeked = reader.peekJson(); peeked.setFailOnUnknown(false); int labelIndex; try { labelIndex = labelIndex(peeked); } finally { peeked.close(); } if (labelIndex == -1) { return this.fallbackJsonAdapter.fromJson(reader); } else { 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 && this.fallbackJsonAdapter == null) { throw new JsonDataException( "Expected one of " + labels + " for key '" + labelKey + "' but found '" + reader.nextString() + "'. Register a subtype for this label."); } 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); final JsonAdapter adapter; if (labelIndex == -1) { if (fallbackJsonAdapter == null) { throw new IllegalArgumentException( "Expected one of " + subtypes + " but found " + value + ", a " + value.getClass() + ". Register this subtype."); } adapter = fallbackJsonAdapter; } else { adapter = jsonAdapters.get(labelIndex); } writer.beginObject(); if (adapter != fallbackJsonAdapter) { 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 + ")"; } } }