johndoe
johndoe

Reputation: 45

Deserializing polymorphic JSON with transient variables using Gson-extras

I have some polymorphic java classes which I am serializing/deserializing. I'm using RuntimeTypeAdapterFactory from Gson-extras to ensure the classes serialize and deserialize correctly with "type" field set to the name of the derived class. It's working fine.

I have some other classes which I am serializing/deserializing which have some variables with the transient modifier applied. I am using PostConstructAdapterFactory, also from Gson-extras, to add a PostConstruct method to my classes so the values of the transient variables can be re-assigned after deserialization has completed and before any of the functions in the classes are called. This is also working fine.

The challenge I have is that I have another group of classes which are polymorphic and also have transient variables where I'd like to have a PostConstruct method execute after deserialization. All of my attempts to use both the RuntimeTypeAdapterFactory and PostConstructAdapterFactory together have failed. I can register both but when I do the PostConstructAdapterFactory behaviour seems to overwrite RuntimeTypeAdapterFactory behaviour resulting in my classes no longer storing the "type" field needed for the polymorphic classes.

It seems I can have one or the other but not both. I even looked at writing a hybrid AdapterFactory class with the capabilities of both Adapter Factories but that was unsuccessful too.

I feel I might be missing something obvious here about the way these are designed. Surely it's possible to get both pieces of functionality working together for my classes? Below is a simple example to demonstrate what I am saying.

public class BaseClass {
    private static final RuntimeTypeAdapterFactory<BaseClass> ADAPTER = RuntimeTypeAdapterFactory.of(BaseClass.class);
    private static final HashSet<Class<?>> REGISTERED_CLASSES= new HashSet<Class<?>>();
    static { GsonUtils.registerType(ADAPTER); }

    private synchronized void registerClass() {
        if (!REGISTERED_CLASSES.contains(this.getClass())) {
            REGISTERED_CLASSES.add(this.getClass());
            ADAPTER.registerSubtype(this.getClass());
        }
    }

    public BaseClass() {
        registerClass();
    }
}

public class DerivedClass extends BaseClass {
   public DerivedClass(Integer number) {
       super();
       this.number = number;
       Init();
   }

   protected Integer number;
   protected transient Integer doubled;
   protected transient Integer tripled;
   protected transient Integer halved;
   protected transient Integer squared;
   protected transient Integer cubed;

   public void Init() {
       halved = number / 2;
       doubled = number * 2;
       tripled = number * 3;
       squared = number * number;
       cubed = number * number * number;
   }

   @PostConstruct
   private void postConstruct() {
       Init();
   }

}

public class GsonUtils {
    private static final GsonBuilder gsonBuilder = new GsonBuilder().registerTypeAdapterFactory(new PostConstructAdapterFactory());

    public static void registerType(RuntimeTypeAdapterFactory<?> adapter) {
        gsonBuilder.registerTypeAdapterFactory(adapter);
    }

    public static Gson getGson() {
        return gsonBuilder.create();
    }    
}

Gson gson = GsonUtils.getGson(); 
String serialized = gson.toJson(new DerivedClass(6), BaseClass.class);
DerivedClass dc = gson.fromJson(serialized, DerivedClass.class);

Update: Thank you for the answer @michał-ziober. Doing as you said did indeed work. It does answer the question however, it still does not exactly solve my problem. Let me modify the question slightly.

The simplified version of my code was perhaps a little too simple. When I attempt to serialize/deserialize DerivedClass as a property on another class, my original problem is highlighted. As below:

public class WrapperClass {
    protected BaseClass dc = new DerivedClass(6);
}

Gson gson = GsonUtils.getGson(); 
String serialized = gson.toJson(new WrapperClass());
WrapperClass wc = gson.fromJson(serialized, WrapperClass.class);

In this scenario, as long as my DerivedClass doesn't define the PostConstruct method, the serialization/deserialization of DerivedClass works fine. But if it does, the "type" property is not written to file.

Just to reiterate, if I serialize/deserialize DerivedClass directly, everything is fine but if it's inside WrapperClass and I serialize/deserialize WrapperClass AND if I define a PostConstruct method, it does not work.

Upvotes: 3

Views: 789

Answers (1)

Michał Ziober
Michał Ziober

Reputation: 38645

Everything is fine with type adapters and you can use them both at the same time. In your case, problem is in order in which code is executed.

  1. GsonUtils.getGson() - creates Gson object with adapters.
  2. gson.toJson(new DerivedClass(6), BaseClass.class) - you create DerivedClass instance which executes super constructor from BaseClass which register given class in adapter (sic!).

Try this code:

DerivedClass src = new DerivedClass(6);
Gson gson1 = GsonUtils.getGson();
String serialized = gson1.toJson(src, BaseClass.class);
System.out.println(serialized);
DerivedClass dc = gson1.fromJson(serialized, DerivedClass.class);

It will work as expected. Creating DerivedClass has a side effect - this is a really good example why classes should be decoupled from each other. You should not have any Gson specific class in BaseClass.

In the best world BaseClass should contain only common properties and some base logic:

class BaseClass {
    // getters, setters, business logic.
}

All Gson configuration should be placed in GsonUtils class:

class GsonUtils {

    public static Gson getGson() {
        return new GsonBuilder()
                .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(BaseClass.class)
                        .registerSubtype(DerivedClass.class))
                .registerTypeAdapterFactory(new PostConstructAdapterFactory())
                .create();
    }
}

If you do not want to specify all classes manually, you need to scan environment in runtime. Take a look at: At runtime, find all classes in a Java application that extend a base class.

Edit after question was updated

It looks like TypeAdapterFactory chaining is not so straightforward as I would expect. It depends from given TypeAdapterFactory implementation whether it returns null or returns new object with some delegation to other factories.

I found a workaround but it could be not easy to use in real project:

  1. I created two Gson objects: one for serialisation and one for deserialisation. Only for deserialisation process we register PostConstructAdapterFactory.
  2. We add a method with @PostConstruct annotation to BaseClass.

Example:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.typeadapters.PostConstructAdapterFactory;
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;

import javax.annotation.PostConstruct;

public class GsonApp {

    public static void main(String[] args) {
        RuntimeTypeAdapterFactory<BaseClass> typeFactory = RuntimeTypeAdapterFactory.of(BaseClass.class)
                .registerSubtype(DerivedClass.class);

        Gson serializer = new GsonBuilder()
                .registerTypeAdapterFactory(typeFactory)
                .create();

        Gson deserializer = new GsonBuilder()
                .registerTypeAdapterFactory(typeFactory)
                .registerTypeAdapterFactory(new PostConstructAdapterFactory())
                .create();

        WrapperClass wrapper = new WrapperClass();
        wrapper.setDc(new DerivedClass(8));

        String json = serializer.toJson(wrapper);
        System.out.println(json);
        System.out.println(deserializer.fromJson(json, WrapperClass.class));
    }

}

class WrapperClass {
    protected BaseClass dc;

    public BaseClass getDc() {
        return dc;
    }

    public void setDc(BaseClass dc) {
        this.dc = dc;
    }

    @Override
    public String toString() {
        return "WrapperClass{" +
                "dc=" + dc +
                '}';
    }
}

class BaseClass {

    @PostConstruct
    protected void postConstruct() {
    }
}

class DerivedClass extends BaseClass {
    public DerivedClass(Integer number) {
        super();
        this.number = number;
        Init();
    }

    protected Integer number;
    protected transient Integer doubled;
    protected transient Integer tripled;
    protected transient Integer halved;
    protected transient Integer squared;
    protected transient Integer cubed;

    public void Init() {
        halved = number / 2;
        doubled = number * 2;
        tripled = number * 3;
        squared = number * number;
        cubed = number * number * number;
    }

    @PostConstruct
    protected void postConstruct() {
        Init();
    }

    @Override
    public String toString() {
        return "DerivedClass{" +
                "number=" + number +
                ", doubled=" + doubled +
                ", tripled=" + tripled +
                ", halved=" + halved +
                ", squared=" + squared +
                ", cubed=" + cubed +
                "} ";
    }
}

Above code prints:

{"dc":{"type":"DerivedClass","number":8}}
WrapperClass{dc=DerivedClass{number=8, doubled=16, tripled=24, halved=4, squared=64, cubed=512} }

Upvotes: 1

Related Questions