Skip to content

Commit

Permalink
Make RuntimeTypeAdapterFactory recognize subclasses only conditiona…
Browse files Browse the repository at this point in the history
…lly. (#2160)

PR #2139 changed this factory so that if given a certain baseType, it will also
recognize any subtype of that type. That is often the right thing to do, but it
is a change in behaviour, and does in fact break at least one current client of
this code. So instead we introduce a new `recognizeSubclasses()` method that
triggers this behaviour. When the method is not called, we revert to the old
behaviour of only recognizing instances of the exact class `baseType`.
  • Loading branch information
eamonnmcmanus committed Jul 27, 2022
1 parent 924c496 commit 2deb209
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 6 deletions.
Expand Up @@ -30,7 +30,6 @@
import java.util.LinkedHashMap;
import java.util.Map;


/**
* Adapts values whose runtime type may differ from their declaration type. This
* is necessary when a field's type is not the same type that GSON should create
Expand Down Expand Up @@ -138,8 +137,10 @@ public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>();
private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>();
private final boolean maintainType;
private boolean recognizeSubtypes;

private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boolean maintainType) {
private RuntimeTypeAdapterFactory(
Class<?> baseType, String typeFieldName, boolean maintainType) {
if (typeFieldName == null || baseType == null) {
throw new NullPointerException();
}
Expand All @@ -151,7 +152,8 @@ private RuntimeTypeAdapterFactory(Class<?> baseType, String typeFieldName, boole
/**
* Creates a new runtime type adapter using for {@code baseType} using {@code
* typeFieldName} as the type field name. Type field names are case sensitive.
* {@code maintainType} flag decide if the type will be stored in pojo or not.
*
* @param maintainType true if the type field should be included in deserialized objects
*/
public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType);
Expand All @@ -173,6 +175,15 @@ public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
return new RuntimeTypeAdapterFactory<>(baseType, "type", false);
}

/**
* Ensures that this factory will handle not just the given {@code baseType}, but any subtype
* of that type.
*/
public RuntimeTypeAdapterFactory<T> recognizeSubtypes() {
this.recognizeSubtypes = true;
return this;
}

/**
* Registers {@code type} identified by {@code label}. Labels are case
* sensitive.
Expand Down Expand Up @@ -205,7 +216,13 @@ public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {

@Override
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
if (type == null || !baseType.isAssignableFrom(type.getRawType())) {
if (type == null) {
return null;
}
Class<?> rawType = type.getRawType();
boolean handle =
recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType);
if (!handle) {
return null;
}

Expand Down
Expand Up @@ -34,8 +34,27 @@ public void testRuntimeTypeAdapter() {

CreditCard original = new CreditCard("Jesse", 234);
assertEquals("{\"type\":\"CreditCard\",\"cvv\":234,\"ownerName\":\"Jesse\"}",
//do not give the explicit typeOfSrc, because if this would be in a list
//or an attribute, there would also be no hint. See #712
gson.toJson(original, BillingInstrument.class));
BillingInstrument deserialized = gson.fromJson(
"{type:'CreditCard',cvv:234,ownerName:'Jesse'}", BillingInstrument.class);
assertEquals("Jesse", deserialized.ownerName);
assertTrue(deserialized instanceof CreditCard);
}

public void testRuntimeTypeAdapterRecognizeSubtypes() {
// We don't have an explicit factory for CreditCard.class, but we do have one for
// BillingInstrument.class that has recognizeSubtypes(). So it should recognize CreditCard, and
// when we call gson.toJson(original) below, without an explicit type, it should be invoked.
RuntimeTypeAdapterFactory<BillingInstrument> rta = RuntimeTypeAdapterFactory.of(
BillingInstrument.class)
.recognizeSubtypes()
.registerSubtype(CreditCard.class);
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(rta)
.create();

CreditCard original = new CreditCard("Jesse", 234);
assertEquals("{\"type\":\"CreditCard\",\"cvv\":234,\"ownerName\":\"Jesse\"}",
gson.toJson(original));
BillingInstrument deserialized = gson.fromJson(
"{type:'CreditCard',cvv:234,ownerName:'Jesse'}", BillingInstrument.class);
Expand Down

0 comments on commit 2deb209

Please sign in to comment.