Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retrofit GSON response abstract mapping

I make a simple HTTP GET request with retrofit and try to map the json response to my model. The thing is, the json return an array of multiples Shapes, the Shape is an abstract class, so it could be a Square, Circle, etc. Every shape has his own specified model, so different fields. How can I map this array of Shape to the model?

The web service json response

{
  "requestId": 0,
  "totalShapes": 2,
  "shapes": [
    {
      "circle": {
        "code": 1,
        "radius": 220
        "color" : "blue"
      }
    },
    {
      "square": {
        "code": 1,
        "size": 220
      }
    }
  ]
}

Main result mapping :

public class Result {
  @SerializedName("requestId") private int requestId;
  @SerializedName("totalShapes") private int totalShapes;
  @SerializedName("shapes") private List<Shape> shapes;
}

Abstract class :

public abstract class Shape implements Serializable {
}

Circle :

public class Circle {
  @SerializedName("code") private int code;
  @SerializedName("radius") private int radius;
  @SerializedName("color") private String color;
  // + getters...
}

Square :

public class Square {
  @SerializedName("code") private int code;
  @SerializedName("size") private int size;
  // + getters...
}
like image 552
Alex L Avatar asked Dec 02 '15 20:12

Alex L


1 Answers

You may implement a custom shape deserializer that acts like a factory. Based on the key for a shape object, you can deserialize it to its corresponding type.

class ShapeDeserializer implements JsonDeserializer<Shape> {
    @Override
    public Shape deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        Map.Entry<String, JsonElement> entry = json.getAsJsonObject().entrySet().iterator().next();
        switch(entry.getKey()) {
            case "circle":
                return context.deserialize(entry.getValue(), Circle.class);
            case "square":
                return context.deserialize(entry.getValue(), Square.class);
            default:
                throw new IllegalArgumentException("Can't deserialize " + entry.getKey());
        }
    }
}

Then you register it in your parser

Gson gson = 
    new GsonBuilder().registerTypeAdapter(Shape.class, new ShapeDeserializer())
                     .create();

and you use it:

Result result = gson.fromJson(myJson, Result.class);
//Result{requestId=0, totalShapes=2, shapes=[Circle{code=1, radius=220, color='blue'}, Square{code=2, size=220}]}

If the key matches exactly the class name, you might use Class.forName directly instead (you'd need to capitalize the key in first place).

Also note that:

  • you could move the code property into the abstract Shape class if it appears in every subclasses
  • I assumed that you have one and only one shape associated with a key. If you can have two circles in the same JsonObject associated with the key "circle" you'd need a more complex logic to parse it. If it's not the case (although it's not recommend by the RFC to have different key-value pairs with the same key) it should work fine.
like image 141
Alexis C. Avatar answered Sep 23 '22 14:09

Alexis C.