google / gson

A Java serialization/deserialization library to convert Java Objects into JSON and back
Apache License 2.0
23.38k stars 4.29k forks source link

Load json data into existing object #1887

Open xxDark opened 3 years ago

xxDark commented 3 years ago

Hello, I wonder if it is possible (without hacking gson library internals) to load data into existing ojects without constructing new ones. Thanks for assistance.

Marcono1234 commented 3 years ago

Yes that is possible by using InstanceCreator, which can be registered using GsonBuilder.registerTypeAdapter(...). Though it depends on your usecase how useful this will be for you. Also be careful to not share the Gson instance with other threads, and ideally create a new one for every deserialization (to avoid multiple calls changing each others existing instance).

Here is an example:

private static class MyClass {
    public String f;
    public int f2;

    @Override
    public String toString() {
        return "f=" + f + ", f2=" + f2;
    }
}

public static void main(String[] args) {
    MyClass existingInstance = new MyClass();
    existingInstance.f = "test";

    Gson gson = new GsonBuilder()
        // Could also create anonymous InstanceCreator subclass instead of using lambda
        .registerTypeAdapter(MyClass.class, (InstanceCreator<?>) type -> existingInstance)
        .create();
    MyClass deserialized = gson.fromJson("{\"f2\":123}", MyClass.class);
    System.out.println("Is same instance: " + (existingInstance == deserialized));
    System.out.println(deserialized);
}
xxDark commented 3 years ago

create a new one for every deserialization (to avoid multiple calls changing each others existing instance).

Creating Gson instances for each deserialization is not cheap, because it does heavy stuff under hood. My first idea was actually instance creator, but I thought it is a hack. I guess there is no other way, right?

Marcono1234 commented 3 years ago

Depending on how complex the object structures you are deserializing are, creation of new Gson objects would indeed not be cheap (since you could not take advantage of its type adapter cache). What you could try is using a static Gson instance and then having an InstanceCreator subclass which stores the instance in a ThreadLocal field. Then before you deserialize your object you would access that instance creator and change its ThreadLocal value. This is somewhat hacky, but it would probably work. However, you need to be careful when using recursion to prevent accidentially replacing the ThreadLocal value in a recurisve call and then later in the original call assume that it still has its old value.

Could you share a little bit more information about your usecase? Then it might be easier to give more specific answers.

xxDark commented 3 years ago

I have a websocket connection between client and server (Netty used), so I need to decode a lot of messages fast. Perhaps I could create Gson instance for each event loop, not for each connection or packet.

Frontear commented 1 year ago

I have a similar use case. I have a Map of strong references to objects in the code that are intended to be configurable via Gson.

When saving them, I turn them into JsonObjects and push them to one large JsonArray, then write that array to a file.

When loading them, I parse the file into a JsonArray, then parse each element as an Object and try to match load (Object key = Map key.toString()).

Although I tried the InstanceCreator tip above, it doesn't seem to actually "manipulate" the object. I need data from the config written into the object, but it doesn't seem to.

Frontear commented 1 year ago
    private static final Map<Class<? extends AbstractTweak>, AbstractTweak> tweaks = new HashMap<>(); // TODO: memory implications with high object counts
    private static final Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().serializeNulls().setPrettyPrinting().registerTypeAdapter(AbstractTweak.class, (InstanceCreator<AbstractTweak>) tweaks::get).create();
    private static final Path config = FabricLoader.getInstance().getConfigDir().resolve("infinity.json");

    static {
        tweaks.put(Tweak1.class, new Tweak1());
        tweaks.put(Tweak2.class, new Tweak2());

        // loading the config, please dont mind this awful mess
        tweaks.forEach((k, v) -> {
            jsonArray.forEach(x -> {
                if (x.getAsJsonObject().get(k.getSimpleName()) != null) {
                    gson.fromJson(x, k); // expected this to modify the item in-place
                }
            });
        });
    }

unless im misunderstanding what i should be doing, this doesn't work. instead, what it seems to do is just return that object that I defined at the type adapter. How would I tell json to manipulate that object once its gotten it? The fields aren't final or anything like that either, just private.

Marcono1234 commented 1 year ago

@Frontear, where is the jsonArray variable in your code coming from? Also, with the code you have shown it looks like it would be easier to just deserialize the Tweak1 and Tweak2 objects as usual with Gson and put them in the tweaks map afterwards instead of trying to do any modifications on existing objects.

This whole workaround of using InstanceCreator to modify existing objects is quite brittle and breaks thread-safety of Gson. For example with your code accessing tweaks or gson outside the static block is most likely not thread-safe unless you add additional synchronization.

Frontear commented 1 year ago

I actually rewrote my logic, but to add explanation: jsonArray came from Gson.fromJson(file_reader, JsonArray.class).

Instead of what I was doing above, I opted to first set the map with null objects, let Gson instantiate and set them first, and if Gson couldn't for whatever reason, it would instantiate the null values only later on.