1

I am using Jackson to deserialize a JSON. The Jackson has to handle 2 things:

  1. Identify if the provided JSON is a document and if so deserialize the elements in CustomerList one by one.
  2. Identify if the provided JSON is a single Customer and if so then deserialize the customer directly.

I am able to achieve this and everything is working as expected but when I provide the CustomerList document then it's unable to read the @Context key-value pair.

Following is the JSON i am trying to deserialize:

{
  "@context": [
    "https://stackoverflow.com",
    {
      "example": "https://example.com"
    }
  ],
  "isA": "CustomerDocument",
  "customerList": [
    {
      "isA": "Customer",
      "name": "Batman",
      "age": "2008"
    }
  ]
}

Following is my Customer POJO class:

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


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

Following is the Main:

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();

        try {
            BaseResponse baseResponse = objectMapper.readValue(jsonParser, BaseResponse.class);
            System.out.println("SINGLE EVENT INPUT" + baseResponse.toString());
        } catch (Exception e) {
            System.out.println("LIST OF CUSTOMER INPUT");
            //Go until the customerList has been reached
            while (!jsonParser.getText().equals("customerList")) {
                System.out.println("Current Token Name : " + jsonParser.getCurrentName());
                if (jsonParser.getCurrentName() != null && jsonParser.getCurrentName().equalsIgnoreCase("@context")) {
                    System.out.println("WITHIN CONTEXT");
                }
                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, BaseResponse.class);
                System.out.println(event.toString());
            }
        }
    }
}

When I run the application I get the following response:

LIST OF CUSTOMER INPUT
Current Token Name : isA
Customer(isA=Customer, name=Batman, age=2008)

As we can see it's printing only Current Token Name: isA I would expect it to print isA and @Context because it's present before the isA.

Now if I remove the following code I have on the interface BaseResponse:

@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "isA")

Then I am able to read the @context but that would result in following error:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `stackover.BaseResponse` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

I tried various things such as adding the following line:

@JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@context")

But nothing seems to work as expected for me. Can someone please suggest some work-around on how to make it work?

I tried to create a copy of the Jackson JsonParser but even that does not seem to work for me.

Please Note:

  1. The CustomerList can have a lot of Customers hence I do not want to store the whole CustomerList into some List as it can take a lot of memories. Hence, I am using JsonParser so I can read one JsonToken at a time.

  2. Also, I do not want to create a CustomerList class rather than that I want to read one Customer at a time and deserialize it.

BATMAN_2008
  • 2,788
  • 3
  • 31
  • 98
  • I can see in your `@context` array one element is a string and other one is a object? Is it deliberate? – ray Sep 03 '21 at 12:34
  • @ray Thanks a lot for the response. Yes, it's like that only. The JSON is actually coming from another application so I cannot modify it. I am running into an error when I am trying to differentiate between the `CustomerDocument` and `Customer`. Is there anything that can be done? – BATMAN_2008 Sep 03 '21 at 12:38

1 Answers1

1

If I'm not mistaken you have only need Customer data. In my previous answer in here (Not the updated one) I thought you want de-serialize into two different classes. That's why I have added BaseResponse, Customer and CustomerDocument. Since that's not the case you don't want it anymore.

I would suggest you to create a Jackson JsonNode from the JSON response you receive. So you can decide how to travers the tree. If you do that you no longer need BaseResponse or any of these Jackson related annotations.

Your Customer class,

class Customer {
    private String isA;
    private String name;
    private String age;
    
    // Getters & Setters
}

You can use this approach to de-serialize.

Element @context cannot de-serialize directly. Because array not hold same type of elements within (one element a string other is an object).

String s = "{\"@context\":[\"https://stackoverflow.com\",{\"example\":\"https://example.com\"}],\"isA\":\"CustomerDocument\",\"customerList\":[{\"isA\":\"Customer\",\"name\":\"Batman\",\"age\":\"2008\"}]}";
//        String s = "{\"isA\":\"Customer\",\"name\":\"Superman\",\"age\":\"2013\"}";

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

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

    if (contextNode instanceof ArrayNode) {
        ArrayNode contextNodeArray = (ArrayNode) contextNode;
        for (JsonNode node : contextNodeArray) {
            if (node instanceof TextNode) {
                System.out.println(node.asText());
            } else if (node instanceof ObjectNode) {
                System.out.println(node.path("example").asText());
            }
        }
    }
}

Output,

Customer(isA=Customer, name=Batman, age=2008)
https://stackoverflow.com
https://example.com

Update

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

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

    JsonNode contextNode = tree.path("@context");
    if (contextNode instanceof ArrayNode) {
        ArrayNode contextNodeArray = (ArrayNode) contextNode;
        for (JsonNode node : contextNodeArray) {
            if (node instanceof TextNode) {
                System.out.println(node.asText());
            } else if (node instanceof ObjectNode) {
                System.out.println(node.path("example").asText());
            }
        }
    }
}
ray
  • 1,512
  • 4
  • 11
  • Thanks a lot for your response. I am able to follow up on what you are doing. The problem is that `CustomerList` can have a huge amount of customers so I do not want to load the whole thing into the memory. Hence, I am using the Jackson Streaming API so I can read the `CustomerList` but have only one customer at a time in memory. The approach suggested above loads the entier `CustomerList` elements into `List` which I am trying to omit actually. That's also the reason I am running into the issue mentioned in the question. – BATMAN_2008 Sep 03 '21 at 14:02
  • Is there anything that I can try which will avoid loading of entier `CustomerList` and have only one `Customer` at a time. Also, work for identifying the JSON if its `CustomerDocument` or `Single Customer`? Looking forward to your response and suggestions. Thanks a lot for the response. – BATMAN_2008 Sep 03 '21 at 14:04
  • You can loop through `customerList` Node as well – ray Sep 03 '21 at 14:09
  • Yes that can be done but the problem is that the whole `CustomerList` has been loaded into the memory. I really want to avoid that. In the code mentioned in the question, I am not loading the whole `CustomerList` I am parsing over the JSON and trying to reach `Customer one-by-one`. This will ensure that only one customer is present in memory and provide better performance. – BATMAN_2008 Sep 03 '21 at 14:11
  • I think you miss understood my answer. Check my update. I meant something like this – ray Sep 03 '21 at 14:23
  • Thanks a lot for your answers and for helping me out with the issue. Maybe I lack the understanding but going by the `updated answer route` don't we store the whole `CustomerList` in memory using `ArrayNode customerArrayNode`? After storing we are looping over it right so won't it impact the performance and memory? Because before I was using the `Jackson JsonParser` which was parsing one token at a time and then I was using the `jsonParser.readValueAsTree()` to read the whole `customer object` one at a time. So I was guranteed to have only one Customer at a time. – BATMAN_2008 Sep 03 '21 at 15:51
  • In the updated code first, we are storing the whole `CustomerList` in `ArrayNode customerArrayNode` so we will have the complete `CustomerList` in memory and then we are traversing over it to read one by one. I am no expert in Java or Jackson but won't it have a memory issue if 10000s customers are provided in the `CustomerList`? I am just trying to make my understanding better. Thanks in advance for your response and clarification. – BATMAN_2008 Sep 03 '21 at 15:53
  • True `JsonParser` uses less memory because it reads the JSON object as bunch of tokens and one by one. In your case I don't think you can use `JsonParser` because structure of your JSON object changes when `isA` parameter change. If the order has changed in your JSON object, you won't be able to de-serialize it correctly. For example you will iterate until you get `isA` parameter, when you get that parameter, you cannot go back to get `@context` data. – ray Sep 03 '21 at 17:23
  • And what happened if you received `isA` as the last parameter in your JSON object. – ray Sep 03 '21 at 17:25
  • As the JSON is coming from another application and it's a standard JSON for my application so there is no way for me to modify it. I can modify the code however I want but I want to make it as efficient as possible. Previously I did not have the single `Customer` requirement so it was easy for me to parse till `CustomerList` then read the each Customer using `jsonParser.readValueAsTree()`. Now, because of the single customer, I need to make the decision and due to this, I am losing the `@context`. Is there a way to store the `jsonParser` so if the first condition i.e single Customer fails – BATMAN_2008 Sep 03 '21 at 17:44
  • Then I can pass the copy of the `JsonParser` to the `catch` block so it has the `@context`. Something like that can be done? I tried but it was not working as expected maybe you can once guide me if it's possible to create a copy and use it rather than using the same `jsonParser` in both case – BATMAN_2008 Sep 03 '21 at 17:45
  • Well you can try to parse assuming everything is there. I'm not sure whether we can create a copy of `JsonParser` nr not. But you can create a new `JsonParser` for sure. – ray Sep 03 '21 at 18:04
  • I tried various things and tried to create a copy of `JsonParser` still no luck for me. Nothing seems to work as I expected. The reason I am unable to read the `@context` is that `JsonParser` has been parsed halfway through so it loses it so when it comes to the `catch` block it has already passed the `@context` so I am unable to get it. Is there any other workaround that you can suggest to me? Because my client is saying we should not store the complete `CustomerList` in the memory as it can impact the performance due to huge size. Any help or suggestions would be really appreciated. – BATMAN_2008 Sep 05 '21 at 04:00
  • You can mapped everything in `@context` to an array of simple POJO and then you can reuse that class when you have the `isA` value! – ray Sep 05 '21 at 04:53
  • Thanks for the response but I am a bit unable to follow. Do you mean I can create another class `Context` which will have the `List`? But how do I handle it in both cases i.e when I have the `CustomerList` and single `Customer`? If possible can you please provide me a sample just to make my understanding better? – BATMAN_2008 Sep 05 '21 at 05:23