Десериализация Jackson JSON с использованием JsonParser пропускает первую пару значений ключа из-за проверки условий

#java #json #jackson #jackson-databind #jackson2

Вопрос:

Я использую Джексона для десериализации JSON. Входной JSON может быть 2 типов CustomerDocument или Single Customer . CustomerDocument будет иметь а CustomerList , которое может состоять из огромного количества Customer , и то Single Customer будет иметь только а Single Customer . Следовательно, Джексон должен справиться с 2 вещами:

  1. Определите , является ли предоставленный JSON a CustomerDocument , если да, то десериализуйте элементы по CustomerList одному, а не сохраняйте все List целиком, чтобы уменьшить использование памяти.
  2. Определите, является ли предоставленный JSON a single Customer , и если да, то десериализуйте customer его напрямую.

Я в состоянии добиться этого, и все работает так, как ожидалось, но когда я предоставляю CustomerDocument , он не может прочитать @Context пару ключ-значение, поскольку она уже была прочитана во время проверки на наличие одного Customer (как вы упомянули в пункте 2). Я думаю, что приведенный ниже код прояснит проблемы:

Ниже приведен JSON, который я пытаюсь десериализовать:

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

Ниже приведен мой класс POJO для клиентов:

 @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 {
}
 

Ниже приведен мой Main класс, который прочитает JSON InputStream и примет решение о том, является ли предоставленный входной JSON CustomerList или Single Customer , а затем десериализуется соответствующим образом:

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

Following is the output I am getting:

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

Как мы видим, это печать, только Current Token Name: isA я ожидал бы, что она будет напечатана isA , и @Context потому что она присутствует перед isA . Но я знаю, что он не печатается, потому что он уже прошел это из-за следующей строки кода в try блоке:

 BaseResponse baseResponse = objectMapper.readValue(jsonParser, BaseResponse.class);
 

Ниже Single Customer приведен JSON (только для справки, и это работает нормально):

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

Может ли кто-нибудь, пожалуйста, подсказать мне, как я могу этого достичь, и есть ли лучшее решение этой проблемы?

Пожалуйста, обратите внимание:

  1. CustomerList Может быть много Customers , поэтому я не хочу хранить все CustomerList в некоторых List , так как это может занять много воспоминаний. Следовательно, я использую JsonParser , чтобы я мог читать по одному JsonToken за раз.
  2. Кроме того, я не хочу создавать CustomerList класс вместо того, чтобы читать его по одному Customer за раз и десериализовывать.
  3. Структура JSON не может быть изменена, так как она исходит из другого приложения, и это стандартный формат для моего приложения.

Ответ №1:

Я исправил код, я удалил блок catch. CUSTOMERS_LIST свойство различается между одним и несколькими клиентами.

JacksonMain.java

    public class JacksonMain {

    private static final String CUSTOMERS_LIST = "customerList";

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

        TreeNode treeNode = objectMapper.readTree(jsonParser);
        if (Objects.isNull(treeNode.get(CUSTOMERS_LIST))) {
            BaseResponse baseResponse = objectMapper.treeToValue(treeNode, BaseResponse.class);
            System.out.println("SINGLE EVENT INPUT: "   baseResponse);
        } else {
            System.out.println("LIST OF CUSTOMER INPUT");
            ArrayNode customerList = (ArrayNode) treeNode.get(CUSTOMERS_LIST);
            for (JsonNode node : customerList) {
                BaseResponse event = objectMapper.treeToValue(node, BaseResponse.class);
                System.out.println(event.toString());
            }
        }
    }
}
 

Я создал два файла json ресурсов

Клиент.json

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

Клиенты.json

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

CustomerDocument.java

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

Customer.java

 @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;
}
 

BaseResponse.java

 @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")})
@JsonIgnoreProperties(ignoreUnknown = true)
interface BaseResponse {
}
 

В интерфейсе я добавил аннотацию JsonIgnoreProperties и добавил еще один пропущенный тип JsonSubType. [Пользовательский документ]

Оба дела сейчас работают.

Тесты производительности:

 Size: 1,065,065 records / customers handled 
Time in seconds: 30.208
 

Комментарии:

1. Спасибо вам за ваш ответ. Я в состоянии понять, как вы этого добиваетесь. Но у меня есть одно сомнение, что ранее я использовал следующую строку кода final JsonNode customerNode = jsonParser.readValueAsTree(); для чтения по одному клиенту за раз из CustomerList . Это гарантировало мне, что у меня всегда будет только по одному Customer в памяти за раз, но в коде, который вы указали, вы храните полный CustomerList код ArrayNode customerList . Разве этот магазин не был бы целым CustomerList ? Их CustomerList может быть огромное количество Customers , поэтому это может повлиять на производительность.

2. Я не эксперт по Java или Джексону, поэтому хочу получить подтверждение этого. Есть ли какой-нибудь способ избежать такого полного хранения CustomerList ? Поскольку это также входит в мои требования, во время выполнения я должен иметь в памяти только по одному Customer за раз. Это требование отчасти противоречит моему другому требованию (разграничение ч/б CustomerDocument и Single Customer ). Пожалуйста, помогите мне в этом и предоставьте некоторые обходные пути или предложения, которые можно использовать для работы с этим кодом в соответствии с требованиями. Заранее большое спасибо и с нетерпением ждем ваших предложений.

3. Сколько записей должно обрабатываться в одном запросе?

4. Спасибо за ваш ответ и за то, что помогли мне с этим вопросом. Если есть CustomerList , то в нем может быть 1000 или 10000s Customer , поэтому я хочу убедиться, что в данный момент в памяти есть только один Customer . Это означает, что я хотел бы читать по одному Customer за раз и переходить к следующему, а не хранить полное CustomerList в памяти, а затем просматривать его. В коде, который я указал в вопросе, я делаю что-то подобное, но проблема в том, что я упускаю информацию, содержащуюся в @context нем, поэтому сейчас пытаюсь достичь и того, и другого. Любые предложения

Ответ №2:

Наконец, я смог заставить его работать, Вместо того, чтобы читать блок @context in catch , я попытался прочитать его до try-catch .

 
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("isA")) {
            System.out.println("Current Token Name : "   jsonParser.getCurrentName());
            if (jsonParser.getCurrentName() != null amp;amp; jsonParser.getCurrentName().equalsIgnoreCase("@context")) {
                System.out.println("WITHIN CONTEXT");
            }
            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");

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