I have a class hierarchy that I want to convert to and from GSON. I'm not sure how to approach this with GSON (I currently have a Factory
class that looks at the JSONObject and based on presence or absence of keys it calls the right constructor, which in turn delegates some of its work to the super class). When I store these objects in the local SQLite DB, I use an integer to denote their type and the factory class uses this type to call the right constructor. I don't have this type in the JSON (which isn't mine).
How do I tell GSON based on the contents of the JSON object which type of object to instantiate for me?
In the examples below, treat ...
inside the JSON brackets as there may or may not be more elements
Here's a breakdown of the class hierarchy:
There is a base abstract type: SuperType
with a JSON representation {"ct":12345,"id":"abc123 ...}
There are 2 main abstract sub types: TypeA
(has json key "a") and TypeB
(has json key "b")
TypeA
Example: {"ct":12345,"id":"abc123, "a":{...}}
TypeA
has 15 children (Let's call these TypeA_A
to TypeA_P
). The JSON representation of these objects would be something like {"ct":12345,"id":"abc123, "a":{"aa":1 ...} ...}
or {"ct":12345,"id":"abc123, "a":{"ag":"Yo dawg I head you like JSON" ...} ...}
TypeB
Example: {"ct":12345,"id":"abc123, "b":{...} ...}
TypeB
has another abstract subtype (TypeB_A
) and few children (Let's call these TypeB_B
to TypeB_I
). The JSON representation of these objects would be {"ct":12345,"id":"abc123, "b":{"ba":{...} ...} ...}
or {"ct":12345,"id":"abc123, "b":{"bg":"Stayin alive" ...} ...}
I could throw it all in one monster type and treat each of the sub types as an inner object, but I'll end up with a lot of inner members that are null (sort of like a tree with a lot of branches that lead to nowhere). As a result, I'll end up with a lot of if (something==null)
just to determine which one of these types I'm dealing with.
I've looked at TypeAdapter
and TypeAdapterFactory
, but I'm still not sure how to approach this since I have to look at the content of the incoming JSON.
How do I tell GSON based on the contents of the JSON object which type of object to instantiate for me?
Thanks.
There's a standard extension TypeAdapter
called RuntimeTypeAdapterFactory that makes this straightforward.
The test case provides some sample code:
RuntimeTypeAdapterFactory<BillingInstrument> rta = RuntimeTypeAdapterFactory.of(
BillingInstrument.class)
.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.class));
BillingInstrument deserialized = gson.fromJson(
"{type:'CreditCard',cvv:234,ownerName:'Jesse'}", BillingInstrument.class);
assertEquals("Jesse", deserialized.ownerName);
assertTrue(deserialized instanceof CreditCard);
So, the RTTAF approach was a push in the right direction, but it expects me to have a field that denotes which subtype I'm using. In my specific case, I have subtypes and subtypes of those subtypes. This is what I ended up doing:
UPDATE: Created Github gist
Note: In my test project (below), I'm using GSON and Lombok for annotations.
The Type Factory
public class CustomTypeAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create (final Gson gson, final TypeToken<T> type) {
if (type.getRawType () != SuperType.class)
return null;
final TypeAdapter<T> delegate = gson.getDelegateAdapter (this, type);
return new TypeAdapter<T> () {
@Override
public void write (final JsonWriter jsonWriter, final T t) throws IOException {
delegate.write (jsonWriter, t);
}
@Override
public T read (final JsonReader jsonReader) throws IOException, JsonParseException {
JsonElement tree = Streams.parse (jsonReader);
JsonObject object = tree.getAsJsonObject ();
if (object.has ("a"))
return (T) readTypeA (tree, object.getAsJsonObject ("a"));
if (object.has ("b"))
return (T) readTypeB (tree, object.getAsJsonObject ("b"));
throw new JsonParseException ("Cannot deserialize " + type + ". It is not a valid SuperType JSON.");
}
private TypeA readTypeA (final JsonElement tree, final JsonObject a) {
if (a.has ("aa"))
return gson.getDelegateAdapter (CustomTypeAdapterFactory.this, TypeToken.get (TypeA_A.class)).fromJsonTree (tree);
if (a.has ("ab"))
return gson.getDelegateAdapter (CustomTypeAdapterFactory.this, TypeToken.get (TypeA_B.class)).fromJsonTree (tree);
if (a.has ("ac"))
return gson.getDelegateAdapter (CustomTypeAdapterFactory.this, TypeToken.get (TypeA_C.class)).fromJsonTree (tree);
throw new JsonParseException ("Cannot deserialize " + type + ". It is not a valid TypeA JSON.");
}
private TypeB readTypeB (final JsonElement tree, final JsonObject b) {
if (b.has ("ba"))
return gson.getDelegateAdapter (CustomTypeAdapterFactory.this, TypeToken.get (TypeB_A.class)).fromJsonTree (tree);
if (b.has ("bb"))
return gson.getDelegateAdapter (CustomTypeAdapterFactory.this, TypeToken.get (TypeB_B.class)).fromJsonTree (tree);
if (b.has ("bc"))
return gson.getDelegateAdapter (CustomTypeAdapterFactory.this, TypeToken.get (TypeB_C.class)).fromJsonTree (tree);
throw new JsonParseException ("Cannot deserialize " + type + ". It is not a valid TypeB JSON.");
}
};
}
}
SuperType.java
@Getter
@Setter
@EqualsAndHashCode
@ToString
public class SuperType {
@SerializedName ("ct")
protected long creationTime;
@SerializedName ("id")
protected String id;
}
Type_A
has no additional data in its level, but does have some common behaviours (methods omitted here for simplicity and because they are irrelevant to parsing).
TypeA.java
@Getter
@Setter
@EqualsAndHashCode (callSuper = true)
@ToString (callSuper = true)
public class TypeA extends SuperType {}
TypeA_A.java
@Getter
@Setter
@EqualsAndHashCode (callSuper = true)
@ToString(callSuper = true)
public class TypeA_A
extends TypeA {
@SerializedName ("a")
protected AA aValue;
@ToString
private static class AA {
@SerializedName ("aa")
private String aaValue;
}
}
Other Type_A children are very similar to TypeA_A.
Type_B
is slightly more complex as it has its own data and behaviours (again, omitted for simplicity):
TypeB.java
@Getter
@Setter
@EqualsAndHashCode (callSuper = true)
@ToString (callSuper = true)
public class TypeB extends SuperType {
// no member declared here
protected static abstract class B {
@SerializedName ("b1")
protected String b1Value;
@SerializedName ("b2")
protected String b2Value;
}
}
Type_BA.java
@Getter
@Setter
@EqualsAndHashCode (callSuper = true)
@ToString (callSuper = true)
public class TypeB_A
extends TypeB {
@SerializedName ("b")
protected BA bValue;
@ToString
private static class BA extends B {
@SerializedName ("ba")
private String baValue;
}
}
TypeB_B.java
@Getter
@Setter
@EqualsAndHashCode (callSuper = true)
@ToString (callSuper = true)
public class TypeB_B
extends TypeB {
@SerializedName ("b")
protected BB bValue;
@ToString
private static class BB extends B {
@SerializedName ("bb")
private String bbValue;
@SerializedName ("bb1")
private String bb1Value;
}
}
There may be some typos here because I had to change the actual type names and values, but I will create a basic java code example and will post to Github.
Thanks to @Jesse Wilson and @Argyle for their help in pointing me in the right direction.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With