javajsonjacksondeserialization

Jackson Custom Deserialization - deserialize into known object instance WITHOUT JsonTypeInfo annotations or Mixins


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:

I 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


Solution

  • 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