当与 Jackson ObjectMapper treeToValue 方法一起使用时,Jackson @JsonAnySetter 会忽略重复键的值

问题描述

我正在使用 Jackson 库来反序列化 JSON。在 JSON 中,我有一些自定义字段,它们的值可以是任何值,因此我尝试使用 @JsonAnySetter@JsonAnyGetter获取值。 JSON 中的字段和值可以重复,我想从 JSON 中获取所有内容并将其存储在地图中。但是,如果键中存在重复项,则直接的 Jackson 实现会存储最后一个值。

我有一个包含许多事件的大型 JSON 文件。我正在逐个读取文件,以便整个 JSON 文件不会存储在内存中。读取单个事件后,我检查事件类型,并根据该类型将其分配给不同的 POJO。以下是我的示例 JSON 文件,包含 2 个事件。

[
  {
    "isA": "Type1","name": "Test","foo": "val1","foo": "val2","bar": "val3","foo": {
      "myField": "Value1","myField": "value2"
    }
  },{
    "isA": "Type2","name": "Test1","myField": "value2"
    }
  }
]

以下是用于反序列化的类:(我有许多在反序列化过程中直接映射的字段,并且可以正常工作,因此为简单起见省略了)。这是 type1 事件的类,如果它是 type2

@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Type1
{
    private String name;
    private Map<String,Object> userExtensions;
   
    @JsonAnyGetter
    public Map<String,Object> getUserExtensions() {
        return userExtensions;
    }
    
    @JsonAnySetter
    public void setUserExtensions(String key,Object value) {
        System.out.println("KEY : " + key + " VALUES : " + values);
    }
    
}

从上面可以看出,我有一个 Map 字段,用于使用 JsonAnySetter 填充扩展。我有一个方法,可以为杰克逊无法直接搜索的字段调用

我尝试在 System.out 方法中设置 @JsonAnySetter,我观察到 Jackson 没有在此方法获取重复字段

  @JsonAnySetter
  public void setUserExtensions(String key,Object value) {
    System.out.println(" Key : " + key + " Value : " + value);
  }

我只得到最后一个字段。对于上面提到的 JSON,我只得到最后一个 foo 的值:

"foo" :{
  "myField" : "Value1"
  "myField" : "value2"
}

带有 fooval1 的前 2 个 val2 甚至不会在此方法中打印。

以下是我的 Main 方法,它实际上是罪魁祸首,因为我使用的是不支持重复字段的 Jackson 的 objectMapper.treeTovalue

public class Main
{
  public static void main (String[]args)
  {
    //File where the JSON is stored
    InputStream jsonStream = Main.class.getClassLoader().getResourceAsstream("InputEPCISEvents.json");
    final JsonFactory jsonFactory = new JsonFactory();
    final JsonParser jsonParser = jsonFactory.createParser (jsonStream);
    jsonParser.setCodec (new ObjectMapper ());
    final ObjectMapper objectMapper = new ObjectMapper();
    
    // Loop until the end of the events file
    while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
        // Get the node
        final JsonNode jsonNode = jsonParser.readValueAsTree();
        // Get the eventType
        final String eventType = jsonNode.get("isA").toString().replaceAll("\"","");
        
        switch (eventType) {
            case "Type1":
                final Type1 objInfo = objectMapper.treeTovalue(jsonNode,Type1.class);
                break;
            case "Type2":
                final Type2 objInfo = objectMapper.treeTovalue(jsonNode,Type2.class);
                break;
            default:
                System.out.println("None of the event Matches");
                break;
        }
        
    }
      
  }
}

我想知道如何让 Jackson 读取重复的键值,以便我可以在 @JsonAnySetter 方法中处理它们。

我想知道是否有办法直接使用 Jackson 处理这些场景,或者我需要构建自己的自定义反序列化。如果是,那么我如何为班级中的一个字段构建一个

PS:我尝试了很多在线可用的东西,但都没有奏效,因此发布了相同的内容。如果发现重复,我真的很抱歉。

我注意到 Stack Overflow 上的大部分代码示例都使用 Jackson readValue 方法读取 JSON,但就我而言,我有一个包含许多事件的大型 JSON 文件,因此我将它们逐个拆分并使用Jackson objectMapper.treeTovalue 方法将其映射到我的类。在这个类中,我尝试将每个事件的字段与相应的类字段进行映射,如果没有找到匹配项,那么我希望使用 @JsonAnySetter 方法填充它们。

解决方法

我想我有一个可能对您有用的解决方案,或者至少让您向前迈进了几步。我使用您的文件创建了一个本地测试来演示。

通过查找 FAIL_ON_READING_DUP_TREE_KEY 的实现方式,我找到了一种合理分配的方法。基本上,响应它的代码在 JsonNodeDeserializer 中。如果你看到这个反序列化器如何看待 _handleDuplicateField,它基本上只会用最新的节点(你看到的副作用)替换最新的节点,如果你设置了失败功能,它就会有条件地抛出一个异常。但是,通过使用重写方法创建这个反序列化器类的扩展,我们基本上可以实现“合并欺骗”而不是“看到一个错误就抛出错误”的行为。结果如下。

来自我的测试运行的控制台日志产生了这个,证明“foos”包含所有独立的字符串和对象......

由于它是在 JSON 级别而不是在对象级别进行合并,它似乎甚至无意中通过将多个字符串合并到一个 arrayNode 来处理内部 myField 用例......我什至没有尝试当时为 myField 求解,但是嘿,你也去吧!

我没有处理 Type2,因为我认为这足以让您继续前进。

当然,您必须在测试包中使用上述内容创建文件 jsonmerge/jsonmerge.json

控制台日志结果

Object Node Read with Dupes: {"isA":"Type1","name":"Test","foo":["val1","val2",{"myField":["Value1","value2"]}],"bar":"val3"}
Type 1 mapped after merge : Type1 [isA=Type1,name=Test,bar=val3,foos=[val1,val2,{myField=[Value1,value2]}]]
None of the event Matches
EOF

测试主要代码 - JsonMerger.java

package jsonmerge;

import java.io.InputStream;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
 * Project to resolve https://stackoverflow.com/questions/67413028/jackson-jsonanysetter-ignores-values-of-duplicate-key-when-used-with-jackson-ob
 *
 */
public class JsonMerger {

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

        // File where the JSON is stored
        InputStream jsonStream = JsonMerger.class.getClassLoader()
            .getResourceAsStream("jsonmerge/jsonmerge.json");
        final JsonFactory jsonFactory = new JsonFactory();
        final ObjectMapper objectMapper = new ObjectMapper();

        // Inject a deserializer that can handle/merge duplicate field declarations of whatever object(s) type into an arrayNode with those objects inside it
        SimpleModule module = new SimpleModule();
        module.addDeserializer(JsonNode.class,new JsonNodeDupeFieldHandlingDeserializer());
        objectMapper.registerModule(module);

        final JsonParser jsonParser = jsonFactory.createParser(jsonStream);
        jsonParser.setCodec(objectMapper);

        JsonToken nextToken = jsonParser.nextToken();

        // Loop until the end of the events file
        while (nextToken != JsonToken.END_ARRAY) {
            nextToken = jsonParser.nextToken();

            final JsonNode jsonNode = jsonParser.readValueAsTree();
            if (jsonNode == null || jsonNode.isNull()) {
                System.out.println("EOF");
                break;
            }

            // Get the eventType
            JsonNode getNode = jsonNode.get("isA");
            final String eventType = getNode
                .toString()
                .replaceAll("\"","");

            switch (eventType) {
            case "Type1":
                final Object obj = objectMapper.treeToValue(jsonNode,JsonNodeDupeFieldHandlingDeserializer.class);
                ObjectNode objNode = (ObjectNode) obj;
                System.out.println();
                System.out.println();

                System.out.println("Object Node Read with Dupes: " + objNode);

                Type1 type1 = objectMapper.treeToValue(objNode,Type1.class);
                System.out.println("Type 1 mapped after merge : " + type1);

                break;
            default:
                System.out.println("None of the event Matches");
                break;
            }

        }

    }
}

Type1.java

package jsonmerge;

import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonAnySetter;

public class Type1 {

    public String       isA;
    public String       name;
    public String       bar;
    public List<Object> foos;

    public Type1() {

        foos = new LinkedList<Object>();
    }

    /**
     * Inspired by https://stackoverflow.com/questions/61528937/deserialize-duplicate-keys-to-list-using-jackson
     * 
     * @param key
     * @param value
     */
    @JsonAnySetter
    public void fieldSetters(String key,Object value) {

        // System.out.println("key = " + key + " -> " + value.getClass() + ": " + value);
        // Handle duplicate "foo" fields to merge in strings/objects into a List<Object>
        if ("foo".equals(key) && value instanceof String) {
            foos.add((String) value);
        } else if ("foo".equals(key) && (value instanceof Collection<?>)) {
            foos.addAll((Collection<?>) value);
        }

    }

    @Override
    public String toString() {

        return "Type1 [isA=" + isA + ",name=" + name + ",bar=" + bar + ",foos=" + foos + "]";
    }

}

JsonNodeDupeFieldHandlingDeserializer

package jsonmerge;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.JsonNodeDeserializer;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
 * Handles JsonNodes by merging their contents if their values conflict into an arrayNode
 * 
 */
@JsonDeserialize(using = JsonNodeDupeFieldHandlingDeserializer.class)
public class JsonNodeDupeFieldHandlingDeserializer extends JsonNodeDeserializer {

    private static final long serialVersionUID = 1L;

    @Override
    protected void _handleDuplicateField(JsonParser p,DeserializationContext ctxt,JsonNodeFactory nodeFactory,String fieldName,ObjectNode objectNode,JsonNode oldValue,JsonNode newValue) throws JsonProcessingException {

        // When you encounter a duplicate field,instead of throwing an exception in this super-logic... (if the feature was set anyway)
//      super._handleDuplicateField(p,ctxt,nodeFactory,fieldName,objectNode,oldValue,newValue);

        // Merge the results into a common arrayNode
        // Note,this assumes that multiple values will combine into an array...
        // And *THAT* array will be used for future nodes to be tacked onto...
        // But if the FIRST thing to combine *IS* an array,then it will be the
        // initial array...
        // You could probably persist some way to track whether the initial array was encountered,but don't want to think about that now..
        ArrayNode asArrayValue = null;

        if (oldValue.isArray()) {
            asArrayValue = (ArrayNode) oldValue;
        } else {
            // If not,create as array for replacement and add initial value..
            asArrayValue = nodeFactory.arrayNode();
            asArrayValue.add(oldValue);
        }

        asArrayValue.add(newValue);
        objectNode.set(fieldName,asArrayValue);

    }

}
,

有了这个测试数据:

{
  "foo" : "val1","foo" : "val2","bar" : "val3","foo" :{
    "myField" : "Value1","myField" : "value2"
  }
}

此代码:

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.File;

public class Main2 {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("test.json").getAbsoluteFile();

        ObjectMapper mapper = new ObjectMapper();
        MyObject myObject = mapper.readValue(jsonFile,MyObject.class);
    }
}

class MyObject {
    // attributes

    @JsonAnySetter
    public void manyFoos(String key,Object value) {
        System.out.println(" Key : " + key + " Value : " + value);
    }
}

打印:

Key : foo Value : val1
Key : foo Value : val2
Key : bar Value : val3
Key : foo Value : {myField=value2}

问题是您在多个级别上有重复的键。使用此代码,您可以处理外部级别的重复项,并随心所欲,但 jackson 反序列化值部分。在第三个 foo 的情况下,该值是一个映射,因此会丢失重复项。