Search code examples
javajsonjacksondeserializationjson-deserialization

Deserializing a complex Json in Java with Map Data Type


I have the following JSON object that is parsed through a HTTP request.

{
 "firstName": "User",
 "lastName": "Test",
 "emailId": "[email protected]",
 "formsAndQuestions": {
   "Form1": {
      "Question1": {
        "value": "NEVER",
       "isBoolean": false
      },
      "Question2": {
        "value": "YES"
      }
    },
    "Form2": {
      "Question1": {
        "value": "OTHER"
      }
    }
  }
}

The form object can be dynamic. Therefore I'm deserializing this in my Java class with the following variable.

Map<String, Map<String, Map<String, String>>> forms;

Then I'm going through the complex loop below to iterate these and read the answer values.

for (Map.Entry<String, Map<String, Map<String, String>>> entry : input.formsAndQuestions().get().entrySet()) {
        logger.log("Form: " + entry.getKey());
        for (Map.Entry<String, Map<String, String>> entry1 : entry.getValue().entrySet()) {
            logger.log("Question: " + entry1.getKey());
            for (Map.Entry<String, String> entry2 : entry1.getValue().entrySet()) {
                logger.log("key: " + entry2.getKey());
                logger.log("value: " + entry2.getValue());
            }
        }
    }

Is there a better way to do this? Loop seems pretty complex. I don't want to create a Java object class for deserializing the json as the "formsAndQuestions" object can have multiple forms added/removed with multiple questions/answers per form added or removed.

Would greatly appreciate any feedback on this approach.


Solution

  • Even so forms and questions vary, the structure is the same. You can easily model this in Java using @JsonAnySetter annotation to set properly random forms. Take a look on below example:

    import com.fasterxml.jackson.annotation.JsonAnySetter;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.ToString;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    import java.util.Map;
    
    public class FormsAndQuestionsApp {
        public static void main(String[] args) throws IOException {
            File jsonFile = new File("./resource/test.json").getAbsoluteFile();
    
            ObjectMapper mapper = new ObjectMapper();
    
            Result result = mapper.readValue(jsonFile, Result.class);
            result.getFormsAndQuestions().getForms().forEach(System.out::println);
        }
    }
    
    @Data
    @ToString
    class Result {
    
        private String firstName;
        private String lastName;
        private String emailId;
        private FormsAndQuestions formsAndQuestions;
    }
    
    @Data
    @ToString
    class FormsAndQuestions {
        private List<Form> forms = new ArrayList<>();
    
        @JsonAnySetter
        public void anySetter(String formName, Map<String, Question> questions) {
            // update questions with keys (question's name)
            questions.forEach((k, v) -> v.setQuestion(k));
    
            forms.add(new Form(formName, questions.values()));
        }
    }
    
    @Data
    @ToString
    @AllArgsConstructor
    class Form {
    
        private String name;
        private Collection<Question> questions;
    }
    
    @Data
    @ToString
    class Question {
        private String question;
        private String value;
        private Boolean isBoolean;
    }
    

    Above code prints:

    Form(name=Form1, questions=[Question(question=Question1, value=NEVER, isBoolean=false), Question(question=Question2, value=YES, isBoolean=null)])
    Form(name=Form2, questions=[Question(question=Question1, value=OTHER, isBoolean=null)])
    

    The key point is public void anySetter(String formName, Map<String, Question> questions) method. Using @JsonAnySetter we convert JSON Object to a list, because for every unknown key-value pair this method will be invoked. Also, we convert inner object to proper Question POJO. value field is set by Jackson we need to set question property only. Right now, it should be much easier to traverse this list and use later in some business logic.