jacksonjackson-dataformat-xml

deserializing XML list with jackson


I have some xml that looks as follows:

<model>
  <action>
     <items>
        <input name="i1"/>
        <input name="i2"/>
        <output name="o1"/>
        <output name="o2"/>
     </items>
  </action>
</model>

I'm using the Jackson XMLMapper to deserialize it:

Model m = xmlMapper.readValue(file, Model.class);

My Model class contains an Action property (with the @JsonProperty("action") annotation)

My Action class that contains a List<Input> and a List<Output>

Both the lists are empty after deserialization.

I can't figure out what annotations I should use to indicate that the items tag contains both inputs and outputs.

I have tried the @JacksonXmlElementWrapper annotation on both lists, but that doesn't seem to work.


Solution

  • Jackson does automatically deserialize xml elements with same name into an array but it will fail if multiple arrays are in the same level. You are going to have to use custom deserializer. here is an example custom deserializer that can successfully deserialize example XML doc

    public class ActionDeserializer extends StdDeserializer<Action> {
    
        public ActionDeserializer() {
            super((Class<?>)null);
        }
    
        public ActionDeserializer(Class<?> vc) {
            super(vc);
        }
    
        @Override
        public Action deserialize(JsonParser parser, DeserializationContext ctx) throws IOException {
    
            Action action = new Action();
            action.inputs = new ArrayList<>();
            action.outputs = new ArrayList<>();
    
            // <action>
            JsonNode actionNode = parser.getCodec().readTree(parser);
            // <items>
            JsonNode itemsNode = actionNode.get("items");
    
            // deserialize <input> elements into pojo list
            JsonNode inputNode = itemsNode.get("input");
            if (inputNode.getNodeType() == JsonNodeType.ARRAY) {
                Iterator<JsonNode> iterator = inputNode.iterator();
                while (iterator.hasNext()) {
                    action.inputs.add(new XmlMapper().treeToValue(iterator.next(), Input.class));
                }
            }
    
            // deserialize <output> elements into pojo list
            JsonNode outputNode = itemsNode.get("output");
            if (outputNode.getNodeType() == JsonNodeType.ARRAY) {
                Iterator<JsonNode> iterator = outputNode.iterator();
                while (iterator.hasNext()) {
                    action.outputs.add(new XmlMapper().treeToValue(iterator.next(), Output.class));
                }
            }
    
            return action;
        }
    }
    

    you register the custom deserializer using simple module

        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addDeserializer(Action.class, new ActionDeserializer());
        xmlMapper.registerModule(simpleModule);
    

    EDIT:

    here is my complete test class. the custom deserializer works even without the condition on type of json node

    public class Model {
    
        public Action action;
    
        public static void main(String[] args) throws JsonProcessingException {
    
            String xml = """
                <model>
                  <action>
                     <items>
                        <input name="i1"/>
                        <input name="i2"/>
                        <output name="o1"/>
                        <output name="o2"/>
                     </items>
                  </action>
                </model>
                """;
    
            XmlMapper xmlMapper = new XmlMapper();
            SimpleModule simpleModule = new SimpleModule();
            simpleModule.addDeserializer(Action.class, new ActionDeserializer());
            xmlMapper.registerModule(simpleModule);
    
            Model m = xmlMapper.readValue(xml, Model.class);
            System.out.println("inputs " + m.action.inputs.stream().map(x -> x.name).toList());
            System.out.println("outputs " + m.action.outputs.stream().map(x -> x.name).toList());
        }
    
        public static class Action {
            public List<Input> inputs;
            public List<Output> outputs;
        }
    
        public static class Input {
            public String name;
        }
    
        public static class Output {
            public String name;
        }
    
        public static class ActionDeserializer extends StdDeserializer<Action> {
    
            public ActionDeserializer() {
                super((Class<?>)null);
            }
    
            @Override
            public Action deserialize(JsonParser parser, DeserializationContext ctx) throws IOException {
    
                Action action = new Action();
                action.inputs = new ArrayList<>();
                action.outputs = new ArrayList<>();
    
                // <action>
                JsonNode actionNode = parser.getCodec().readTree(parser);
                // <items>
                JsonNode itemsNode = actionNode.get("items");
    
                // deserialize <input> elements into pojo list
                JsonNode inputNode = itemsNode.get("input");
                Iterator<JsonNode> iterator = inputNode.iterator();
                while (iterator.hasNext()) {
                    action.inputs.add(new XmlMapper().treeToValue(iterator.next(), Input.class));
                }
    
                // deserialize <output> elements into pojo list
                JsonNode outputNode = itemsNode.get("output");
                iterator = outputNode.iterator();
                while (iterator.hasNext()) {
                    action.outputs.add(new XmlMapper().treeToValue(iterator.next(), Output.class));
                }
    
                return action;
            }
        }
    }
    

    output

    inputs [i1, i2]
    outputs [o1, o2]