Skip to content
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