javaxmljacksonjackson-databindjackson-dataformat-xml

How to use Jackson to map XML child entries with a root element that has attributes


Let's say we have the following XML file:

<?xml version="1.0" encoding="UTF-8" ?>
<car>
    <name>Speedy VW Golf</name>
    <tires>
        <tire>
            <value>Michelin FL</value>
        </tire>
        <tire>
            <value>Michelin FR</value>
        </tire>
        <tire>
            <value>Michelin BL</value>
        </tire>
        <tire>
            <value>Michelin BR</value>
        </tire>
    </tires>
    <component componentId="compId", serialId="lalal">
        <frame frameId="1234-56789-0" vendor="Steel MC">
            <item vendorName="Foo" country="Italy"/>
            <item vendorName="Bar" country="Germany"/>
            <item vendorName="Foobar" country="China"/>
        </frame>
    </component>
</car>

How can I map all item children? The following Java code:

package com.acme.carsample;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;

import java.io.File;
import java.nio.file.Files;
import java.util.List;

public class CarReader {

    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Car {

        private String name;

        private List<Tire> tires;

        private Component component;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public List<Tire> getTires() {
            return tires;
        }

        public void setTires(List<Tire> tires) {
            this.tires = tires;
        }

        public Component getComponent() {
            return component;
        }

        public void setComponent(Component component) {
            this.component = component;
        }
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Tire {

        private String value;

        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Component {

        private Frame frame;

        public Frame getFrame() {
            return frame;
        }

        public void setFrame(Frame frame) {
            this.frame = frame;
        }
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Frame {

        @JacksonXmlProperty(isAttribute = true)
        private String frameId;

        @JacksonXmlProperty(isAttribute = true)
        private String vendor;

        @JacksonXmlElementWrapper(useWrapping = false)
        private List<Item> items;

        public String getFrameId() {
            return frameId;
        }

        public void setFrameId(String frameId) {
            this.frameId = frameId;
        }

        public String getVendor() {
            return vendor;
        }

        public void setVendor(String vendor) {
            this.vendor = vendor;
        }

        public List<Item> getItems() {
            return items;
        }

        public void setItems(List<Item> items) {
            this.items = items;
        }
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    @JacksonXmlRootElement(localName = "item")
    public static class Item {

        @JacksonXmlProperty(isAttribute = true)
        private String vendorName;

        @JacksonXmlProperty(isAttribute = true)
        private String country;

        public String getVendorName() {
            return vendorName;
        }

        public void setVendorName(String vendorName) {
            this.vendorName = vendorName;
        }

        public String getCountry() {
            return country;
        }

        public void setCountry(String country) {
            this.country = country;
        }
    }

    public static void main(String[] arguments) {
        try {
            // Deserialize the car object (XML to Java)
            String data = new String(Files.readAllBytes(new File("Car.xml").toPath()));
            XmlMapper xmlMapper = new XmlMapper();
            Car car = xmlMapper.readValue(data, Car.class);

            // Serialize the car object (Java to json)
            ObjectMapper objectMapper = new ObjectMapper();
            data = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(car);
            System.out.println(data);

        } catch (Exception exception) {
            System.err.println("An error occurred: " + exception.getMessage());
        }
    }
}

Results in the following JSON output (The item children attributes are not mapped as list, but are instead null:

{
  "name" : "Speedy VW Golf",
  "tires" : [ {
    "value" : "Michelin FL"
  }, {
    "value" : "Michelin FR"
  }, {
    "value" : "Michelin BL"
  }, {
    "value" : "Michelin BR"
  } ],
  "component" : {
    "frame" : {
      "frameId" : "1234-56789-0",
      "vendor" : "Steel MC",
      "items" : null
    }
  }
}

Solution

  • The solution is to move the @JacksonXmlRootElement class annotation from the Item class to the field variable items in Frame:

    @JacksonXmlProperty(localName = "item")
    @JacksonXmlElementWrapper(useWrapping = false)
    private List<Item> items;
    

    Afterwards the child item are properly read.