Make GSON accept single objects where it expects arrays

后端 未结 4 675
你的背包
你的背包 2020-12-06 06:29

I have bunch of model classes which have fields of type List where X is one of many things (e.g. String, Integer

4条回答
  •  失恋的感觉
    2020-12-06 06:58

    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
            implements TypeAdapterFactory {
    
        // Gson can instantiate it itself
        private AlwaysListTypeAdapterFactory() {
        }
    
        @Override
        public  TypeAdapter create(final Gson gson, final TypeToken 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 elementTypeAdapter = (TypeAdapter) 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 alwaysListTypeAdapter = (TypeAdapter) 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
                extends TypeAdapter> {
    
            private final TypeAdapter elementTypeAdapter;
    
            private AlwaysListTypeAdapter(final TypeAdapter elementTypeAdapter) {
                this.elementTypeAdapter = elementTypeAdapter;
            }
    
            @Override
            public void write(final JsonWriter out, final List list) {
                throw new UnsupportedOperationException();
            }
    
            @Override
            public List read(final JsonReader in)
                    throws IOException {
                // This is where we detect the list "type"
                final List list = new ArrayList<>();
                final JsonToken token = in.peek();
                switch ( token ) {
                case BEGIN_ARRAY:
                    // If it's a regular list, just consume [, , 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 foo = null;
    
        @JsonAdapter(AlwaysListTypeAdapterFactory.class)
        final List 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'}]}

提交回复
热议问题