Open kistlers opened 7 months ago
Thank you @kistlers. Yes, a new issue with reproduction works. I labelled it with record
since that is likely relevant here.
I think the record
is not necessarily relevant here, but rather the constructor/JsonCreator (see below).
At least, I still got the same error using these two final classes (IntelliJ -> convert to record, the make sure they correspond to the same records as above) when I remove the @JsonCreator
:
@JacksonXmlRootElement(localName = "ITEMROOT")
static final class ItemRoot {
@JsonProperty("Item")
@JacksonXmlElementWrapper(useWrapping = false)
private final List<Item> item;
ItemRoot(
@JsonProperty("Item") @JacksonXmlElementWrapper(useWrapping = false)
final List<Item> item) {
this.item = item;
}
@JsonProperty("Item")
@JacksonXmlElementWrapper(useWrapping = false)
public List<Item> item() {
return item;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof final ItemRoot itemRoot)) {
return false;
}
return new EqualsBuilder().append(item, itemRoot.item).isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(item).toHashCode();
}
public static final class Item {
@JsonProperty("name")
@JacksonXmlProperty(isAttribute = true)
private final String name;
@JacksonXmlText private final String value;
public Item(
@JsonProperty("name") @JacksonXmlProperty(isAttribute = true) final String name,
@JacksonXmlText final String value) {
this.name = name;
this.value = value;
}
@JsonCreator
public Item(final Map<String, String> item) {
this(item.get("name"), item.get(""));
}
@JsonProperty("name")
@JacksonXmlProperty(isAttribute = true)
public String name() {
return name;
}
@JacksonXmlText
public String value() {
return value;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof final Item item)) {
return false;
}
return new EqualsBuilder()
.append(name, item.name)
.append(value, item.value)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(name).append(value).toHashCode();
}
}
}
However, this works (no final classes and fields, all public properties, no setters/getters). I also quickly tested that with private fields with getters and setters, which also works:
@JacksonXmlRootElement(localName = "ITEMROOT")
static class ItemRoot {
@JsonProperty("Item")
@JacksonXmlElementWrapper(useWrapping = false)
public List<Item> item;
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof final ItemRoot itemRoot)) {
return false;
}
return new EqualsBuilder().append(item, itemRoot.item).isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(item).toHashCode();
}
public static class Item {
@JsonProperty("name")
@JacksonXmlProperty(isAttribute = true)
public String name;
@JacksonXmlText public String value;
// @JsonCreator
// public Item(final Map<String, String> item) {
// this(item.get("name"), item.get(""));
// }
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof final Item item)) {
return false;
}
return new EqualsBuilder()
.append(name, item.name)
.append(value, item.value)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(name).append(value).toHashCode();
}
}
}
@kistlers Thank you. I was about to suggest trying to see if equivalent POJO exhibited same problem.
I suspect this may be due to more general jackson-databind
problem with linking (or lack thereof) of property annotations for Constructors not explicitly annotated with @JsonCreator
.
Although I am not 100% sure since you are providing all annotations via constructor parameter too, so that should not matter (normally all annotations from all "accesors", including constructor parameters, are merged -- but this does not work for auto-detected constructors).
Another note: use of Map<String, String>
may be problematic as well: XML structures are not good match with Java Map
s.
But I am also confused as to intent of 2 annotated constructrors:
public Item(
@JsonProperty("name") @JacksonXmlProperty(isAttribute = true) final String name,
@JacksonXmlText final String value) {
this.name = name;
this.value = value;
}
@JsonCreator
public Item(final Map<String, String> item) {
this(item.get("name"), item.get(""));
}
both of which would be detected; but that cannot really be used together (how would Jackson know which one to use, basically).
I guess it'd be good to have still bit more minimal reproduction as I am not quite sure how this model is expected to work, esp. wrt Map
value.
About the use of the Map
, I used it as it was the only solution I found to make deserialization work with records.
Anyway, here is a simpler reproduction (I think). I removed the outer ItemRoot
class and just kept Item
.
So this fails:
class XmlMapperReproductionTest {
@Test
void testDeserializeItemRoot() throws JsonProcessingException {
var xmlMapper = new XmlMapper().registerModule(new ParanamerModule());
var item = new Item("name1", "value1");
var itemSerialized = xmlMapper.writerWithDefaultPrettyPrinter().writeValueAsString(item);
var itemXml = """
<Item name="name1">value1</Item>
""";
assertEquals(itemXml, itemSerialized);
var itemDeserialized = xmlMapper.readValue(itemXml, Item.class);
assertEquals(item, itemDeserialized);
}
@JacksonXmlRootElement
public record Item(
@JsonProperty("name") @JacksonXmlProperty(isAttribute = true) String name,
@JacksonXmlText String value) {}
}
with the error message:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid definition for property '' (of type `ch.ubique.backend.test.assertion.XmlMapperReproductionTest$Item`): Could not find creator property with name '' (known Creator properties: [name, value])
at [Source: (StringReader); line: 1, column: 1]
Swapping the record to this very simple POJO (the Equals, HashCode, and constructor are only there to keep the Test class identical):
@JacksonXmlRootElement
public static class Item {
@JsonProperty("name")
@JacksonXmlProperty(isAttribute = true)
public String name;
@JacksonXmlText public String value;
public Item(String name, String value) {
this.name = name;
this.value = value;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Item item)) {
return false;
}
return new EqualsBuilder().append(name, item.name).append(value, item.value).isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(name).append(value).toHashCode();
}
}
Also, using this Item
class with private final fields also passes the test:
@JacksonXmlRootElement
public static class Item {
@JsonProperty("name")
@JacksonXmlProperty(isAttribute = true)
private final String name;
@JacksonXmlText private final String value;
public Item(String name, String value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Item item)) {
return false;
}
return new EqualsBuilder().append(name, item.name).append(value, item.value).isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(name).append(value).toHashCode();
}
}
Simple workaround is to add @JacksonXmlProperty(localName = "") along with @JacksonXmlText. Could be done in meta-annotation too.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@JacksonXmlText
@JacksonXmlProperty(localName = "")
@JacksonAnnotationsInside
public @interface XmlText {
}
@Aemmie Your meta-annotation solution wraps value into another xml element. It's not text. And use of @JacksonXmlText @JacksonXmlProperty(localName = "")
leads to the original exception during deserialization.
This is a reproduction of #198. It was mentioned opening a new issue is preferred.
The issue is, that
@JacksonXmlText
seems to work as intended for serialization, but not for deserialization.Hence, my reproduction of the original issue with 2.15.4:
I have the following models and tests:
First, I serialize the model to verify what I actually want to deserialize is correct and then I serialize the XML again.
The tests pass because of
@JsonCreator
inItem
. Without the annotation, I get the following error on thexmlMapper.readValue()
: