Search code examples
javajsonjackson-databind

Deserializing JSON object bi-directional one-to-many with Jackson ObjectMapper


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.


Solution

  • 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.