具有空元素键的 Jackson JsonNode

Jackson JsonNode with empty element key

我正在使用 jackson-dataformat-xml (2.9) 将 XML 解析为 Json 节点,然后将其解析为 JSON (XML 非常动态,所以这就是为什么我使用 JsonNode 而不是绑定到 POJO。例如 'elementName' 和 'id' 名称可能会有所不同)。

碰巧在JSON解析阶段,元素键之一是空字符串(“”)。

XML:

<elementName>
      <id type="pid">abcdef123</id>
</elementName>

解析逻辑:

public Parser() {
        ObjectMapper jsonMapper = new ObjectMapper();
        XmlMapper xmlMapper = new XmlMapper(new XmlFactory(new WstxInputFactory()));
}

public InputStream parseXmlResponse(InputStream xmlStream) {
        InputStream stream = null;

        try {
            JsonNode node = xmlMapper.readTree(xmlStream);
            stream = new ByteArrayInputStream(jsonMapper.writer().writeValueAsBytes(node));
        } catch (IOException e) {
            e.printStackTrace();
        }

        return stream;
    }

Json:

结果:

{
   "elementName": {
     "id": {
        "type": "pid",
        "": "abcdef123"
     }
   },
}

预计:

{
   "elementName": {
     "id": {
        "type": "pid",
        "value": "abcdef123"
     }
   },
}

我的想法是每当我有空键“”时找到并用"value"替换它。在 XML 反序列化或 JSON 序列化期间。 我已经尝试使用默认的序列化程序、过滤器,但还没有以简洁明了的方式使用它。

非常感谢您的建议。

感谢您的帮助。

可能的解决方案:

根据@shoek 的建议,我决定编写一个自定义序列化程序以避免在此过程中创建中间对象 (ObjectNode)。

编辑:重构基于@shoek 提出的相同解决方案。

public class CustomNode {
    private JsonNode jsonNode;

    public CustomNode(JsonNode jsonNode) {
        this.jsonNode = jsonNode;
    }

    public JsonNode getJsonNode() {
        return jsonNode;
    }
}

public class CustomObjectsResponseSerializer extends StdSerializer<CustomNode> {

    protected CustomObjectsResponseSerializer() {
        super(CustomNode.class);
    }

    @Override
    public void serialize(CustomNode node, JsonGenerator jgen, SerializerProvider provider) throws IOException {
        convertObjectNode(node.getJsonNode(), jgen, provider);
    }

    private void convertObjectNode(JsonNode node, JsonGenerator jgen, SerializerProvider provider) throws IOException {
        jgen.writeStartObject();
        for (Iterator<String> it = node.fieldNames(); it.hasNext(); ) {
            String childName = it.next();
            JsonNode childNode = node.get(childName);
            // XML parser returns an empty string as value name. Replacing it with "value"
            if (Objects.equals("", childName)) {
                childName = "value";
            }

            if (childNode instanceof ArrayNode) {
                jgen.writeFieldName(childName);
                convertArrayNode(childNode, jgen, provider);
            } else if (childNode instanceof ObjectNode) {
                jgen.writeFieldName(childName);
                convertObjectNode(childNode, jgen, provider);
            } else {
                provider.defaultSerializeField(childName, childNode, jgen);
            }
        }
        jgen.writeEndObject();

    }

    private void convertArrayNode(JsonNode node, JsonGenerator jgen, SerializerProvider provider) throws IOException {
        jgen.writeStartArray();
        for (Iterator<JsonNode> it = node.elements(); it.hasNext(); ) {
            JsonNode childNode = it.next();

            if (childNode instanceof ArrayNode) {
                convertArrayNode(childNode, jgen, provider);
            } else if (childNode instanceof ObjectNode) {
                convertObjectNode(childNode, jgen, provider);
            } else {
                provider.defaultSerializeValue(childNode, jgen);
            }
        }
        jgen.writeEndArray();
    }
}

复制到新的 ObjectNode 可能会解决您的问题。

package com.example;

import java.util.Iterator;
import java.util.Objects;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ValueNode;

public class Whosebug62009220 {
    public static void main(String[] args) throws JsonProcessingException {
        convert("{\"elementName\":{\"id\":{\"type\":\"pid\",\"\":\"abcdef123\"}}}");

        convert("{\"array\":[1,99,3]}");

        convert("{\"complex-array\":[null, 1, [3,7,5], {\"type\":\"pid\",\"\":\"abcdef123\"}]}");
    }

    private static void convert(String str) throws JsonProcessingException {
        JsonNode input = (new ObjectMapper()).readTree(str);
        System.out.println("in:");
        System.out.println(input);

        ObjectMapper mapper = new ObjectMapper();

        ObjectNode obj = convertObjectNode(input, mapper);

        String output = mapper.writer().writeValueAsString(obj);
        System.out.println("out:");
        System.out.println(output);
        System.out.println("----------");
    }

    private static ArrayNode convertArrayNode(JsonNode current, ObjectMapper mapper) {
        ArrayNode to = mapper.createArrayNode();
        for (Iterator<JsonNode> it = current.elements(); it.hasNext();) {
            JsonNode childNode = it.next();

            if (childNode instanceof ValueNode) {
                to.add(childNode);
            } else if (childNode instanceof ArrayNode) {
                // recurse
                to.add(convertArrayNode(childNode, mapper));
            } else if (childNode instanceof ObjectNode) {
                to.add(convertObjectNode(childNode, mapper));
            }
        }
        return to;
    }

    private static ObjectNode convertObjectNode(JsonNode current, ObjectMapper mapper) {
        ObjectNode to = mapper.createObjectNode();
        for (Iterator<String> it = current.fieldNames(); it.hasNext();) {
            String childName = it.next();
            JsonNode childNode = current.get(childName);

            if (Objects.equals("", childName)) {
                childName = "value";
            }

            if (childNode instanceof ValueNode) {
                to.set(childName, childNode);
            } else if (childNode instanceof ArrayNode) {
                to.set(childName, convertArrayNode(childNode, mapper));
            } else if (childNode instanceof ObjectNode) {
                // recurse
                to.set(childName, convertObjectNode(childNode, mapper));
            }
        }
        return to;
    }
}

前面的代码导致:

in:
{"elementName":{"id":{"type":"pid","":"abcdef123"}}}
out:
{"elementName":{"id":{"type":"pid","value":"abcdef123"}}}
----------
in:
{"array":[1,99,3]}
out:
{"array":[1,99,3]}
----------
in:
{"complex-array":[null,1,[3,7,5],{"type":"pid","":"abcdef123"}]}
out:
{"complex-array":[null,1,[3,7,5],{"type":"pid","value":"abcdef123"}]}
----------

P.S.

我找不到为非类型 JsonNode 使用自定义序列化程序(如 this)的方法。 如果有人知道,请 post 您的回答。考虑到内存 usage/processing 时间,这可能是一个更好的解决方案。

序列化器版本。

package com.example;

import java.io.IOException;
import java.util.Iterator;
import java.util.Objects;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.module.SimpleSerializers;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

public class Whosebug62009220_B {
    public static void main(String[] args) throws JsonProcessingException {
        // see https://www.baeldung.com/jackson-call-default-serializer-from-custom-serializer

        convert("{\"elementName\":{\"id\":{\"type\":\"pid\",\"\":\"abcdef123\"}}}");

        // j =  {"":"is_empty_field","num":1,"str":"aa","null_val":null,"empty_val":"","array":[3,5],"obj":{"a":"A","b":22}}
        // (simple json object)
        String j = "{\"\":\"is_empty_field\",\"num\":1,\"str\":\"aa\",\"null_val\":null,\"empty_val\":\"\",\"array\":[3,5],\"obj\":{\"a\":\"A\",\"b\":22}}";
        convert(j);

        // g = {"":"is_empty_field","num":1,"str":"aa","null_val":null,"empty_val":"","array":[3,{"":"is_empty_field","num":1,"str":"aa","null_val":null,"empty_val":"","array":[3,5],"obj":{"a":"A","b":22}}],"obj":{"":"is_empty_field","num":1,"str":"aa","null_val":null,"empty_val":"","array":[3,5],"obj":{"a":"A","b":22}}}
        // (includes an array containing object j, and an object j containing array)
        String g = " {\"\":\"is_empty_field\",\"num\":1,\"str\":\"aa\",\"null_val\":null,\"empty_val\":\"\",\"array\":[3,{\"\":\"is_empty_field\",\"num\":1,\"str\":\"aa\",\"null_val\":null,\"empty_val\":\"\",\"array\":[3,5],\"obj\":{\"a\":\"A\",\"b\":22}}],\"obj\":{\"\":\"is_empty_field\",\"num\":1,\"str\":\"aa\",\"null_val\":null,\"empty_val\":\"\",\"array\":[3,5],\"obj\":{\"a\":\"A\",\"b\":22}}}";
        convert(g);
    }

    private static void convert(String str) throws JsonProcessingException {
        JsonNode input = (new ObjectMapper()).readTree(str);
        System.out.println("in:");
        System.out.println(input);

        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        SimpleSerializers serializers = new SimpleSerializers();
        serializers.addSerializer(ObjectNode.class, new MyObjectNodeSerializer());
        module.setSerializers(serializers);
        mapper.registerModule(module);

        String output = mapper.writer().writeValueAsString(input);
        System.out.println("out:");
        System.out.println(output);
        System.out.println("----------");
    }
}

class MyObjectNodeSerializer extends StdSerializer<ObjectNode> {

    public MyObjectNodeSerializer() {
        super(ObjectNode.class);
    }

    public static MyObjectNodeSerializer create() {
        return new MyObjectNodeSerializer();
    }

    @Override
    public void serialize(ObjectNode value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartObject();
        for (Iterator<String> it = value.fieldNames(); it.hasNext();) {
            String childName = it.next();
            JsonNode childNode = value.get(childName);

            if (Objects.equals("", childName)) {
                childName = "value";
            }

            if (childNode instanceof ArrayNode) {
                gen.writeFieldName(childName);
                MyArrayNodeSerializer.create().serialize((ArrayNode) childNode, gen, provider);
            } else if (childNode instanceof ObjectNode) {
                gen.writeFieldName(childName);
                this.serialize((ObjectNode) childNode, gen, provider);
            } else {
                provider.defaultSerializeField(childName, childNode, gen);
            }
        }
        gen.writeEndObject();
    }
}

class MyArrayNodeSerializer extends StdSerializer<ArrayNode> {

    public MyArrayNodeSerializer() {
        super(ArrayNode.class);
    }

    public static MyArrayNodeSerializer create() {
        return new MyArrayNodeSerializer();
    }

    @Override
    public void serialize(ArrayNode value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartArray();
        for (Iterator<JsonNode> it = value.elements(); it.hasNext();) {
            JsonNode childNode = it.next();
            if (childNode instanceof ArrayNode) {
                this.serialize((ArrayNode) childNode, gen, provider);
            } else if (childNode instanceof ObjectNode) {
                MyObjectNodeSerializer.create().serialize((ObjectNode) childNode, gen, provider);
            } else {
                provider.defaultSerializeValue(childNode, gen);
            }
        }
        gen.writeEndArray();
    }
}

您也可以简单地 post-process JSON DOM,遍历所有对象,然后 重命名"value" 的空字符串键。

竞争条件: 这样的键可能已经存在,不能被覆盖
(例如 <id type="pid" value="existing">abcdef123</id>)。

用法:
(注意:您不应该默默地抑制异常和 return null,而是允许它传播以便调用者可以决定捕获并在需要时应用故障转移逻辑)

public InputStream parseXmlResponse(InputStream xmlStream) throws IOException {
    JsonNode node = xmlMapper.readTree(xmlStream);
    postprocess(node);
    return new ByteArrayInputStream(jsonMapper.writer().writeValueAsBytes(node));
}

Post-处理中:

private void postprocess(JsonNode jsonNode) {

    if (jsonNode.isArray()) {
        ArrayNode array = (ArrayNode) jsonNode;
        Iterable<JsonNode> elements = () -> array.elements();

        // recursive post-processing
        for (JsonNode element : elements) {
            postprocess(element);
        }
    }
    if (jsonNode.isObject()) {
        ObjectNode object = (ObjectNode) jsonNode;
        Iterable<String> fieldNames = () -> object.fieldNames();

        // recursive post-processing
        for (String fieldName : fieldNames) {
            postprocess(object.get(fieldName));
        }
        // check if an attribute with empty string key exists, and rename it to 'value',
        // unless there already exists another non-null attribute named 'value' which
        // would be overwritten.
        JsonNode emptyKeyValue = object.get("");
        JsonNode existing = object.get("value");
        if (emptyKeyValue != null) {
            if (existing == null || existing.isNull()) {
                object.set("value", emptyKeyValue);
                object.remove("");
            } else {
                System.err.println("Skipping empty key value as a key named 'value' already exists.");
            }
        }
    }
}

输出: 符合预期。

{
   "elementName": {
     "id": {
        "type": "pid",
        "value": "abcdef123"
     }
   },
}

编辑:对性能的考虑:

我用一个大 XML 文件进行了测试(enwikiquote-20200520-pages-articles-multistream.xml、en.wikiquote XML 转储,498.4 MB) , 100 轮,测量时间如下(使用带 System.nanoTime() 的增量):

  • 平均 读取 时间(文件,SSD):2870.96 毫秒
    (JsonNode node = xmlMapper.readTree(xmlStream);)
  • 平均 post处理 时间:0.04 毫秒
    (postprocess(node);)
  • 平均写入时间(内存):0.31毫秒
    (new ByteArrayInputStream(jsonMapper.writer().writeValueAsBytes(node));)

对于从大约 500 MB 的文件构建对象树来说,这只是几分之一毫秒 - 因此性能非常好,不用担心。

我发现可以通过配置实现此行为。 这是 kotlin 代码,但转换为 java 很简单 只需使用适当的配置创建 xmlMapper

    fun jacksonCreateXmlMapper(): XmlMapper {
        val module = JacksonXmlModule()
        module.setXMLTextElementName("value")
        return XmlMapper(module)
    }

输入

<products>
    <product count="5">apple</product>
    <product count="10">orange</product>
</products>

你得到:

{
  "product" : [ {
    "count" : "5",
    "value" : "apple"
  }, {
    "count" : "10",
    "value" : "orange"
  } ]
}