Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make GSON accept single objects where it expects arrays

Tags:

java

json

gson

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?

like image 784
Matthias Avatar asked Apr 14 '17 13:04

Matthias


1 Answers

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'}]}

like image 190
Lyubomyr Shaydariv Avatar answered Sep 19 '22 00:09

Lyubomyr Shaydariv