Reputation: 1284
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
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).
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);
}
}
}
}
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