diff --git a/release-notes/CREDITS b/release-notes/CREDITS index d60e231e7..d0a2af212 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -151,4 +151,9 @@ Charles Moulliard (@cmoulliard) Christian Beikov (@beikov) * Reported #871: `XmlMapper` regression in 3.2.0: `xsi:nil` collection element on unwrapped collection wrongly read as null collection - (3.3.0) + (3.2.1) + +@soc + * Reported #884: `NullPointerException` in `XmlValueInstantiators` for Record + with `@JsonIgnore`-annotated getter + (3.2.2) diff --git a/release-notes/VERSION b/release-notes/VERSION index 89708a38f..3a7f8066a 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -9,6 +9,13 @@ Version: 3.x (for earlier see VERSION-2.x) No changes since 3.2 +3.2.2 (not yet released) + +#884: `NullPointerException` in `XmlValueInstantiators` for Record with + `@JsonIgnore`-annotated getter + (reported by @soc) + (fix by @cowtowncoder, w/ Claude code) + 3.2.1 (10-Jul-2026) #871: `XmlMapper` regression in 3.2.0: `xsi:nil` collection element on unwrapped 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 be18629a0..8c4b38b0c 100644 --- a/src/main/java/tools/jackson/dataformat/xml/deser/XmlValueInstantiators.java +++ b/src/main/java/tools/jackson/dataformat/xml/deser/XmlValueInstantiators.java @@ -134,6 +134,13 @@ private Map _findPropertyRenames(DeserializationConfig config, for (BeanPropertyDefinition propDef : beanDescRef.get().findProperties()) { final AnnotatedMember member = propDef.getPrimaryMember(); + // 24-Jul-2026, tatu: [dataformat-xml#884] Property definition may have no + // accessors at all (Record with `@JsonIgnore`d getter): if so, nothing + // to rename -- and must match `XmlBeanDeserializerModifier.updateProperties()` + // which also skips such properties + if (member == null) { + continue; + } final String origName = propDef.getName(); String renamed = null; diff --git a/src/main/java/tools/jackson/dataformat/xml/util/AnnotationUtil.java b/src/main/java/tools/jackson/dataformat/xml/util/AnnotationUtil.java index ee31ff8ed..04da2178b 100644 --- a/src/main/java/tools/jackson/dataformat/xml/util/AnnotationUtil.java +++ b/src/main/java/tools/jackson/dataformat/xml/util/AnnotationUtil.java @@ -5,17 +5,27 @@ import tools.jackson.databind.cfg.MapperConfig; import tools.jackson.databind.introspect.AnnotatedMember; +/** + * Helper class for accessing XML-specific annotation information via + * {@link AnnotationIntrospector.XmlExtensions} introspectors. + *

+ * NOTE: all methods accept {@code null} member and return {@code null} in that + * case: property definitions without any accessors do occur (see + * [dataformat-xml#884]) and cannot have annotations to find, anyway. + */ public class AnnotationUtil { public static String findNamespaceAnnotation(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { - for (AnnotationIntrospector intr : ai.allIntrospectors()) { - if (intr instanceof AnnotationIntrospector.XmlExtensions) { - String ns = ((AnnotationIntrospector.XmlExtensions) intr).findNamespace(config, prop); - if (ns != null) { - return ns; + if (prop != null) { + for (AnnotationIntrospector intr : ai.allIntrospectors()) { + if (intr instanceof AnnotationIntrospector.XmlExtensions) { + String ns = ((AnnotationIntrospector.XmlExtensions) intr).findNamespace(config, prop); + if (ns != null) { + return ns; + } } } } @@ -26,13 +36,15 @@ public static Boolean findIsAttributeAnnotation(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { - for (AnnotationIntrospector intr : ai.allIntrospectors()) { - if (intr instanceof AnnotationIntrospector.XmlExtensions) { - Boolean b = ((AnnotationIntrospector.XmlExtensions) intr).isOutputAsAttribute(config, prop); - if (b != null) { - return b; + if (prop != null) { + for (AnnotationIntrospector intr : ai.allIntrospectors()) { + if (intr instanceof AnnotationIntrospector.XmlExtensions) { + Boolean b = ((AnnotationIntrospector.XmlExtensions) intr).isOutputAsAttribute(config, prop); + if (b != null) { + return b; + } } - } + } } return null; } @@ -41,11 +53,13 @@ public static Boolean findIsTextAnnotation(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { - for (AnnotationIntrospector intr : ai.allIntrospectors()) { - if (intr instanceof AnnotationIntrospector.XmlExtensions) { - Boolean b = ((AnnotationIntrospector.XmlExtensions) intr).isOutputAsText(config, prop); - if (b != null) { - return b; + if (prop != null) { + for (AnnotationIntrospector intr : ai.allIntrospectors()) { + if (intr instanceof AnnotationIntrospector.XmlExtensions) { + Boolean b = ((AnnotationIntrospector.XmlExtensions) intr).isOutputAsText(config, prop); + if (b != null) { + return b; + } } } } @@ -56,11 +70,13 @@ public static Boolean findIsCDataAnnotation(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { - for (AnnotationIntrospector intr : ai.allIntrospectors()) { - if (intr instanceof AnnotationIntrospector.XmlExtensions) { - Boolean b = ((AnnotationIntrospector.XmlExtensions) intr).isOutputAsCData(config, prop); - if (b != null) { - return b; + if (prop != null) { + for (AnnotationIntrospector intr : ai.allIntrospectors()) { + if (intr instanceof AnnotationIntrospector.XmlExtensions) { + Boolean b = ((AnnotationIntrospector.XmlExtensions) intr).isOutputAsCData(config, prop); + if (b != null) { + return b; + } } } } @@ -75,11 +91,13 @@ public static PropertyName findXmlPropertyInnerName(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { - for (AnnotationIntrospector intr : ai.allIntrospectors()) { - if (intr instanceof AnnotationIntrospector.XmlExtensions) { - PropertyName name = ((AnnotationIntrospector.XmlExtensions) intr).findXmlPropertyInnerName(config, prop); - if (name != null) { - return name; + if (prop != null) { + for (AnnotationIntrospector intr : ai.allIntrospectors()) { + if (intr instanceof AnnotationIntrospector.XmlExtensions) { + PropertyName name = ((AnnotationIntrospector.XmlExtensions) intr).findXmlPropertyInnerName(config, prop); + if (name != null) { + return name; + } } } } diff --git a/src/test/java/tools/jackson/dataformat/xml/records/RecordIgnoredGetter884Test.java b/src/test/java/tools/jackson/dataformat/xml/records/RecordIgnoredGetter884Test.java new file mode 100644 index 000000000..b09f59070 --- /dev/null +++ b/src/test/java/tools/jackson/dataformat/xml/records/RecordIgnoredGetter884Test.java @@ -0,0 +1,34 @@ +package tools.jackson.dataformat.xml.records; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import tools.jackson.dataformat.xml.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// [dataformat-xml#884]: NPE when Record has `@JsonIgnore`d getter (which leaves +// behind a property definition with no accessors at all) +public class RecordIgnoredGetter884Test extends XmlTestUtil +{ + public interface Id884 { + String name(); + + @JsonIgnore + default String getQualifiedName() { + return "id:" + name(); + } + } + + public record ObjectId884(String name, String repo) implements Id884 { } + + private final XmlMapper MAPPER = newMapper(); + + @Test + public void testIgnoredDefaultGetter() throws Exception { + ObjectId884 input = new ObjectId884("value", "repo"); + String xml = MAPPER.writeValueAsString(input); + assertEquals(input, MAPPER.readValue(xml, ObjectId884.class)); + } +} diff --git a/src/test/java/tools/jackson/dataformat/xml/util/AnnotationUtilTest.java b/src/test/java/tools/jackson/dataformat/xml/util/AnnotationUtilTest.java new file mode 100644 index 000000000..1595ed08a --- /dev/null +++ b/src/test/java/tools/jackson/dataformat/xml/util/AnnotationUtilTest.java @@ -0,0 +1,27 @@ +package tools.jackson.dataformat.xml.util; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.AnnotationIntrospector; +import tools.jackson.databind.cfg.MapperConfig; +import tools.jackson.dataformat.xml.XmlMapper; +import tools.jackson.dataformat.xml.XmlTestUtil; + +import static org.junit.jupiter.api.Assertions.assertNull; + +public class AnnotationUtilTest extends XmlTestUtil +{ + // [dataformat-xml#884]: property definitions without any accessors do occur, + // and have no annotations to find + @Test + public void testNullMemberAccepted() throws Exception { + MapperConfig config = new XmlMapper().serializationConfig(); + AnnotationIntrospector intr = config.getAnnotationIntrospector(); + + assertNull(AnnotationUtil.findNamespaceAnnotation(config, intr, null)); + assertNull(AnnotationUtil.findIsAttributeAnnotation(config, intr, null)); + assertNull(AnnotationUtil.findIsTextAnnotation(config, intr, null)); + assertNull(AnnotationUtil.findIsCDataAnnotation(config, intr, null)); + assertNull(AnnotationUtil.findXmlPropertyInnerName(config, intr, null)); + } +}