3

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.

johndoe
  • 45
  • 4

1 Answers1

1

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} }
Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • Hi @michał-ziober. Thank you for the answer. I modified everything to work your way and it did work for the simple example. Then I tried my old way and it also worked for the simple example, which was a bit too simple I guess. Going back to your way though, it works, but, if I wrap DerivedClass into another class and serialize it, it does not work. I've modified the question to reflect this. Any ideas? – johndoe Jan 21 '20 at 03:01
  • 1
    @johndoe - I updated question but I could not find any simple solution in your case. – Michał Ziober Jan 22 '20 at 21:28
  • 1
    Wonderful, thank you. Your workaround is completely suitable solution to my problem. It's no inconvenience at all to have a separate serializer and deserializer. In your opinion, is this a bug in gson? I feel it is, due to the way it works as expected for a single object but not when nested in another class. – johndoe Jan 23 '20 at 01:06
  • 1
    @johndoe, probably it is not a bug. It is how inner implementation of `RuntimeTypeAdapterFactory` and `PostConstructAdapterFactory` factories are working together. Method `create` which both of them implementing returns delegates for some type and `null` for others and this is where chaining is broken. I thought that `POJO` is passes between them but this is not actually true. For more info I would have to debug this code and test for all cases but if workaround works there it is no need to do that. – Michał Ziober Jan 23 '20 at 06:46