21

I want to deserialize json objects to specific types of objects (using Gson library) based on type field value, eg.:

[
    {
          "type": "type1",
          "id": "131481204101",
          "url": "http://something.com",
          "name": "BLAH BLAH",
          "icon": "SOME_STRING",
          "price": "FREE",
          "backgroundUrl": "SOME_STRING"
    },
    {
        ....
    }
]

So type field will have different (but known) values. Based on that value I need to deserialize that json object to appropriate model object, eg.: Type1Model, Type2Model etc. I know I can easily do that before deserialization by converting it to JSONArray, iterate through it and resolve which type it should be deserialized to. But I think it's ugly approach and I'm looking for better way. Any suggestions?

lukaleli
  • 3,427
  • 3
  • 22
  • 32
  • Unfortunately because of what is a bad design in terms of the JSON being supplied, what you describe is what you have to do. The answer provided below helps if you can create a class hierarchy in Java that models the data, but that's about as good as you're going to get if that's even applicable. – Brian Roach Feb 15 '14 at 05:07

4 Answers4

41

You may implement a JsonDeserializer and use it while parsing your Json value to a Java instance. I'll try to show it with a code which is going to give you the idea:

1) Define your custom JsonDeserializer class which creates different instance of classes by incoming json value's id property:

class MyTypeModelDeserializer implements JsonDeserializer<MyBaseTypeModel> {

    @Override
    public MyBaseTypeModel deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
            throws JsonParseException {

        JsonObject jsonObject = json.getAsJsonObject();

        JsonElement jsonType = jsonObject.get("type");
        String type = jsonType.getAsString();

        MyBaseTypeModel typeModel = null;     

        if("type1".equals(type)) {
            typeModel = new Type1Model();
        } else if("type2".equals(type)) {
            typeModel = new Type2Model();
        }
        // TODO : set properties of type model

        return typeModel;
    }
}

2) Define a base class for your different instance of java objects:

class  MyBaseTypeModel {
    private String type;
    // TODO : add other shared fields here
}

3) Define your different instance of java objects' classes which extend your base class:

class Type1Model extends MyBaseTypeModel {
    // TODO: add specific fields for this class
}

class Type2Model extends MyBaseTypeModel {
    // TODO: add specific fields for this class
}

4) Use these classes while parsing your json value to a bean:

GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(MyBaseTypeModel.class, new MyTypeModelDeserializer());
Gson gson = gsonBuilder.create();

MyBaseTypeModel myTypeModel = gson.fromJson(myJsonString, MyBaseTypeModel.class);

I can not test it right now but I hope you get the idea. Also this link would be very helpful.

Abby
  • 1,610
  • 3
  • 19
  • 35
Devrim
  • 15,345
  • 4
  • 66
  • 74
  • 21
    Small tip: instead of setting the properties of the type model manually in the `MyTypeModelDeserializer` you can also call `context.deserialize(json, TypeNModel.class)` to use Gson's default deserialization for the actual model. Careful: do not pass `MyBaseTypeModel.class` as a type since this would result in an infinite deserialization loop. If you register specialized type adapters for your subclasses, they will be invoked as well by the `context.deserialize` call. – Martin Matysiak May 09 '14 at 14:54
  • To build on what @MartinMatysiak said. A hack-ish way to still be able to use GSON for MyBaseTypeModel you could declare "class MyBaseTypeModelConcrete extends MyBaseTypeModel{}" and then pass MyBaseTypeModelConcrete.class. Not useful in this case but in some cases it can be useful. – startoftext Mar 14 '16 at 22:38
  • @DevrimTuncer I have similar question on Gson [here](http://stackoverflow.com/questions/41203175/how-to-lowercase-the-jsonelement-value-in-the-custom-deserializer-gson). Wanted to see if you can help me out. – john Dec 18 '16 at 07:56
17

@stephane-k 's answer works, but it is a bit confusing and could be improved upon (see comments to his answer)

Copy https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java into your project. (It's ok; these classes are designed to be copy/pasted https://github.com/google/gson/issues/845#issuecomment-217231315)

Setup model inheritance:

// abstract is optional
abstract class BaseClass {
}

class Type1Model extends BaseClass {
}

class Type2Model extends BaseClass {
}

Setup GSON or update existing GSON:

RuntimeTypeAdapterFactory<BaseClass> typeAdapterFactory = RuntimeTypeAdapterFactory
        .of(BaseClass.class, "type")
        .registerSubtype(Type1Model.class, "type1")
        .registerSubtype(Type2Model.class, "type2");

Gson gson = new GsonBuilder().registerTypeAdapterFactory(typeAdapterFactory)
                .create();

Deserialize your JSON into base class:

String jsonString = ...
BaseClass baseInstance = gson.fromJson(jsonString, BaseClass.class);

baseInstance will be instanceof either Type1Model or Type2Model.

From here you can either code to an interface or check instanceof and cast.

tir38
  • 9,810
  • 10
  • 64
  • 107
  • Wow, much more clean than using JsonDeserializer. It works, thanks. – Rubén Viguera Oct 23 '19 at 16:26
  • I've tried using this solution with nested objects and lists in the json and it works as well (as long as there is a key which differentiates the objects, such as `"type1"` or `"type2"` in the question). – Ernani Mar 10 '20 at 15:59
2

use https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java

then configure it with

public static final class JsonAdapterFactory extends 
    RuntimeTypeAdapterFactory<MediumSummaryInfo> {
        public JsonAdapterFactory() {
            super(MyBaseType.class, "type");
            registerSubtype(MySubtype1.class, "type1");
            registerSubtype(MySubtype2.class, "type2");
        }
}

and add the annotation:

@JsonAdapter(MyBaseType.JsonAdapterFactory.class)

to MyBaseType

Much better.

Robert
  • 589
  • 7
  • 20
stephane k.
  • 1,668
  • 20
  • 21
  • RuntimeTypeAdapterFactory is final. how to extends? – gfan Jul 01 '16 at 10:37
  • 1
    I made it non final in my project :-) it's in Gson's extras not in the lib anyway, you can copy it in. – stephane k. Sep 28 '16 at 22:43
  • 1
    According to docs in this file, the factory should not be instantiated directly, but via `RuntimeTypeAdapterFactory.of` method. Whenever, the answer is generally right and therefore underrated. Thank you. – N. Kudryavtsev Apr 22 '18 at 12:32
1

If you have a lot of sub types and you do not want to or cannot maintain a list of them, you can also use an annotation based approach.

Here is the required code and also some usage examples: https://gist.github.com/LostMekka/d90ade1fe051732d6b4ac60deea4f9c2 (it is Kotlin, but can easily be ported to Java)

For me, this approach is especially appealing, since I write a small library that does not know all possible sub types at compile time.

LostMekkaSoft
  • 143
  • 1
  • 7