Search code examples
androidandroid-jsonjsonserializergson

Gson serialize with two List types in a single JSON object


I have two types of list to bind the request using Gson library. I tried in a way, it is working as excepted. But I want to know whether we have any default procedure to serialize the lists.

I tried below and working. But this is not the way to implement. Searching for better solution.

class Request{
    @Expose
    @SerializedName("ListC")
    private List<TypeOne> mList;

    public Request(List<TypeOne> listOne, List<TypeTwo> listTwo){
        this.mList = new ArrayList<>();
        for (int i = 0; i < listTwo.size(); i++) {
           // Adding the Typetwo values in TypeOne model class - Not best approach
           listOne.get(i).setAmount(listTwo.get(i).getAmount());
           listOne.get(i).setName(listTwo.get(i).getName());
           listOne.get(i).setType(listTwo.get(i).getType());
        }
        this.mList.addAll(listOne);
    }

    public String getJsonString(){
         Gson gson = new GsonBuilder()
            .registerTypeAdapter(TypeOne.class, new Serializer())
            .registerTypeAdapter(Date.class, new DateSerializer())
            .excludeFieldsWithoutExposeAnnotation()
            .create();

    JsonObject in = new JsonObject();
    in.add("in", gson.toJsonTree(this));

    JsonObject obj = new JsonObject();
    obj.add("req", in);

    return obj.toString();
    }
}

Is anyone faced the similar issue and fixed this, please direct me to get it done. Trying from long back to find better solution.

Output JSON:

{
"list":[
    Obj{
    "Type_1_object_1":"value",
    "Type_1_object_2":"value",
    "Type_1_object_3":"value",
    "Type_2_object_5":"value",
    "Type_2_object_6":"value"
    },
    Obj{
    "Type_1_object_1":"value",
    "Type_1_object_2":"value",
    "Type_1_object_3":"value", 
    "Type_2_object_5":"value", 
    "Type_2_object_6":"value"
    }
]
}

Solution

  • There are at least two approaches that are different at the very base:

    • Using data transfer objects letting you control the result using object mapping
    • Using type adapters (and possibly streaming, but I'm not sure if it's possible in Gson for you case)

    Let's assume the following data models:

    final class User {
    
        final String username;
        final String contact;
    
        User(final String username, final String contact) {
            this.username = username;
            this.contact = contact;
        }
    
    }
    
    final class FooBar {
    
        final int foo;
        final int bar;
    
        FooBar(final int foo, final int bar) {
            this.foo = foo;
            this.bar = bar;
        }
    
    }
    

    And suppose these two should be zipped/merged.

    Data transfer objects

    The result DTO might look as follows:

    final class UserFooBarDto {
    
        final String username;
        final String contact;
        final Integer foo;
        final Integer bar;
    
        private UserFooBarDto(final String username, final String contact, final Integer foo, final Integer bar) {
            this.username = username;
            this.contact = contact;
            this.foo = foo;
            this.bar = bar;
        }
    
        static UserFooBarDto userFooBarDto(final User user) {
            return new UserFooBarDto(user.username, user.contact, null, null);
        }
    
        static UserFooBarDto userFooBarDto(final FooBar fooBar) {
            return new UserFooBarDto(null, null, fooBar.foo, fooBar.bar);
        }
    
        static UserFooBarDto userFooBarDto(final User user, final FooBar fooBar) {
            return new UserFooBarDto(user.username, user.contact, fooBar.foo, fooBar.bar);
        }
    
    }
    

    Then the result might be accomplished using the following code:

    static void main(final String... args) {
        final List<User> users = ImmutableList.of(
                new User("john.doe", "john.doe@mail.com"),
                new User("alice.bob", "alice.and.bob@mail.com")
        );
        final List<FooBar> fooBars = ImmutableList.of(
                new FooBar(1, 2),
                new FooBar(3, 4),
                new FooBar(5, 6),
                new FooBar(7, 8)
        );
        final List<UserFooBarDto> zippedList = zip(users, fooBars, zipper);
        final String json = gson.toJson(zippedList);
        System.out.println(json);
    }
    
    private static final Gson gson = new Gson();
    
    // It's a good idea to create such objects once, and use them everywhere where necessary not instantiating them over and over
    private static final IZipper<User, FooBar, UserFooBarDto, List<UserFooBarDto>> zipper = new IZipper<User, FooBar, UserFooBarDto, List<UserFooBarDto>>() {
        @Override
        public List<UserFooBarDto> collectTo() {
            return new ArrayList<>();
        }
    
        @Override
        public UserFooBarDto zip(final User user, final FooBar fooBar) {
            if ( user != null && fooBar != null ) {
                return userFooBarDto(user, fooBar);
            } else if ( user != null ) {
                return userFooBarDto(user);
            } else if ( fooBar != null ) {
                return userFooBarDto(fooBar);
            } else {
                throw new AssertionError();
            }
        }
    };
    
    // A simple zipper interface that would tell where collect the zipped result to and how a zipped result element should be combined from
    private interface IZipper<I1, I2, O, C extends Collection<O>> {
    
        C collectTo();
    
        O zip(I1 i1, I2 i2);
    
    }
    
    private static <I1, I2, O, C extends Collection<O>> C zip(
            final Iterable<? extends I1> list1,
            final Iterable<? extends I2> list2,
            final IZipper<? super I1, ? super I2, ? extends O, C> zipper
    ) {
        final C collection = zipper.collectTo();
        final Iterator<? extends I1> iterator1 = list1.iterator();
        final Iterator<? extends I2> iterator2 = list2.iterator();
        while ( iterator1.hasNext() || iterator2.hasNext() ) {
            final I1 i1 = iterator1.hasNext() ? iterator1.next() : null;
            final I2 i2 = iterator2.hasNext() ? iterator2.next() : null;
            final O o = zipper.zip(i1, i2);
            collection.add(o);
        }
        return collection;
    }
    

    Type adapters

    Another, more "dynamic", approach is using type adapters that may be somewhat harder to implement, but probably easier to use.

    static void main(final String... args) {
        final List<User> users = ImmutableList.of(
                new User("john.doe", "john.doe@mail.com"),
                new User("alice.bob", "alice.and.bob@mail.com")
        );
        final List<FooBar> fooBars = ImmutableList.of(
                new FooBar(1, 2),
                new FooBar(3, 4),
                new FooBar(5, 6),
                new FooBar(7, 8)
        );
        final ZippedList<User, FooBar> zippedList = new ZippedList<>(users, fooBars);
        final String json = gson.toJson(zippedList, userAndFooBarZippedListType);
        System.out.println(json);
    }
    
    // TypeToken types are immutable types and can be safely assigned to static final fields
    private static final Type userAndFooBarZippedListType = new TypeToken<ZippedList<User, FooBar>>() {}.getType();
    
    // Gson instances are thread-safe and can be instantiated once too more saving instantiation time
    private static final Gson gson = new GsonBuilder()
            .registerTypeAdapterFactory(new ZippedListTypeAdapterFactory())
            .create();
    
    // A special container class to let Gson pick a proper type adapter
    private static final class ZippedList<T1, T2> {
    
        private final List<T1> list1;
        private final List<T2> list2;
    
        private ZippedList(final List<T1> list1, final List<T2> list2) {
            this.list1 = list1;
            this.list2 = list2;
        }
    
    }
    
    private static final class ZippedListTypeAdapterFactory
            implements TypeAdapterFactory {
    
        @Override
        public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
            // Not a class we must handle ourselves? Let Gson pick another best type adapter itself
            if ( !ZippedList.class.isAssignableFrom(typeToken.getRawType()) ) {
                return null;
            }
            // Narrowing down the scope of @SuppressWarnings("unchecked") and making the type adapter to take care for nulls automatically
            @SuppressWarnings("unchecked")
            final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) new ZippedListTypeAdapter<>(gson).nullSafe();
            return typeAdapter;
        }
    
    }
    
    private static final class ZippedListTypeAdapter<T1, T2>
            extends TypeAdapter<ZippedList<T1, T2>> {
    
        private final Gson gson;
    
        private ZippedListTypeAdapter(final Gson gson) {
            this.gson = gson;
        }
    
        @Override
        @SuppressWarnings("resource")
        public void write(final JsonWriter out, final ZippedList<T1, T2> zippedList)
                throws IOException {
            // Write [ to the output
            out.beginArray();
            final Iterator<? extends T1> iterator1 = zippedList.list1.iterator();
            final Iterator<? extends T2> iterator2 = zippedList.list2.iterator();
            // Iterate over two sequences trying to merge their respective elements JSON representations
            while ( iterator1.hasNext() || iterator2.hasNext() ) {
                final T1 i1 = iterator1.hasNext() ? iterator1.next() : null;
                final T2 i2 = iterator2.hasNext() ? iterator2.next() : null;
                // This is not very efficient because it builds in-memory JSON trees thus consuming memory
                // It would be nice if it would be possible to decorate JsonWriter to control its beginObject and endObject
                // The latter control would help to suppress { and } at the top level, and delegate the real serialization to Gson with the decorated JsonWriter
                // But JsonWriter constructor requires a Writer, not at JsonWriter, and we do not have where to obtain a writer instance from
                // So we can just merge the trees...
                final JsonElement tree = mergeInto(gson.toJsonTree(i1), gson.toJsonTree(i2));
                gson.toJson(tree, out);
            }
            // Write ] to the output
            out.endArray();
        }
    
        @Override
        public ZippedList<T1, T2> read(final JsonReader in) {
            throw new UnsupportedOperationException();
        }
    
        // JSON object types dispatching party...
        private static JsonElement mergeInto(final JsonElement e1, final JsonElement e2) {
            if ( e1.isJsonNull() ) {
                if ( e2.isJsonObject() ) {
                    return mergeInto(e1.getAsJsonNull(), e2.getAsJsonObject());
                } else {
                    throw new AssertionError("TODO: " + e2.getClass());
                }
            } else if ( e1.isJsonObject() ) {
                if ( e2.isJsonObject() ) {
                    return mergeInto(e1.getAsJsonObject(), e2.getAsJsonObject());
                } else {
                    throw new AssertionError("TODO: " + e2.getClass());
                }
            } else {
                throw new AssertionError("TODO: " + e1.getClass());
            }
        }
    
        // A bunch of specialized mergeInto overloads letting javac to pick the best one
        private static JsonObject mergeInto(@SuppressWarnings("unused") final JsonNull jsonNull1, final JsonObject jsonObject2) {
            return jsonObject2;
        }
    
        private static JsonObject mergeInto(final JsonObject jsonObject1, final JsonObject jsonObject2) {
            for ( final Entry<String, JsonElement> e : jsonObject2.entrySet() ) {
                jsonObject1.add(e.getKey(), e.getValue());
            }
            return jsonObject1;
        }
    
    }
    

    Both examples produce the following JSON (prettified):

    [
        {
            "username": "john.doe",
            "contact": "john.doe@mail.com",
            "foo": 1,
            "bar": 2
        },
        {
            "username": "alice.bob",
            "contact": "alice.and.bob@mail.com",
            "foo": 3,
            "bar": 4
        },
        {
            "foo": 5,
            "bar": 6
        },
        {
            "foo": 7,
            "bar": 8
        }
    ]