Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Instantiate child object with params from parent object being deserialized with GSON and use generics?

I have roughly the following structure

class MyDeserialParent<T extends MyChildInterface> {

     MyChildInterface mSerialChild;
     ... //some other fields (not 'type')

}

But it's deserialized from a messy JSON structure the two properties of the child are returned on the parent node like follows.

{
    "myDeserialParents" : [
        {
            ... //some parent properties
            "type": "value", //used in a TypeAdapter to choose child implementation
            "childProp1": "1",
            "childProp2": "2",
         },
         ... //more in this list
     ]
}

Obviously this prevents me from just annotating mSerialChild with SerializedName and letting a TypeAdapter work its magic. So what I'm hoping to do is when MyDeserialParent is deserialised use "type" to find the correct concrete class of MyChildInterface and make a new one using childProp1 and childProp2 as params for the constructor. I don't know how to go about this.

I can imagine using a TypeAdapter (JsonDeserializer) for MyDeserialParent and in deserialize get the type field (as well as the two child properties), then instantiate the correct concrete for MyChildInterface myself.

This then means I have to create my MyDeserialParent class (with context.deserialize(json, MyDeserialParent.class)) and call a setter with MyChildInterface instance. That feels wrong like I am missing something. Is there a better way?

Is there also a way to specify the generics (T on MyDeserialParent) if I manually create the parent object also? or does Type Erasure mean there is no way to do this? (This question is less important because I know I can get type safety if I use specific subtypes of MyDeserialParent which already infer T instead, but I'd like to avoid it)

like image 579
Nick Cardoso Avatar asked Feb 09 '16 15:02

Nick Cardoso


1 Answers

You obviously need a custom TypeAdapter. But the tricky parts are:

  • your parent class is a generic one
  • mSerialChild is not of type T, but of type MyChildInterface
  • we want to avoid parsing by hand the json for each child class and be able to add properties to the parent without having to modify the whole code.

Keeping that in mind, I ended up with the following solution.

public class MyParentAdapter implements JsonDeserializer<MyDeserialParent>{

    private static Gson gson = new GsonBuilder().create();
    // here is the trick: keep a map between "type" and the typetoken of the actual child class
    private static final Map<String, Type> CHILDREN_TO_TYPETOKEN;

    static{
        // initialize the mapping once
        CHILDREN_TO_TYPETOKEN = new TreeMap<>();
        CHILDREN_TO_TYPETOKEN.put( "value", new TypeToken<MyChild1>(){}.getType() );
    }


    @Override
    public MyDeserialParent deserialize( JsonElement json, Type t, JsonDeserializationContext
            jsonDeserializationContext ) throws JsonParseException{
        try{
            // first, get the parent
            MyDeserialParent parent = gson.fromJson( json, MyDeserialParent.class );
            // get the child using the type parameter
            String type = ((JsonObject)json).get( "type" ).getAsString();
            parent.mSerialChild = gson.fromJson( json, CHILDREN_TO_TYPETOKEN.get( type ) );
            return parent;

        }catch( Exception e ){
            e.printStackTrace();
        }
        return null;
    }
}

remarks:

  • the custom adapter must be registered on the gsonBuilder
  • if you need some custom gson properties for your children, you can pass the Gson object in the constructor of MyParentAdapter, since right now it uses the default one;
  • children and parent must have attributes with distinct names;
  • every new type must be added to the map with the corresponding class.

Complete example

Main:

public class DeserializeExample{

    MyDeserialParent[] myDeserialParents;

    static String json = "{\n" +
            "    \"myDeserialParents\" : [\n" +
            "        {\n" +
            "            \"otherProp\": \"lala\"," +
            "            \"type\": \"value\", //used in a TypeAdapter to choose child implementation\n" +
            "            \"childProp1\": \"1\",\n" +
            "            \"childProp2\": \"2\"\n" +
            "         }\n" +
            "     ]\n" +
            "}";


    public static void main( String[] args ){
        Gson gson = new GsonBuilder().registerTypeAdapter( MyDeserialParent.class, new MyParentAdapter() ).create();
        DeserializeExample result = gson.fromJson( json, DeserializeExample.class );
        System.out.println( gson.toJson( result ));
        // output: 
        // {"myDeserialParents":[{"mSerialChild":{"childProp1":"1","childProp2":"2"},"otherProp":"lala"}]}
    }//end main

}//end class

Parent:

class MyDeserialParent<T extends MyChildInterface>{

    MyChildInterface mSerialChild;
    //some other fields (not 'type')
    String otherProp;
}

child:

public class MyChild1 implements MyChildInterface {
    String childProp1;
    String childProp2;
}//end class
like image 114
Derlin Avatar answered Nov 04 '22 00:11

Derlin