Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public List<BeanPropertyDefinition> updateProperties(DeserializationConfig confi
{
final AnnotationIntrospector intr = config.getAnnotationIntrospector();
int changed = 0;

for (int i = 0, propCount = propDefs.size(); i < propCount; ++i) {
BeanPropertyDefinition prop = propDefs.get(i);
AnnotatedMember acc = prop.getPrimaryMember();
Expand All @@ -49,6 +49,31 @@ public List<BeanPropertyDefinition> updateProperties(DeserializationConfig confi
// name (and hope this does not break other parts...)
Boolean b = AnnotationUtil.findIsTextAnnotation(config, intr, acc);
if (b != null && b.booleanValue()) {
// [dataformat-xml#559] For records with JAXB @XmlValue: the annotation
// introspector may rename the property (e.g. to "value"), causing a
// property definition split where the constructor parameter ends up in
// a separate property under the original Java name. If we detect this
// split, rename the constructor-parameter property to the text value
// name and remove this (getter-only) one.
String memberName = acc.getName();
if (!memberName.equals(prop.getName())) {
int splitIdx = _findSplitProperty(propDefs, memberName, i);
if (splitIdx >= 0) {
// make copy-on-write as necessary
if (changed == 0) {
propDefs = new ArrayList<>(propDefs);
}
++changed;
// Rename the split counterpart (which has the constructor param)
propDefs.set(splitIdx, propDefs.get(splitIdx).withSimpleName(_cfgNameForTextValue));
// Remove this getter-only property definition
propDefs.remove(i);
--propCount;
--i; // re-examine this index
continue;
}
}
// Default: just rename this property
BeanPropertyDefinition newProp = prop.withSimpleName(_cfgNameForTextValue);
if (newProp != prop) {
// 24-Mar-2026, tatu: Create defensive copy
Expand All @@ -62,7 +87,7 @@ public List<BeanPropertyDefinition> updateProperties(DeserializationConfig confi
}
// second: do we need to handle wrapping (for Lists)?
PropertyName wrapperName = prop.getWrapperName();

if (wrapperName != null && wrapperName != PropertyName.NO_NAME) {
String localName = wrapperName.getSimpleName();
if ((localName != null && localName.length() > 0)
Expand All @@ -81,6 +106,27 @@ public List<BeanPropertyDefinition> updateProperties(DeserializationConfig confi
return propDefs;
}

/**
* [dataformat-xml#559] Find a property definition that was split from the isText
* property due to annotation-introspector-driven renaming (e.g. JAXB @XmlValue
* assigning implicit name "value" while the constructor param keeps the Java name).
*
* @param memberName Java member name of the isText property's primary member
* @param excludeIdx index to skip (the isText property itself)
* @return index of the split counterpart, or -1 if not found
*/
private int _findSplitProperty(List<BeanPropertyDefinition> propDefs,
String memberName, int excludeIdx)
{
for (int j = 0, len = propDefs.size(); j < len; ++j) {
if (j == excludeIdx) continue;
if (memberName.equals(propDefs.get(j).getName())) {
return j;
}
}
return -1;
}

@Override
public ValueDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription.Supplier beanDescRef, ValueDeserializer<?> deser)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,21 @@ private Map<String, String> _findPropertyRenames(DeserializationConfig config,
if (!_cfgNameForTextValue.equals(origName)) {
renamed = _cfgNameForTextValue;
}
// [dataformat-xml#559] For records with JAXB @XmlValue: the annotation
// introspector may assign an implicit name (e.g. "value") to the
// accessor, but the constructor parameter keeps the original declared
// Java name. Use the primary member's Java name (e.g. record accessor
// method name) to add a rename entry for the creator param.
if (member != null) {
String memberName = member.getName();
if (!memberName.equals(origName)
&& !_cfgNameForTextValue.equals(memberName)) {
if (renames.isEmpty()) {
renames = new HashMap<>();
}
renames.put(memberName, _cfgNameForTextValue);
}
}
} else {
// Check wrapper name (for Lists)
PropertyName wrapperName = propDef.getWrapperName();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package tools.jackson.dataformat.xml.tofix;
package tools.jackson.dataformat.xml.jaxb;

import org.junit.jupiter.api.Test;

Expand All @@ -7,28 +7,19 @@
import tools.jackson.databind.AnnotationIntrospector;
import tools.jackson.databind.introspect.JacksonAnnotationIntrospector;
import tools.jackson.dataformat.xml.*;
import tools.jackson.dataformat.xml.testutil.failure.JacksonTestFailureExpected;
import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector;

import static org.junit.jupiter.api.Assertions.assertEquals;

// [dataformat-xml#559] JAXB @XmlValue deserializing not working with records
//
// Root cause: @XmlValue targets only FIELD and METHOD, not PARAMETER.
// For records, Java doesn't propagate the annotation to the constructor parameter,
// so Jackson can't match the property-based creator param to the @XmlValue property.
// Additionally, the JAXB introspector assigns implicit name "value" to @XmlValue
// properties, causing a property definition split (constructor param keeps "name",
// while field/getter get renamed to "value").
public class JaxbXmlValueRecord559Test extends XmlTestUtil
{
@XmlRootElement(name = "TestObject")
record TestObject(
@XmlValue String name,
@XmlAttribute int age) {}

// POJO equivalent — works fine because field/getter/setter all merge
// under the JAXB-assigned "value" name (no constructor param involved)
// POJO equivalent
@XmlRootElement(name = "TestObject")
static class TestPojo {
@XmlValue
Expand Down Expand Up @@ -57,8 +48,7 @@ public void testDeserializePojo559() throws Exception {
assertEquals(12, obj.age);
}

// Record: fails
@JacksonTestFailureExpected
// [dataformat-xml#559] Record: was failing, now fixed
@Test
public void testDeserializeRecord559() throws Exception {
String xml = "<TestObject age=\"12\">foo</TestObject>";
Expand Down