19

I have bunch of model classes which have fields of type List<X> where X is one of many things (e.g. String, Integer, but also some of my own types). I'm using GSON to parse JSON representations of these models.

My problem is that the server I'm dealing with (which is beyond my control) somehow removed singleton arrays and replaces them by the contained object.

For example, instead of returning:

{ "foo": [ "bar"], "bleh": [ { "some": "object" } ] } 

It returns:

{ "foo": "bar", "bleh": { "some": "object" } } 

Now assume that the Java model class look something like this:

public class Model { private List<String> foo; private List<SomeObject> bleh; } 

Currently this causes GSON to throw an exception because it finds BEGIN_STRING or BEGIN_OBJECT where it expects BEGIN_ARRAY.

For arrays or lists of Strings this is easily solved using a TypeAdapter<List<String>>. But the problem is I have Lists with many different element types and I don't want to write a separate TypeAdapter for each case. Nor have I been able to a generic TypeAdapter<List<?>>, because at some point you need to know the type. So is there another way to configure GSON to be smart enough to turn single objects or values into arrays/lists? Or in other words, just "pretend" that the [ and ] are there where it expects to find them although they aren't there?

5 Answers 5

31

But the problem is I have Lists with many different element types and I don't want to write a separate TypeAdapter for each case. Nor have I been able to a generic TypeAdapter>, because at some point you need to know the type.

This is what type adapter factories are designed for: you can control every type in Gson instance configuration.

final class AlwaysListTypeAdapterFactory<E> implements TypeAdapterFactory { // Gson can instantiate it itself private AlwaysListTypeAdapterFactory() { } @Override public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) { // If it's not a List -- just delegate the job to Gson and let it pick the best type adapter itself if ( !List.class.isAssignableFrom(typeToken.getRawType()) ) { return null; } // Resolving the list parameter type final Type elementType = resolveTypeArgument(typeToken.getType()); @SuppressWarnings("unchecked") final TypeAdapter<E> elementTypeAdapter = (TypeAdapter<E>) gson.getAdapter(TypeToken.get(elementType)); // Note that the always-list type adapter is made null-safe, so we don't have to check nulls ourselves @SuppressWarnings("unchecked") final TypeAdapter<T> alwaysListTypeAdapter = (TypeAdapter<T>) new AlwaysListTypeAdapter<>(elementTypeAdapter).nullSafe(); return alwaysListTypeAdapter; } private static Type resolveTypeArgument(final Type type) { // The given type is not parameterized? if ( !(type instanceof ParameterizedType) ) { // No, raw return Object.class; } final ParameterizedType parameterizedType = (ParameterizedType) type; return parameterizedType.getActualTypeArguments()[0]; } private static final class AlwaysListTypeAdapter<E> extends TypeAdapter<List<E>> { private final TypeAdapter<E> elementTypeAdapter; private AlwaysListTypeAdapter(final TypeAdapter<E> elementTypeAdapter) { this.elementTypeAdapter = elementTypeAdapter; } @Override public void write(final JsonWriter out, final List<E> list) { throw new UnsupportedOperationException(); } @Override public List<E> read(final JsonReader in) throws IOException { // This is where we detect the list "type" final List<E> list = new ArrayList<>(); final JsonToken token = in.peek(); switch ( token ) { case BEGIN_ARRAY: // If it's a regular list, just consume [, <all elements>, and ] in.beginArray(); while ( in.hasNext() ) { list.add(elementTypeAdapter.read(in)); } in.endArray(); break; case BEGIN_OBJECT: case STRING: case NUMBER: case BOOLEAN: // An object or a primitive? Just add the current value to the result list list.add(elementTypeAdapter.read(in)); break; case NULL: throw new AssertionError("Must never happen: check if the type adapter configured with .nullSafe()"); case NAME: case END_ARRAY: case END_OBJECT: case END_DOCUMENT: throw new MalformedJsonException("Unexpected token: " + token); default: throw new AssertionError("Must never happen: " + token); } return list; } } } 

Now you just have to tell Gson which fields are not well-formed. Of course, you might configure the whole Gson instance to accept such lists, but let it be more precise using the @JsonAdapter annotation:

final class Model { @JsonAdapter(AlwaysListTypeAdapterFactory.class) final List<String> foo = null; @JsonAdapter(AlwaysListTypeAdapterFactory.class) final List<SomeObject> bleh = null; @Override public String toString() { return "Model{" + "foo=" + foo + ", bleh=" + bleh + '}'; } } final class SomeObject { final String some = null; @Override public String toString() { return "SomeObject{" + "some='" + some + '\'' + '}'; } } 

Test data:

single.json

{ "foo": "bar", "bleh": {"some": "object"} } 

list.json

{ "foo": ["bar"], "bleh": [{"some": "object"}] } 

Example:

private static final Gson gson = new Gson(); public static void main(final String... args) throws IOException { for ( final String resource : ImmutableList.of("single.json", "list.json") ) { try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43412261.class, resource) ) { final Model model = gson.fromJson(jsonReader, Model.class); System.out.println(model); } } } 

And the output:

Model{foo=[bar], bleh=[SomeObject{some='object'}]}
Model{foo=[bar], bleh=[SomeObject{some='object'}]}

Sign up to request clarification or add additional context in comments.

10 Comments

Works like a charm! Thanks!
Just so I understand what's going on, why do you have 2 distinct generic types in AlwaysListTypeAdapterFactory (i.e. E and T)? I see that E is the List-element type, but what is T? The List-type itself?
@Matthias Yes, the T type parameter is defined and required by Gson (it's defined as a generic method type parameter) to "align" with type tokens (see the TypeAdapterFactory interface). The List-type itself? -- yes, exactly. Unfortunately, due to how generics are implemented in Java, there is a special check above: !List.class.isAssignableFrom(typeToken.getRawType()) (literally, "not a list?"). Once we are sure it's a list, we're trying to determine it's actual parameterization type if possible, and combine two type adapters (custom for-list one and provided for-element one).
@Matthias Yep, it fails. It depends on your requirements. If you want to serialize it always as a list (and I believe it should work like this), just take the delegate type adapter from Gson using getDelegateAdapter(this, TypeToken.getParameterized(List.class, elementType)) when creating the type adapter and just delegate the job to it. If you want the type adapter to write a single object if list size is 1 or a multiple objects array if list size is 2 or more, then just add custom logic (the same what read does but reversed).
@Matthias Yep, by quick look it looks exactly what I described for the first scenario.
|
2

You can simply write your own JsonDeserializer where you check whether your bleh or foo are JsonObjects or JsonArrays.

To check if a JsonElement is an array or an object:

JsonElement element = ...; if (element.isJsonObject()) { //element is a JsonObject } else if (element.isJsonArray()) { //element is a JsonArray } 

2 Comments

That doesn't solve my problem really. What am I supposed to write in the object case if I don't know what type it is.
Gson checks for you whether the current element (can be an array, object, int etc.) is an array or an object. In case foo is given as an array, you can use the second check otherwise you can check if foo is a single string.
1

One solution to this would be to write a custom TypeAdapterFactory which creates an adapter which peeks at the JSON data. If it encounters something other than a JSON array (or JSON null) it wraps it inside a JSON array before deserializing it:

// Only intended for usage with @JsonAdapter on fields class SingleValueOrListAdapterFactory implements TypeAdapterFactory { @Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { // Note: Cannot use getDelegateAdapter due to https://github.com/google/gson/issues/1028 TypeAdapter<T> listAdapterDelegate = gson.getAdapter(type); TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class); return new TypeAdapter<T>() { @Override public void write(JsonWriter out, T value) throws IOException { listAdapterDelegate.write(out, value); } @Override public T read(JsonReader in) throws IOException { JsonToken peeked = in.peek(); if (peeked == JsonToken.NULL || peeked == JsonToken.BEGIN_ARRAY) { return listAdapterDelegate.read(in); } else { // Wrap JSON element in a new JSON array before deserializing it JsonElement jsonElement = jsonElementAdapter.read(in); JsonArray jsonArray = new JsonArray(); jsonArray.add(jsonElement); return listAdapterDelegate.fromJsonTree(jsonArray); } } }; } } 

The above implementation is designed only for usage with @JsonAdapter on fields, for example:

@JsonAdapter(SingleValueOrListAdapterFactory.class) private List<MyClass> myField; 

Compared to the currently accepted answer this provides the following advantages because it simply delegates the actual deserialization to listAdapterDelegate:

  • Custom List (or Collection) subclasses are supported because creation of them is delegated to Gson
  • Gson's default type resolution logic is used to determine the element type and to deserialize it

But it also has the following disadvantage:

  • Decreased performance because if the data is not already in a JSON array it is first deserialized to a JsonElement before the actual deserialization is performed

Comments

0

When using the GSON library, you could just check whether or not the following token is an object or an array. This of course requires you to go more fine grained while parsing the XML, but it allows you full control of what do you want to get from it. Sometimes we are not under control of the XML, and it could come handy.

This is an example to check if the next token is an object or an array, using the JsonReader class to parse the file:

if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) { jsonReader.beginArray() } else if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) { jsonReader.beginObject() } 

And at the end of the array / object, you could do the same, but for the end tokens:

if (jsonReader.peek() == JsonToken.END_ARRAY) { jsonReader.endArray() } else if (jsonReader.peek() == JsonToken.END_OBJECT) { jsonReader.endObject() } 

This way, you could have identical code (adding an extra check, to verify if you are on an array or on an object) to parse your array of objects, or a single object.

Comments

0

I had this same problem consuming xml / json from a vendor - they certainly weren't going to change their code for me :) There were several resources on the web that I used before changing adapting them to my own version This SO answer was very helpful. I spent some time looking at the gson code and finding a lot of private variable that I wanted access to. So, essentially what my custom collection adapter does is peek to see if the next element is an object. If not, we just delegate the read to the previous adapter (that we have overridden).

If the next element is an object, we use gson to process that. We then convert that to an array of one object. Use gson to write that to a string, then pass that string as a JsonReader to the underlying adapter. This can then create an instance of the underlying list and add the one element we have.

Here's the AdapterTypeFactory:

 public enum ListSingleObjectAdapterFactory implements TypeAdapterFactory { INSTANCE; // Josh Bloch's Enum singleton pattern @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) { Class<? super T> rawType = typeToken.getRawType(); if (!Collection.class.isAssignableFrom(rawType)) { return null; } TypeAdapter collectionAdapter = gson.getDelegateAdapter(this, typeToken); Class genericClass = (Class) ((ParameterizedType) typeToken.getType()) .getActualTypeArguments()[0]; return new SingleObjectOrCollectionAdapter( gson, collectionAdapter, genericClass); } } 

Then the type adapter I have is:

public class SingleObjectOrCollectionAdapter<T> extends TypeAdapter<Collection<T>> { private Class<T> adapterclass; private Gson gson; private TypeAdapter arrayTypeAdapter; public SingleObjectOrCollectionAdapter(Gson gson, TypeAdapter<T> collectionTypeAdapter, Class<T> componentType) { arrayTypeAdapter = collectionTypeAdapter; this.gson = gson; adapterclass = componentType; } @Override public Collection<T> read(JsonReader reader) throws IOException { Collection<T> collection; JsonReader myReader = reader; if (reader.peek() == JsonToken.BEGIN_OBJECT) { T inning = gson.fromJson(reader, adapterclass); String s = gson.toJson(new Object[]{inning}); myReader = new JsonReader(new StringReader(s)); } collection = (Collection)arrayTypeAdapter.read( myReader ); return collection; } @Override public void write(JsonWriter writer, Collection<T> value) throws IOException { arrayTypeAdapter.write(writer, value); } } 

Finally, we need to register the adapter factory:

GsonBuilder gb = new GsonBuilder().registerTypeAdapterFactory(ListSingleObjectAdapterFactory.INSTANCE); 

So far, it seems to be working well handling both single and multiple objects - although I wouldn't be surprised if it needs some tweaking down the road.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.