Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GSON Custom serialization with annotations

I have a very specific case of custom serialization with GSON:

Let's say I have a following class:

public class Student extends BaseModel{
    private int id;
    private String name;
    private Student goodFriend;
    private Student bestFriend;
}

BaseModel is just a base class for all my model classes.

When I simply do

gson.toJson(student /* Some Student instance */);

I will get for example:

{
 id: 1, 
 name: "Jack", 
 goodFriend: {id: 2, name: "Matt"}, 
 bestFriend: {id: 3, name "Tom"}
}

It's fine, but what I need is this:

{
 id: 1, 
 name: "Jack", 
 goodFriend: 2, // only an ID for this field
 bestFriend: {id: 3, name "Tom"} // whole object for this field
 // both fields are of the same Type, so I can't use TypeAdapterFactory for this
}

I need some way of marking the fields with serialization type (id or object) and then using that marking to serialize as needed. How do I do that in general, not just for a Student class, but for all subclasses of BaseModel?

My only idea was to use custom annotations: describing the fields that I want to serialize as ID only with one annotation, and fields that I want to serialize as objects with another annotation, but I couldn't find a way to retrieve the annotations in TypeAdapter's write method.

Any ideas how to handle this?

like image 548
jdabrowski Avatar asked May 05 '17 20:05

jdabrowski


1 Answers

I found an answer myself. It turns out there already is an annotation for this kind of case in GSON. It's called @JsonAdapter.

First I had to create a TypeAdapterFactory:

public class BaseModelForeignKeyTypeAdapterFactory implements TypeAdapterFactory {

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        if (!BaseModel.class.isAssignableFrom(type.getRawType())) {
            return null;
        }

        TypeAdapter defaultAdapter = gson.getAdapter(type);

        //noinspection unchecked
        return (TypeAdapter<T>) new Adapter(defaultAdapter);
    }

    private static class Adapter<T extends BaseModel> extends TypeAdapter<T> {

        private final TypeAdapter<T> defaultAdapter;

        Adapter(TypeAdapter<T> defaultAdapter) {
            this.defaultAdapter = defaultAdapter;
        }

        @Override
        public void write(JsonWriter out, T value) throws IOException {
            out.value(value.getId());
        }

        @Override
        public T read(JsonReader in) throws IOException {
            return defaultAdapter.read(in);
        }
    }
}

In the create() method I retrieve the default adapter Gson would use for this field and pass it to the Adapter for use when deserializing the field. This way this Adapter is only used for serialization, while deserialization is delegated to the default adapter.

Now I just need to annotate the fields in my Student class, which I want to be serialized as IDs with this TypeAdapterFactory like this:

public class Student extends BaseModel{
    private int id;
    private String name;

    @JsonAdapter(BaseModelForeignKeyTypeAdapterFactory.class)
    private Student goodFriend;

    private Student bestFriend;
}

And this is all, now gson.toJson(student) will output:

{
 id: 1, 
 name: "Jack", 
 goodFriend: 2, // using "ForeignKey" TypeAdapter
 bestFriend: {id: 3, name "Tom"} // using default TypeAdapter
}

I hope this helps someone!

like image 104
jdabrowski Avatar answered Oct 15 '22 04:10

jdabrowski