Guimareshh
Guimareshh

Reputation: 1284

ARRAY or OBJECT in Retrofit on Android using TypeAdapter, in a two depth level

I'm struggling with TypeAdapter. Indeed for a json field, I can have an Array (when it's empty) or an Object (when it's not empty). This can't be changed.

Here is the JSON received :

{
    "notifications": [
        {
            ...
        }
    ],
    "meta": {
        "pagination": {
            "total": 13,
            "count": 13,
            "per_page": 20,
            "current_page": 1,
            "total_pages": 1,
            "links": []
        }
    }
}

The field concerned is links, as you can see the field is inside pagination, which is inside meta. And that's my issue, I don't know how the TypeAdapter has to handle links in a two depth level.

I used this reply to start building a solution. Here it is :

My Custom TypeAdapter class :

public class PaginationTypeAdapter extends TypeAdapter<Pagination> {

    private Gson gson = new Gson();

    @Override
    public void write(JsonWriter out, Pagination pagination) throws IOException {
        gson.toJson(pagination, Links.class, out);
    }

    @Override
    public Pagination read(JsonReader jsonReader) throws IOException {

        Pagination pagination;

        jsonReader.beginObject();

        if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
            pagination = new Pagination((Links[]) gson.fromJson(jsonReader, Links[].class));
        } else if(jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
            pagination = new Pagination((Links) gson.fromJson(jsonReader, Links.class));
        } else {
            throw new JsonParseException("Unexpected token " + jsonReader.peek());
        }

        return pagination;
    }
}

My Pagination class :

public class Pagination {

    private int total;

    private int count;

    @SerializedName("per_page")
    private int perPage;

    @SerializedName("current_page")
    private int currentPage;

    @SerializedName("total_pages")
    private int totalPages;

    private Links links;


    Pagination(Links ... links) {
        List<Links> linksList = Arrays.asList(links);
        this.links = linksList.get(0);
    }
}

And I'm building my Gson object like that :

Gson gson = new GsonBuilder().registerTypeAdapter(Pagination.class, new PaginationTypeAdapter()).create();

For now my error is : com.google.gson.JsonParseException: Unexpected token NAME

So I know I'm not doing it right, because I'm building my Gson with pagination. But I don't know how it should be handle. Using a TypeAdapter with meta ?

Any help will be welcome, thanks !

Upvotes: 1

Views: 1588

Answers (1)

Lyubomyr Shaydariv
Lyubomyr Shaydariv

Reputation: 21105

When you implement a custom type adapter, make sure that your type adapter has balanced token reading and writing: if you open a composite token pair like [ and ], you have to close it (applies for both JsonWriter and JsonReader). You just don't need this line to fix your issue:

jsonReader.beginObject();

because it moves the JsonReader instance to the next token, so the next token after BEGIN_OBJECT is either NAME or END_OBJECT (the former in your case sure).


Alternative option #1

I would suggest also not to use ad-hoc Gson object instatiation -- this won't share the configuration between Gson instances (say, your "global" Gson has a lot of custom adapters registered, but this internal does not have any thus your (de)serialization results might be very unexpected). In order to overcome this, just use TypeAdapterFactory that is more context-aware than a "free" Gson instance.

final class PaginationTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory paginationTypeAdapterFactory = new PaginationTypeAdapterFactory();

    private PaginationTypeAdapterFactory() {
    }

    static TypeAdapterFactory getPaginationTypeAdapterFactory() {
        return paginationTypeAdapterFactory;
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        // Classes can be compared using == and !=
        if ( typeToken.getRawType() != Pagination.class ) {
            // Not Pagination? Let Gson pick up the next best-match
            return null;
        }
        // Here we get the references for two types adapters:
        // - this is what Gson.fromJson does under the hood
        // - we save some time for the further (de)serialization
        // - you classes should not ask more than they require
        final TypeAdapter<Links> linksTypeAdapter = gson.getAdapter(Links.class);
        final TypeAdapter<Links[]> linksArrayTypeAdapter = gson.getAdapter(Links[].class);
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) new PaginationTypeAdapter(linksTypeAdapter, linksArrayTypeAdapter);
        return typeAdapter;
    }

    private static final class PaginationTypeAdapter
            extends TypeAdapter<Pagination> {

        private final TypeAdapter<Links> linksTypeAdapter;
        private final TypeAdapter<Links[]> linksArrayTypeAdapter;

        private PaginationTypeAdapter(final TypeAdapter<Links> linksTypeAdapter, final TypeAdapter<Links[]> linksArrayTypeAdapter) {
            this.linksTypeAdapter = linksTypeAdapter;
            this.linksArrayTypeAdapter = linksArrayTypeAdapter;
        }

        @Override
        public void write(final JsonWriter out, final Pagination pagination)
                throws IOException {
            linksTypeAdapter.write(out, pagination.links);
        }

        @Override
        public Pagination read(final JsonReader in)
                throws IOException {
            final JsonToken token = in.peek();
            // Switches are somewhat better: you can let your IDE or static analyzer to check if you covered ALL the cases
            switch ( token ) {
            case BEGIN_ARRAY:
                return new Pagination(linksArrayTypeAdapter.read(in));
            case BEGIN_OBJECT:
                return new Pagination(linksTypeAdapter.read(in));
            case END_ARRAY:
            case END_OBJECT:
            case NAME:
            case STRING:
            case NUMBER:
            case BOOLEAN:
            case NULL:
            case END_DOCUMENT:
                // MalformedJsonException, not sure, might be better, because it's an IOException and the read method throws IOException
                throw new MalformedJsonException("Unexpected token: " + token + " at " + in);
            default:
                // Maybe some day Gson adds something more here... Let be prepared
                throw new AssertionError(token);
            }
        }
    }

}

Alternative option #2

You can annotate your private Links links; with @JsonAdapter and bind a type adapter factory directly to links: Gson will "inject" links objects directly to Pagination instances, so you don't even need a constructor there.

Upvotes: 2

Related Questions