Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I parse class hierarchies with gson?

Tags:

java

json

gson

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.

like image 737
copolii Avatar asked Feb 08 '14 02:02

copolii


2 Answers

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);
like image 136
Jesse Wilson Avatar answered Oct 13 '22 02:10

Jesse Wilson


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.

like image 24
copolii Avatar answered Oct 13 '22 00:10

copolii