Make RuntimeTypeAdapterFactory recognize subclasses only conditionally. (#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`.
This commit is contained in:
Éamonn McManus 2022-07-27 12:18:20 -07:00 committed by GitHub
parent 924c496b95
commit 2deb2099d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 42 additions and 6 deletions

View File

@ -30,7 +30,6 @@ import java.io.IOException;
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
@ -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();
}
@ -151,7 +152,8 @@ public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
/**
* 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);
@ -173,6 +175,15 @@ public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
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.
@ -205,7 +216,13 @@ public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
@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;
}

View File

@ -34,8 +34,27 @@ public final class RuntimeTypeAdapterFactoryTest extends TestCase {
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);