I am trying to deserialize an JSON object like
{
"name":"aaa",
"children": [
{"name":"bbb"}
]
}
Into Java objects where the child has a reference to the parent object, e.g.:
public class Parent {
public String name;
public List<Child> children;
}
public class Child {
public String name;
public Parent parent;
}
// ...
new ObjectMapper().readValue(<JSON>, Parent.class);
When deserializing it like this, Child#parent
will not point back to the parent object.
I read about two approaches while doing my online research, but non seems to work.
1. Adding a constructor arg to the Child
class to set the parent object
public class Child {
public String name;
public Parent parent;
public Child(Parent parent) {
this.parent = parent;
}
}
When doing this I get the error:
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `Child` (no Creators, like default construct, exist):
cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"name":"aaa","children":[{"name":"bbb"}]}"; line: 1, column: 27]
(through reference chain: Parent["children"]->java.util.ArrayList[0])
2. Using the @JsonBackReference
and @JsonManagedReference
annotations
public class Parent {
public String name;
@JsonBackReference
public List<Child> children;
}
public class Child {
public String name;
@JsonManagedReference
public Parent parent;
}
This fails with:
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot handle managed/back reference 'defaultReference':
back reference type (java.util.List) not compatible with managed type (Child)
at [Source: (String)"{"name":"aaa","children":[{"name":"bbb"}]}"; line: 1, column: 1]
The JavaDoc of @JsonBackReference
says it cannot be applied to collections so it obviously does not work, but I wonder why there are so many examples online where it is applied to a collection.
Question How can I achieve that a child object get's its parent/owner object automatically set when the object graph is deserialized. I actually would prefer to somehow get the first approach working somehow, as it does not pollute require to pollute the classes with framework specific annotations.
I took some more time to dig into the Jackson source code I came up with a generalized solution using a custom BeanDeserializer
. The custom deserializer checks if the corresponding Java class for the JSON node to be serialized has a single-argument constructor that can take the parent object and uses it to instantiated the object.
import org.apache.commons.lang3.reflect.ConstructorUtils;
public static class CustomBeanDeserializer extends BeanDeserializer {
private static final long serialVersionUID = 1L;
public CustomBeanDeserializer(BeanDeserializerBase src) {
super(src);
}
@Override
protected Object deserializeFromObjectUsingNonDefault(JsonParser p, DeserializationContext ctxt) throws IOException {
Object parentObject = getParentObject(p);
if (parentObject != null) {
// determine constructor that takes parent object
Constructor<?> ctor = ConstructorUtils.getMatchingAccessibleConstructor(_beanType.getRawClass(), parentObject.getClass());
if (ctor != null) {
try {
// instantiate object
Object bean = ctor.newInstance(parentObject);
p.setCurrentValue(bean);
// deserialize fields
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
String propName = p.getCurrentName();
do {
p.nextToken();
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop == null) {
handleUnknownVanilla(p, ctxt, bean, propName);
continue;
}
try {
prop.deserializeAndSet(p, ctxt, bean);
} catch (final Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
} while ((propName = p.nextFieldName()) != null);
}
return bean;
} catch (ReflectiveOperationException ex) {
ex.printStackTrace();
}
}
}
return super.deserializeFromObjectUsingNonDefault(p, ctxt);
}
private Object getParentObject(JsonParser p) {
JsonStreamContext parentCtx = p.getParsingContext().getParent();
if (parentCtx == null)
return null;
Object parentObject = parentCtx.getCurrentValue();
if (parentObject == null)
return null;
if (parentObject instanceof Collection || parentObject instanceof Map || parentObject.getClass().isArray()) {
parentCtx = parentCtx.getParent();
if (parentCtx != null) {
parentObject = parentCtx.getCurrentValue();
}
}
return parentObject;
}
}
The deserializer can be used like this:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule myModule = new SimpleModule();
myModule.setDeserializerModifier(new BeanDeserializerModifier() {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig cfg, BeanDescription beanDescr, JsonDeserializer<?> deserializer) {
if (deserializer instanceof BeanDeserializerBase)
return new CustomBeanDeserializer((BeanDeserializerBase) deserializer);
return deserializer;
}
});
objectMapper.registerModule(myModule);
objectMapper.readValue(<JSON>, Parent.class);
I am still interested in a better solution requiring less custom code.