Search code examples
javajacksonjackson-databind

Jackson Serialization with recursive structure only printing qNodeId and not additional child nodes


I have 3 main Node types as follows: Node, CNode, QNode. The QNode has can have a list of CNodes which can refer to other CNodes but never to the parent QNode. The QNode is a type of Node but with additional fields, so it extends from Node which is the most basic Node. I am having problems serialization this structure into JSON and to Java objects. There is also a TypeInfo class that can have an optional Node reference. TypeInfo is optional in QNode. When I serialize and print the result object I am only able to get the qNodeId of the QNode and its additional properties but not the child node objects. Here is my attempt. I would really appreciate very much any help with this issue as I have been on it for a while with no success

First the sample json input


{
  "qNodes": [
    {

        "qNodeId": "Q-11122",
        "id": "PO11111",
        "typeInfo": {
          "applicationLevel": "medium",
          "required": true
         },
        "cNodes": [
          {
              "cNodeId": "Q-11155",
              "cNodes": [
                {
                  "qNodeId": "Q-7420",
                  "typeInfo": {
                    "applicationLevel": "low",
                    "assetNode": {
                      "id": "WQ-222",
                      "qNode": {
                        "qNodeId": "Q-0988"
                      }
                    },
                    "required": true
                  },
                  "url": "https://example.com ",
                  "id": "qS111"
                }
              ]
          }
        ]
      }
  ]
}

Next here is the Node class


    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import lombok.Data;

    @Data 
    public class Node {
      public String id;
      public QNode qNode;

      @JsonCreator
      public Node(@JsonProperty("qNode") QNode qNode, 
   @JsonProperty("id") String id){
         this.id = id;
         this.qNode = qNode;
     }
   }

Here is the QNode class


   import com.fasterxml.jackson.annotation.JsonAnySetter;
   import com.fasterxml.jackson.annotation.JsonCreator;
   import com.fasterxml.jackson.annotation.JsonProperty;
   import lombok.Data;

   import java.util.HashMap;
   import java.util.List;
   import java.util.Map;

   @Data
   public class QNode extends BaseNode{
     @JsonProperty("qNodeId")
     private String qNodeId;
     @JsonProperty("cNodes")
     private List<CNode> cNodes;
     private TypeInfo typeInfo;
     private Map<String, String> additionalElements = new HashMap<>();


    @JsonCreator
    public QNode(@JsonProperty("qNode") String qNode,
                 @JsonProperty("id") String id
                 ) {
        super(id);
        this.qNode = qNode;
    }

    @JsonAnySetter
    public void additionalElements(String property, String value) {
        this.additionalElements.put(property, value);
    }
}

Here is the CNode class


    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonInclude;
    import com.fasterxml.jackson.annotation.JsonProperty;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    public class CNode {
      private String cNodeId;
      private QNode qNode;
      @JsonCreator
      public CNode(@JsonProperty("cNodeId") String cNodeId,@JsonProperty("qNode")QNode 
      qnode){
        this.cNodeId = cNodeId;
        this.qNode = qnode;
    }
}


And finally the TypeInfo class


    public class TypeInfo {
    public enum Level {
        low,
        medium,
        high;
    }
    private Level applicationLevel;
    private Node assetNode;
    private boolean required;
}


Here is BaseNode:

    import com.fasterxml.jackson.annotation.JsonAnySetter;
    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import lombok.NoArgsConstructor;

    import java.util.HashMap;
    import java.util.Map;

    @NoArgsConstructor
    public class BaseNode {
    protected String id;
    protected Map<String, String> additionalProperties = new 
    HashMap<>();

    @JsonCreator
    public BaseNode(@JsonProperty("id") String id) {
        this.id = id;

    }

    @JsonAnySetter
    public void additionAnswerProperties(String property, String 
  value) {
        additionalProperties.put(property, value);
    }

  }

Here is the container where the nodes are collected:


    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import lombok.Data;

    import java.util.List;

    @Data
    public class QNodeContainer {
      List<QNode> qNodes;

      @JsonCreator
      public QNodeContainer(@JsonProperty("qNodes") List<QNode> 
 qNodes){
        this.qNodes = qNodes;
    }
 }


Here is how I read the json into the class:


    File resource2 = new ClassPathResource(
                "data.json").getFile();
        String dataJson = new String(
                Files.readAllBytes(resource2.toPath()));
        ObjectMapper mapper = new 
 ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        QNodeContainer qNodes = mapper.readValue(dataJson, 
               QNodeContainer.class);


Solution

  • I've managed to perform deserialization and subsequent serialization of the JSON you've provided after applying some changes.

    The very first advice: before examining serialization of a complex object graph, have a look at smaller pieces. And when you're certain that all custom types can be serialized/deserialized as expected separately, it's time to check the whole thing.

    These are the issues I found:

    • QNode effectively has two methods annotated with @JsonAnySetter: additionalElements() and additionAnswerProperties() inherited from BaseNode. That's a huge No-No, this annotation is used to handle all the unmapped properties which can be found in the input JSON. There should at most one such annotation, either of these two maps and methods in QNode and BaseNode should be removed. If for some reason you need to separate some of these properties into another map, then they need to be grouped in the JSON-input under a particular well-defined name.

    • You might want to use @JsonAnyGetter annotation to flatten the data contained in the map additionalProperties (I've applied it, pay attention to the comments to spot the changes).

    • JSON representation of CNode is inconsistent with your POJO. There's a JSON-array, whilst the field in the class is type QNode (I've changed it to List<QNode>).

    • Some getters were absent (see the comments).

    That's the edited version:

    @Data
    public class Node {
        public String id;
        public QNode qNode;
        
        @JsonCreator
        public Node(@JsonProperty("qNode") QNode qNode,
                    @JsonProperty("id") String id) {
            this.id = id;
            this.qNode = qNode;
        }
    }
    
    @Data
    public class QNode extends BaseNode {
        @JsonProperty("qNodeId")
        private String qNodeId;
        @JsonProperty("cNodes")
        private List<CNode> cNodes;
        private TypeInfo typeInfo;
    //        private Map<String, String> additionalElements = new HashMap<>(); // see the comment above `additionalElements()`
        
        
        @JsonCreator
        public QNode(@JsonProperty("qNode") String qNode,
                     @JsonProperty("id") String id
        ) {
            super(id);
            this.qNodeId = qNode; // changed from `this.qNode = qNode;` which raised a compilation error
        }
    
    //        @JsonAnySetter // <- conflicts with @JsonAnySetter on `additionAnswerProperties()` inherited from the super class - there should be only one
    //        public void additionalElements(String property, String value) {
    //            this.additionalElements.put(property, value);
    //        }
    }
    
    
    @JsonInclude(JsonInclude.Include.NON_NULL)
    @Getter   // <- added
    @ToString // <- added
    public class CNode {
        private String cNodeId;
        private List<QNode> qNodes; // <- QNode to List<QNodes>
        
        @JsonCreator
        public CNode(@JsonProperty("cNodeId") String cNodeId,
                     @JsonProperty("cNodes") List<QNode> qNodes) { // <- "qNode" to "cNodes" & QNode to List<QNodes>
            this.cNodeId = cNodeId;
            this.qNodes = qNodes;
        }
    }
    
    @ToString // <- added
    @Getter   // <- added
    public class TypeInfo {
        private Level applicationLevel;
        private Node assetNode;
        private boolean required;
        
        public enum Level {
            low,
            medium,
            high;
        }
    }
    
    @NoArgsConstructor
    public class BaseNode {
        @Getter // <- added
        protected String id;
        @JsonAnyGetter // <- added
        protected Map<String, String> additionalProperties = new HashMap<>();
        
        @JsonCreator
        public BaseNode(@JsonProperty("id") String id) {
            this.id = id;
        }
        
        @JsonAnySetter
        public void additionAnswerProperties(String property, String value) {
            additionalProperties.put(property, value);
        }
    }
    
    @Data
    public class QNodeContainer {
        private List<QNode> qNodes;
        
        @JsonCreator
        public QNodeContainer(@JsonProperty("qNodes") List<QNode> qNodes) {
            this.qNodes = qNodes;
        }
    }
    

    A code demonstrating deserialization/serialization of the JSON sample provided in the question:

    String json = """
        past your JSON here
        """;
    
    ObjectMapper mapper = new ObjectMapper()
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            
    QNodeContainer qNodes = mapper.readValue(json, QNodeContainer.class);
            
    System.out.println(qNodes);
            
    System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(qNodes));
    

    Illustration of the discrepancy between the specified JSON representation and your Java-classes:

    enter image description here