irobotxx
irobotxx

Reputation: 6063

How do I parse a nested JSON array of object with a custom Gson deserializer?

Good day all. I am having a bit of trouble understanding this. I have a JSON that looks like this:

  {
      "data": [
        {
          "id": "43",
          "type": "position",
          "attributes": {
            "address-id": "1",
            "employer-id": "11"
          }
        }
      ],
      "included": [
        {
          "id": "1",
          "type": "address",
          "attributes": {
            "line-1": "21 london london",
            "line-2": "",
            "line-3": "",
            "locality": "",
            "region": "London",
            "post-code": "",
            "country": "UK",
            "latitude": "",
            "longitude": ""
          }
        },
        {
          "id": "11",
          "type": "employer",
          "attributes": {
            "title": "Mr",
            "first-name": "S",
            "last-name": "T"
          }
        }
      ]
    }

And my Retrofit call is:

 @GET("/api/positions")
 Single<PositionResponse> getPosition();

And my PositionResponse class:

public class PositionResponse {

        @SerializedName("data")
        @Expose
        private List<DataResponse> data;
        @SerializedName("included")
        @Expose
        private List<IncludedModel> included;

        public List<DataResponse> getData() {
            return data;
        }

        public void setData(List<DataResponse> data) {
            this.data = data;
        }

        public List<IncludedModel> getIncluded() {
            return included;
        }

        public void setIncluded(List<IncludedModel> included) {
            this.included = included;
        }

        }
    }

Now imagine it has a lot more data. How can I create a custom TypeAdapter or JsonDeserializer for parsing the List<IncludedModel>? For some reason, I can create a custom JsonDeserializer or TypeAdapter for Object, but when it comes to a List, I don't seem to be able to get that to work.

My TypeAdapter is as follows:

  public class IncludedTypeAdapter extends TypeAdapter<ArrayList<IncludedModel>> {

        @Override
        public void write(JsonWriter out, ArrayList<IncludedModel> value) throws IOException {

        }

        @Override
        public ArrayList<IncludedModel> read(JsonReader in) throws IOException {
            ArrayList<IncludedModel> list = new ArrayList<>();
            IncludedModel model = new IncludedModel();
            Gson gson = new Gson();
            in.beginArray();
            String id = null;
            //in.beginObject();
            while(in.hasNext()){
                JsonToken nextToken = in.peek();

                if(JsonToken.BEGIN_OBJECT.equals(nextToken)){
                    in.beginObject();

                } else if(JsonToken.NAME.equals(nextToken)){

                    if(JsonToken.NAME.name().equals("id")){
                        id = in.nextString();
                        model.setId(id);

                    } else if(JsonToken.NAME.name().equals("type")){
                        String type = in.nextString();
                        model.setMytype(type);

                        switch (type) {
                            case BaseModelType.Employer:
                                EmployerResponse employer = gson.fromJson(in, EmployerResponse.class);
                                model.setEmployer(employer);
                                break;
                        }
                    }
                }
            }
            list.add(model);

            return list;
        }

And i register to my Gson:

  GsonBuilder gsonBuilder = new GsonBuilder();
      gsonBuilder.registerTypeAdapter(IncludeModel.class, new IncludedTypeAdapter());
     //gsonBuilder.registerTypeAdapter(new IncludedTypeAdapter());
      gsonBuilder.serializeNulls();
      Gson gson = gsonBuilder.create();

      return gson;

Which I register on retrofit through GsonConverterFactory.

I am getting:

Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 6292 path $.included[0]

which I suspect is because my Retrofit response is <PositionResponse> which is a JsonObject.

To summarize my question: how do I deserialize the List<IncludeModel> object with my own custom type adapter bearing in mind the response type from my Retrofit service is PositionResponse? Many thanks for your patients and answers.

Upvotes: 0

Views: 1442

Answers (1)

Lyubomyr Shaydariv
Lyubomyr Shaydariv

Reputation: 21105

It's easy if you're using JSON tree models using JsonDeserializer. Pure type adapters are somewhat an overkill (as well as RuntimeTypeAdapterFactory is, I think, since it's still tree-oriented), and in the most simple case for your JSON document you could use something like this (you can find a similar approach in my yesterday answer having some more explanations, but you case slightly differs).

I'm assuming you would like to have mappings like these:

abstract class Element {

    final String id = null;

    private Element() {
    }

    static final class Address
            extends Element {

        @SerializedName("line-1") final String line1 = null;
        @SerializedName("line-2") final String line2 = null;
        @SerializedName("line-3") final String line3 = null;
        @SerializedName("locality") final String locality = null;
        @SerializedName("region") final String region = null;
        @SerializedName("post-code") final String postCode = null;
        @SerializedName("country") final String country = null;
        @SerializedName("latitude") final String latitude = null;
        @SerializedName("longitude") final String longitude = null;

        private Address() {
        }

        @Override
        public String toString() {
            return country + " " + region;
        }

    }

    static final class Employer
            extends Element {

        @SerializedName("title") final String title = null;
        @SerializedName("first-name") final String firstName = null;
        @SerializedName("last-name") final String lastName = null;

        private Employer() {
        }

        @Override
        public String toString() {
            return title + ' ' + firstName + ' ' + lastName;
        }

    }

    static final class Position
            extends Element {

        @SerializedName("address-id") final String addressId = null;
        @SerializedName("employer-id") final String employerId = null;

        private Position() {
        }

        @Override
        public String toString() {
            return '(' + addressId + ';' + employerId + ')';
        }

    }

}

All you have to do is just:

  • determine the expected object type;
  • "align" the JSON tree (if it fits your needs sure);
  • just delegate the deserialization work to Gson via the deserialization context (your example does not do it well: you're instantiating Gson once again losing the original configuration; you redo all Gson can do out of box: lists and POJO by reflection; JsonToken are much better if checked via switch (by the way enums are singletons and it's perfectly legal to compare them using reference equality ==), etc).

So, it can be implemented by something like this:

final class ElementJsonDeserializer
        implements JsonDeserializer<Element> {

    private static final JsonDeserializer<Element> elementJsonDeserializer = new ElementJsonDeserializer();

    private ElementJsonDeserializer() {
    }

    static JsonDeserializer<Element> getElementJsonDeserializer() {
        return elementJsonDeserializer;
    }

    @Override
    public Element deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        final JsonObject jsonObject = jsonElement.getAsJsonObject();
        final String typeCode = jsonObject.getAsJsonPrimitive("type").getAsString();
        final Class<? extends Element> clazz;
        switch ( typeCode ) {
        case "address":
            clazz = Element.Address.class;
            break;
        case "employer":
            clazz = Element.Employer.class;
            break;
        case "position":
            clazz = Element.Position.class;
            break;
        default:
            throw new JsonParseException("Unrecognized type: " + typeCode);
        }
        reattach(jsonObject, "attributes");
        return context.deserialize(jsonElement, clazz);
    }

    private static void reattach(final JsonObject parent, final String property) {
        final JsonObject child = parent.getAsJsonObject(property);
        parent.remove(property); // remove after we're sure it's a JSON object
        copyTo(parent, child);
    }

    private static void copyTo(final JsonObject to, final JsonObject from) {
        for ( final Entry<String, JsonElement> e : from.entrySet() ) {
            to.add(e.getKey(), e.getValue());
        }
    }

}

Of course, you can refactor the above to extract a strategy to implement the strategy design pattern to reuse it. Put it all together:

final class Response {

    final List<Element> data = null;
    final List<Element> included = null;

}

(The above one looks like a Map<String, List<Element>> but you decide).

private static final Gson gson = new GsonBuilder()
        .registerTypeAdapter(Element.class, getElementJsonDeserializer())
        .create();

public static void main(final String... args)
        throws IOException {
    try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43811168.class, "data.json") ) {
        final Response response = gson.fromJson(jsonReader, Response.class);
        dump(response.data);
        dump(response.included);
    }
}

private static void dump(final Iterable<Element> elements) {
    for ( final Element e : elements ) {
        System.out.print(e.getClass().getSimpleName());
        System.out.print(" #");
        System.out.print(e.id);
        System.out.print(": ");
        System.out.println(e);
    }
}

Output:

Position #43: (1;11)
Address #1: UK London
Employer #11: Mr S T

Upvotes: 1

Related Questions