Reputation: 45
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
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.
GsonUtils.getGson()
- creates Gson
object with adapters.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.
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:
Gson
objects: one for serialisation and one for deserialisation. Only for deserialisation process we register PostConstructAdapterFactory
.@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