diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/XmlBeanDeserializerModifier.java b/src/main/java/tools/jackson/dataformat/xml/deser/XmlBeanDeserializerModifier.java index d82bfb1c..78b807ff 100644 --- a/src/main/java/tools/jackson/dataformat/xml/deser/XmlBeanDeserializerModifier.java +++ b/src/main/java/tools/jackson/dataformat/xml/deser/XmlBeanDeserializerModifier.java @@ -36,7 +36,7 @@ public List 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(); @@ -49,6 +49,31 @@ public List 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 @@ -62,7 +87,7 @@ public List 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) @@ -81,6 +106,27 @@ public List 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 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) diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/XmlValueInstantiators.java b/src/main/java/tools/jackson/dataformat/xml/deser/XmlValueInstantiators.java index be18629a..f6e68890 100644 --- a/src/main/java/tools/jackson/dataformat/xml/deser/XmlValueInstantiators.java +++ b/src/main/java/tools/jackson/dataformat/xml/deser/XmlValueInstantiators.java @@ -143,6 +143,21 @@ private Map _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(); diff --git a/src/test/java/tools/jackson/dataformat/xml/tofix/JaxbXmlValueRecord559Test.java b/src/test/java/tools/jackson/dataformat/xml/jaxb/JaxbXmlValueRecord559Test.java similarity index 69% rename from src/test/java/tools/jackson/dataformat/xml/tofix/JaxbXmlValueRecord559Test.java rename to src/test/java/tools/jackson/dataformat/xml/jaxb/JaxbXmlValueRecord559Test.java index 996320b7..f4c243f7 100644 --- a/src/test/java/tools/jackson/dataformat/xml/tofix/JaxbXmlValueRecord559Test.java +++ b/src/test/java/tools/jackson/dataformat/xml/jaxb/JaxbXmlValueRecord559Test.java @@ -1,4 +1,4 @@ -package tools.jackson.dataformat.xml.tofix; +package tools.jackson.dataformat.xml.jaxb; import org.junit.jupiter.api.Test; @@ -7,19 +7,11 @@ 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") @@ -27,8 +19,7 @@ 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 @@ -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 = "foo";