Jackson deserialization using JsonParser distinguish between the direct object and objects within array

I am using the Jackson for the Deseilization of the JSON. The Deseilization works perfectly for a JSON with CustomerDocument. However, I have a new requirement in which I need to find whether provided JSON has CustomerDocument or just Customer.

I am able to develop the logic for both but the problem is that when I try to merge it won’t work for CustomerDocument. I am looking for a solution that would work for both. All I would like to do is build the logic to differentiate the incoming JSON based on customerDocument and single Customer.

Following is the CustomerDocument JSON:

{
  "isA": "CustomerDocument",
  "customerList": [
    {
      "isA": "Customer",
      "name": "Batman",
      "age": "2008"
    }
  ]
}

Customer.class:

@Data
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "isA")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Customer {
    private String isA;
    private String name;
    private String age;
}

JacksonMain:

public class JacksonMain {
    public static void main(String[] args) throws IOException {
        final InputStream jsonStream = JacksonMain.class.getClassLoader().getResourceAsStream("Customer.json");
        final JsonParser jsonParser = new JsonFactory().createParser(jsonStream);
        final ObjectMapper objectMapper = new ObjectMapper();
        jsonParser.setCodec(objectMapper);
        
        //Goto the start of the document
        jsonParser.nextToken();
        
        //Go until the customerList has been reached
        while (!jsonParser.getText().equals("customerList")) {
            jsonParser.nextToken();
        }
        jsonParser.nextToken();

        //Loop through each object within the customerList and deserilize them
        while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
            final JsonNode customerNode = jsonParser.readValueAsTree();
            final String eventType = customerNode.get("isA").asText();
            Object event = objectMapper.treeToValue(customerNode, Customer.class);
            System.out.println(event.toString());
        }
    }
}

The above code works perfectly and produces the following result:

Customer(isA=Customer, name=Batman, age=2008)

Scenario-2

Now user can provide the direct customer object without the customerDocument. Something like this:

{
  "isA": "Customer",
  "name": "Superman",
  "age": "2013"
}

‘Customer.class’ would remain the same and JacksonMain would be modified to:

public class JacksonMain {
    public static void main(String[] args) throws IOException {
        final InputStream jsonStream = JacksonMain.class.getClassLoader().getResourceAsStream("Customer.json");
        final JsonParser jsonParser = new JsonFactory().createParser(jsonStream);
        final ObjectMapper objectMapper = new ObjectMapper();
        jsonParser.setCodec(objectMapper);

        //Goto the start of the document
        jsonParser.nextToken();


        final JsonNode jsonNode = jsonParser.readValueAsTree();
        final String inputType = jsonNode.get("isA").asText();

        if (inputType.equalsIgnoreCase("Customer")) {
            Object singleCustomer = objectMapper.treeToValue(jsonNode, Customer.class);
            System.out.println(singleCustomer.toString());
        } else if (inputType.equalsIgnoreCase("CustomerDocument")) {
            //Go until the customerList has been reached
            while (!jsonParser.getText().equals("customerList")) {
                jsonParser.nextToken();
            }
            jsonParser.nextToken();

            //Loop through each object within the customerList and deserilize them
            while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
                final JsonNode customerNode = jsonParser.readValueAsTree();
                final String eventType = customerNode.get("isA").asText();
                Object event = objectMapper.treeToValue(customerNode, Customer.class);
                System.out.println(event.toString());
            }
        }
    }
}

For a single CUstomer this would produce the following result:

Customer(isA=Customer, name=Superman, age=2013)

For the same code now if I provide the CustomerDocument (the first JSON) then it would not work and fail with error:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because the return value of "com.fasterxml.jackson.core.JsonParser.getText()" is null
    at stackover.JacksonMain.main(JacksonMain.java:32)

I know this issue is happening because of the line

final JsonNode jsonNode = jsonParser.readValueAsTree();

Can someone please explain how to make the code work for both the type of JSON customerDocument and just single Customer using Jackson? I just want to differentiate whether incoming JSON is customerDocument or single Customer. Any help would be really appreciated.

  1. I want to use Jackson to make the differentiation between both the input.
  2. It would be great if there is no need to create any additional classes. However, it’s fine if there is a need to create an interface to achieve this.
  3. My CustomerList can be very huge so I am reading one by one so it does not make much memory. hence I do not have the CustomerDocument class with List<Customer> rather I am looking over it and mapping one by one.

Answer

Well you can use Jackson sub type to de-serialize between Customer and CustomerDocument.

Something like following,

public class Main {

    public static void main(String[] args) throws IOException {

        String s = "{"isA":"CustomerDocument","customerList":[{"isA":"Customer","name":"Batman","age":"2008"}]}";
//        String s = "{"isA":"Customer","name":"Superman","age":"2013"}";

        ObjectMapper om = new ObjectMapper();
        BaseResponse baseResponse = om.readValue(s, BaseResponse.class);

        if (baseResponse instanceof CustomerDocument) {
            CustomerDocument cd = (CustomerDocument) baseResponse;
            System.out.println("Inside If..");
            cd.getCustomerList().forEach(System.out::println);
        } else if (baseResponse instanceof Customer) {
            System.out.println("Inside Else If..");
            Customer cs = (Customer) baseResponse;
            System.out.println(cs);;
        }
    }
}


@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "isA")
@JsonSubTypes({
        @JsonSubTypes.Type(value = Customer.class, name = "Customer"),
        @JsonSubTypes.Type(value = CustomerDocument.class, name = "CustomerDocument")})
interface BaseResponse {}


@Getter
@Setter
@ToString
class Customer implements BaseResponse{
    private String isA;
    private String name;
    private String age;
}

@Getter
@Setter
@ToString
class CustomerDocument implements BaseResponse{
    private String isA;
    private List<Customer> customerList;
}

PS – Uncomment the string in main method to illustrate the other case.

Update

public class Main {

    public static void main(String[] args) throws IOException {

        String s = "{"isA":"CustomerDocument","customerList":[{"isA":"Customer","name":"Batman","age":"2008"},{"isA":"Customer B","name":"Superman","age":"2013"}]}";
//        String s = "{"isA":"Customer","name":"Superman","age":"2013"}";

        ObjectMapper om = new ObjectMapper();
        JsonNode node = om.readTree(s);
        String type = node.get("isA").asText();

        if (type.equals("Customer")) {
            Customer c = om.readValue(s, Customer.class);
            System.out.println(c);
        } else if (type.equals("CustomerDocument")) {
            JsonNode nextNode = node.path("customerList");
            List<Customer> cl = om.convertValue(nextNode, new TypeReference<List<Customer>>() {});
            cl.forEach(System.out::println);
        }
    }
}

@Getter
@Setter
@ToString
class Customer {
    private String isA;
    private String name;
    private String age;
}