DEV Community

yangbongsoo
yangbongsoo

Posted on

jackson-dataformat-xml issues

I summarized the issues encountered while using jackson-dataformat-xml(2.12.4).

1. interface naming

'interface' is a reserved word in Java, making it a poor choice for an XML element name.

However, the existing code was using the 'interface' naming, and it was necessary to develop in accordance with the same 'interface' naming to maintain backward compatibility.

Therefore, I first attached the @XmlElement annotation to the field to be bound, specifying the name as 'interface' like below.

@XmlElement(name = "interface")
private String interFace;
Enter fullscreen mode Exit fullscreen mode

Since the Lombok library was used, the setter was automatically created with the naming 'setInterFace'.

public void setInterFace(String interFace) {
    this.interFace = interFace;
}
Enter fullscreen mode Exit fullscreen mode

However, the value of the XML 'interface' tag was not being bound correctly. Instead, changing the setter naming as follows resulted in successful binding. In other words, the 'f' was changed to lowercase.

public void setInterface(String interFace) {
    this.interFace = interFace;
}
Enter fullscreen mode Exit fullscreen mode

To understand the exact reason why the binding failed, I conducted debugging.

Image description

In attempting to find 'interface' within _beanProperties, no result appears, leading to the prop becoming null.
Consequently, an error occurs in handleUnknownVanilla with com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "interface".

Another solution to this problem is utilizing the _caseInsensitive attribute found in _beanProperties.

By adding the configuration objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true), it becomes possible to find in _beanProperties without distinguishing between uppercase and lowercase, thereby finding the properties correctly.

Additionally, using @JacksonXmlProperty instead of @XmlElement results in 'interface' being registered in _beanProperties, making it discoverable.

@JacksonXmlProperty(localName = "interface")
private String interFace;
Enter fullscreen mode Exit fullscreen mode

Image description

However, We decided not to apply it in practice because adding the dependency on com.fasterxml.jackson.dataformat.xml.annotation directly, rather than relying on javax.xml.bind.annotation, was deemed not advisable.

2. list

If multiple XML tags of the same kind are input, they can be bound to a Java List object. However, if the same XML tags do not come in succession, the behavior differs. Let's look at the example code below.

package com.fasterxml.jackson.dataformat.xml.lists;

import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import com.fasterxml.jackson.annotation.JsonMerge;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlCData;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;

public class ListTest {
    public static void main(String[] args) throws JsonProcessingException {
        String xml = ""
            + "<Data>"
            + "    <product>a</product>"
            + "    <product>b</product>"
            + "    <backUrl>www.naver.com</backUrl>"
            + "    <product>c</product>"
            + "    <product>d</product>"
            + "    <backUrl>www.naver.com</backUrl>"
            + "    <product>e</product>"
            + "</Data>";

        XmlMapper m = new XmlMapper();
        // m.setDefaultMergeable(true);
        Data data = m.readValue(xml, Data.class);
        System.out.println(data.getProduct());
    }
}

@JacksonXmlRootElement(localName = "data")
class Data {
    @JacksonXmlCData
    @JacksonXmlElementWrapper(useWrapping = false)
    @JacksonXmlProperty
    private List<String> product;

    @JacksonXmlCData
    @JacksonXmlElementWrapper(useWrapping = false)
    @JacksonXmlProperty
    private String backUrl;

    public List<String> getProduct() {
        return product;
    }

    public void setProduct(List<String> product) {
        this.product = product;
    }

    public String getBackUrl() {
        return backUrl;
    }

    public void setBackUrl(String backUrl) {
        this.backUrl = backUrl;
    }
}
Enter fullscreen mode Exit fullscreen mode

Multiple 'product' tags are supposed to be bound to a List object, but when actually calling getProduct, only 'e' appears. Although the XML request format was not standard, proper List object binding occurred with the older jaxb-api(2.1), necessitating a solution for this issue within jackson-dataformat-xml as well.

The first thing we can do is modify the setProduct method as follows. Changing it to add the product instead of overwriting it will yield the desired result.

public void setProduct(List<String> product) {
    if (this.product == null) {
        this.product = product;
    } else {
        this.product.addAll(product);
    }
}
Enter fullscreen mode Exit fullscreen mode

Another method exists as well. From jackson-dataformat-xml(2.9) onwards, you can use the provided setDefaultMergeable.

Result without setDefaultMergeable true setting: [e]
Result with setDefaultMergeable true setting: [a, b, c, d, e]

However, there's one point of caution when using setDefaultMergeable. If the getter for the product is implemented as below to defend against null, then since the initial this.product is null, the result of the getProduct method becomes EMPTY_LIST.

public List<String> getProduct() {
    return Objects.isNull(this.product) ? 
    Collections.emptyList() :
    Collections.unmodifiableList(this.product);
}
Enter fullscreen mode Exit fullscreen mode

Internally, when merging, the getProduct method is called, but since it initially receives EMPTY_LIST as a result, addition doesn't occur. This leads to an error: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: (was java.lang.UnsupportedOperationException); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.UnsupportedOperationException), even when using @JsonMerge.

Image description

Therefore, either implement getProduct in its basic form or

public List<String> getProduct() {
    return this.product;
}
Enter fullscreen mode Exit fullscreen mode

work as follows to prevent a null return.

public List<String> getProduct() {
    if (this.product == null) {
        this.product = new ArrayList<>();
    }
    return this.product;
}
Enter fullscreen mode Exit fullscreen mode

Finally, XmlMapper is a subclass of ObjectMapper. Setting XmlMapper.setDefaultMergeable(true) is fine because it calls the setDefaultMergeable method implemented by the superclass ObjectMapper.

Top comments (0)