1

I'm using GSON for persist and recover data into my app. The problem is that, in come cases, I have objects that are referenced in two different objects, I mean, the same instance is referenced. So multiple paths can lead to the same object.

When persisting the decoded GSON string of my model, In case of existing two references for the same object in two places, it persist them apparently correct, but when opening the app again and load the data and decoding the GSON string, two different instances of the same object are being created instead of being the same instance. Doing a change in the first instance doesn't reflects in the second instance, because are different objects after decoding the json.

This is a trace of the problem:

Having Model, Person and Car:

public class Model{
    Car car;
    Person person;
}

public class Person{
    Car car;
}

I set the same instance of car to model and person:

Car car = new Car();
model.setCar(car);
person.setCar(car);

car is the same instance in car and person, and now I encode and persist the data with GSON:

Gson gson = new Gson();
String json = gson.toJson(model);

Then I close the app and reopen the app, and I decode the json String to recover the model:

Gson gson = new Gson();
gson.fromJson(json, Model.class);

Now, I have two different instances of Car, one inside person and other inside Model, they are not the same instance but must be the same instance! If I modify the car of Model then the car of person is not modified, that's an error.

How to solve this?

halfer
  • 19,824
  • 17
  • 99
  • 186
NullPointerException
  • 36,107
  • 79
  • 222
  • 382

1 Answers1

2

Gson by default does not provide any way to cache instances and check whether it was already seen or not. To do that we need to implement custom com.google.gson.TypeAdapterFactory. Also, we need to assume that Car class (and Person class if needed) implements properly public boolean equals(Object o) and public int hashCode() so we can use Map to cache all instances.

Let's assume that your model looks like below:

class Model {
    private Car car;
    private Person person;

    // getters, setters, toString
}

class Person {
    private int id;
    private String name;
    private Car car;

    // getters, setters, toString

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

class Car {
    private int id;
    private String name;

    // getters, setters, toString

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return id == car.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

Car and Person classes have id fields which we use to distinguish instances. You can use any properties you want in your implementation.

Custom adapter implementation which uses Map to cache instances:

class CachedInstancesTypeAdapterFactory implements TypeAdapterFactory {

    private final Map<Class, Map> cachedMaps = new HashMap<>();

    public CachedInstancesTypeAdapterFactory(Set<Class> customizedClasses) {
        Objects.requireNonNull(customizedClasses);
        customizedClasses.forEach(clazz -> cachedMaps.compute(clazz, (c, m) -> new HashMap<>()));
    }

    public final <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        if (cachedMaps.containsKey(type.getRawType())) {
            final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
            return createCustomTypeAdapter(delegate);
        }

        return null;
    }

    @SuppressWarnings("unchecked")
    private <T> TypeAdapter<T> createCustomTypeAdapter(TypeAdapter<T> delegate) {
        return new TypeAdapter<T>() {
            @Override
            public void write(JsonWriter out, T value) throws IOException {
                delegate.write(out, value);
            }

            @Override
            public T read(JsonReader in) throws IOException {
                Object deserialized = delegate.read(in);

                Map tInstances = Objects.requireNonNull(cachedMaps.get(deserialized.getClass()));
                return (T) tInstances.computeIfAbsent(deserialized, k -> deserialized);
            }
        };
    }
}

And below you find example how to use it:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

public class GsonApp {

    public static void main(String[] args) {
        Gson gson = createGson();

        String json = gson.toJson(createModel());
        System.out.println(json);

        Model result = gson.fromJson(json, Model.class);

        System.out.println(result);
        System.out.println("Two car instances are the same: " + (result.getCar() == result.getPerson().getCar()));
    }

    private static Model createModel() {
        Car car = new Car();
        car.setId(9943);
        car.setName("Honda");

        Person person = new Person();
        person.setId(123);
        person.setName("Jon");
        person.setCar(car);

        Model model = new Model();
        model.setCar(car);
        model.setPerson(person);
        return model;
    }

    private static Gson createGson() {
        Set<Class> classes = new HashSet<>();
        classes.add(Car.class);
        classes.add(Person.class);

        return new GsonBuilder()
                .setPrettyPrinting()
                .registerTypeAdapterFactory(new CachedInstancesTypeAdapterFactory(classes))
                .create();
    }
}

Above code prints, firstly JSON:

{
  "car": {
    "id": 9943,
    "name": "Honda"
  },
  "person": {
    "id": 123,
    "name": "Jon",
    "car": {
      "id": 9943,
      "name": "Honda"
    }
  }
}

And after that:

Model{car=Car{id=9943, name='Honda'}, person=Person{id=123, name='Jon', car=Car{id=9943, name='Honda'}}}
Two car instances are the same: true

Note

Above CachedInstancesTypeAdapterFactory implementation is not thread safety. Moreover, you must create always new Gson object for each thread and for each attempt when you want to deserialise JSON payload with Car and Person instances. Reason is CachedInstancesTypeAdapterFactory#cachedMaps object can be used only once.

See also:

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146