Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GSON deserialization with generic types and generic field names

Let's say we have structure like this:

JSON:

{
  "body": {
    "cats": [
      {
        "cat": {
          "id": 1,
          "title": "cat1"
        }
      },
      {
        "cat": {
          "id": 2,
          "title": "cat2"
        }
      }
    ]
  }
}

And corresponding POJO:

Response.class

private final Body body;

Body.class

private final Collection<CatWrapper> cats

CatWrapper.class

private final Cat cat

Cat.class

private final int id;
private final String title;

But let say now we have the same structure, but instead of Cat we receive truck

{
  "body": {
    "trucks": [
      {
        "truck": {
          "id": 1,
          "engine": "big",
          "wheels": 12
        }
      },
      {
        "truck": {
          "id": 2,
          "engine": "super big",
          "wheels": 128
        }
      }
    ]
  }
}

I'm using GSON (default Retrofit json parser) on Android, consider this while giving solutions.

We could use generics here so response would look like:

private final Body<ListResponse<ItemWrapper<T>> but we can't since the field names are specific to a class.

QUESTION:

What Can I do to serialize it automaticaly without creating so many boilerplate classes? I don't really need separate classes like BodyCat, BodyTruck, BodyAnyOtherClassInThisStructure and I'm looking for a way to avoid having them.

@EDIT:

I've change classes (cat, dog -> cat,truck) due to inheritance confusion, classes used here as example DO NOT extends one another

like image 419
Than Avatar asked Jan 07 '16 16:01

Than


1 Answers

One idea would be to define a custom generic deserializer. Its generic type will represent the concrete class of the list's elements wrapped in a Body instance.

Assuming the following classes:

class Body<T> {
    private List<T> list;

    public Body(List<T> list) {
        this.list = list;
    }
}

class Cat {
    private int id;
    private String title;

    ...
}

class Truck {
    private int id;
    private String engine;
    private int wheels;

    ...
}

The deserializer assumes that the structure of the json is always the same, in the sense that you have an object that contains an object named "body". Also it assumes that the value in the first key-value pair of this body is a list.

Now for each element in the json array, we need again to fetch the inner object associated with each key. We deserialize this value and put it in the list.

class CustomJsonDeserializer<T> implements JsonDeserializer<Body<T>> {

    private final Class<T> clazz;

    public CustomJsonDeserializer(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public Body<T> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        JsonObject body = json.getAsJsonObject().getAsJsonObject("body");
        JsonArray arr = body.entrySet().iterator().next().getValue().getAsJsonArray();
        List<T> list = new ArrayList<>();
        for(JsonElement element : arr) {
            JsonElement innerElement = element.getAsJsonObject().entrySet().iterator().next().getValue();
            list.add(context.deserialize(innerElement, clazz));
        }
        return new Body<>(list);
    }
}

For the final step, you just need to create the corresponding type, instantiate and register the adapter in the parser. For instance for trucks:

Type truckType = new TypeToken<Body<Truck>>(){}.getType();
Gson gson = new GsonBuilder()
    .registerTypeAdapter(truckType, new CustomJsonDeserializer<>(Truck.class))
    .create();
Body<Truck> body = gson.fromJson(new FileReader("file.json"), truckType);

You can even return the list directly from the adapter if you want to get rid of the Body class.

With the trucks you'll get [1_big_12, 2_super big_128] as output and [1_cat1, 2_cat2] with the cats.

like image 161
Alexis C. Avatar answered Oct 21 '22 01:10

Alexis C.