I have a class BaseClass
that during runtime will store a reference to another object Thing
. BaseClass
is meant to be extensible so I've made it abstract like so:
abstract class BaseClass{
// see explanation for annotations below
@JsonIgnore private final Thing storedThing;
@JsonProperty private final String thingId;
protected BaseClass(){} // empty constructor
protected BaseClass(Thing thing){
// store thing
}
// other stuff
}
Thing
is polymorphic and has a LOT of data. So for serialization/deserialization, I'm looking to only store the id of Thing
instead of the whole object (this is a hard requirement). So if we have a class SomeClass
implementing BaseClass
, its json might look like so:
// SomeClass extends BaseClass
"SomeClass":{
"thingId" : "ahbjhadbv" // target id of stored Thing. All other info is ignored
}
The intent being that during deserialization I can look up the actual Thing
instance using the id (Thing
is stored elsewhere and can be queried) and put that back in.
Implementors of BaseClass
are also free to add any state data to their implementations should they want. E.g. SomeOtherClass
which implements BaseClass
could look like so:
// SomeOtherClassextends BaseClass
"SomeOtherClass":{
"thingId" : "asdawetgvd" // target id of stored Thing. All other info is ignored
"someOtherData": {...}
"evenMoreData" : {...}
}
Implementing serialization to suit my needs was simple enough with Jackson.
Now the annoying/interesting bit - for reasons I can't go into, I am hamstrung with these constraints:
@JsonTypeInfo
/ @JsonSubTypes
annotations.Thing
and ThingId
on BaseClass
.BaseClass
will implement the multi-argument constructorBaseClass
will all implement an empty constructor.@JsonProperty()
and define a different nameI do, however, have assurances that all derived classes will implement at least one of the two provided constructors in BaseClass
. I will also have knowledge of the class type that extends BaseClass
. Finally, I am also assured that objects will be serialized and deserialized within the same runtime (so if I store any params during serialization, they can be available for use during deserialization)
Knowing this, I decided to create my own custom deserializer - a part of which is shown below:
// Clazz is known beforehand
<T extends BaseClass> T deserialize(Class<T> clazz, JsonNode serializedData){
// implementors are guaranteed to use one of the two available constructors from the base class.
boolean hasEmptyConstructor = Arrays.stream(clazz.getDeclaredConstructors()).anyMatch(x->x.getParameterCount() == 0)
T instance;
if(hasEmptyConstructor){
instance = clazz.getConstructor().newInstance();
} else {
Thing dummyThing = getDummyThing(); // empty thing object needed only for deserialzation
instance = clazz.getConstructor(Thing.class).newInstance(dummyThing);
}
// populate instance with other state data from serializedData
}
And now the question - given that the class T
can have state data fields other than thingId
, how can I reliably populate the instantiated object's fields with said data using jackson ?
Since the state data might have field names different from what's stored in the serializedData
input (due to implementors using @JsonProperty()
and defining a different name)
I cannot directly use reflection to get field names. I will need to use jackson somehow, but I haven't been able to figure out how. Most available methods seem to require a type instead of an instantiated object, and I can't find any way to inject said object into the deserializer.
Edit: The best solution for my case was to use mapper.readerForUpdating(object).readValue(json);
as described in this post: Deserialize JSON into existing object (Java).
Full credits to Olivier in comments below
For my use case, the best solution for my case was to use mapper.readerForUpdating(object).readValue(json);
as described in this post: Deserialize JSON into existing object (Java).
Full credits to @Olivier in comments