jackson-dataformat-xml issues
Tags: java, springboot
Published: 2024-03-16 15:29:51

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.

```java
@XmlElement(name = "interface")
private String interFace;
```

Since the Lombok library was used, the setter was automatically created with the naming 'setInterFace'.
```java
public void setInterFace(String interFace) {
	this.interFace = interFace;
}
```

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.
```java
public void setInterface(String interFace) {
	this.interFace = interFace;
}
```

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4hoj5e2nr96s3ecrn73j.png)

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.

```java
@JacksonXmlProperty(localName = "interface")
private String interFace;
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2n3atnrz5w490n2a7g8m.png)

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.

```java
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;
	}
}
```

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.

```java
public void setProduct(List<String> product) {
	if (this.product == null) {
		this.product = product;
	} else {
		this.product.addAll(product);
	}
}
```

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.

```java
public List<String> getProduct() {
	return Objects.isNull(this.product) ? 
	Collections.emptyList() :
	Collections.unmodifiableList(this.product);
}
```


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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/byht08vsr3kah46zapdh.png)

Therefore, either implement getProduct in its basic form or 
```java
public List<String> getProduct() {
	return this.product;
}
```

work as follows to prevent a null return.
```java
public List<String> getProduct() {
	if (this.product == null) {
		this.product = new ArrayList<>();
	}
	return this.product;
}
```

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