From 32e293cb9a56c8f1c036d30babbd8ffbff691b52 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 24 Jul 2026 09:24:26 -0700 Subject: [PATCH 1/4] Fix #884 (NPE for annotations) --- release-notes/CREDITS | 5 +++ release-notes/VERSION | 7 ++++ .../xml/deser/XmlValueInstantiators.java | 7 ++++ .../dataformat/xml/util/AnnotationUtil.java | 23 +++++++++++++ .../records/RecordIgnoredGetter884Test.java | 34 +++++++++++++++++++ .../xml/util/AnnotationUtilTest.java | 27 +++++++++++++++ 6 files changed, 103 insertions(+) create mode 100644 src/test/java/tools/jackson/dataformat/xml/records/RecordIgnoredGetter884Test.java create mode 100644 src/test/java/tools/jackson/dataformat/xml/util/AnnotationUtilTest.java diff --git a/release-notes/CREDITS b/release-notes/CREDITS index d60e231e7..279ef428a 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -152,3 +152,8 @@ 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) + +@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..777897c9c 100644 --- a/src/main/java/tools/jackson/dataformat/xml/util/AnnotationUtil.java +++ b/src/main/java/tools/jackson/dataformat/xml/util/AnnotationUtil.java @@ -5,12 +5,23 @@ 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) { + if (prop == null) { + return null; + } for (AnnotationIntrospector intr : ai.allIntrospectors()) { if (intr instanceof AnnotationIntrospector.XmlExtensions) { String ns = ((AnnotationIntrospector.XmlExtensions) intr).findNamespace(config, prop); @@ -26,6 +37,9 @@ public static Boolean findIsAttributeAnnotation(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { + if (prop == null) { + return null; + } for (AnnotationIntrospector intr : ai.allIntrospectors()) { if (intr instanceof AnnotationIntrospector.XmlExtensions) { Boolean b = ((AnnotationIntrospector.XmlExtensions) intr).isOutputAsAttribute(config, prop); @@ -41,6 +55,9 @@ public static Boolean findIsTextAnnotation(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { + if (prop == null) { + return null; + } for (AnnotationIntrospector intr : ai.allIntrospectors()) { if (intr instanceof AnnotationIntrospector.XmlExtensions) { Boolean b = ((AnnotationIntrospector.XmlExtensions) intr).isOutputAsText(config, prop); @@ -56,6 +73,9 @@ public static Boolean findIsCDataAnnotation(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { + if (prop == null) { + return null; + } for (AnnotationIntrospector intr : ai.allIntrospectors()) { if (intr instanceof AnnotationIntrospector.XmlExtensions) { Boolean b = ((AnnotationIntrospector.XmlExtensions) intr).isOutputAsCData(config, prop); @@ -75,6 +95,9 @@ public static PropertyName findXmlPropertyInnerName(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { + if (prop == null) { + return null; + } for (AnnotationIntrospector intr : ai.allIntrospectors()) { if (intr instanceof AnnotationIntrospector.XmlExtensions) { PropertyName name = ((AnnotationIntrospector.XmlExtensions) intr).findXmlPropertyInnerName(config, prop); 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)); + } +} From 13299760d614ff4ae984c9404429bb0c51b25070 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 24 Jul 2026 09:27:49 -0700 Subject: [PATCH 2/4] Fix release notes --- release-notes/CREDITS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 279ef428a..d0a2af212 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -151,7 +151,7 @@ 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 From 72b1c2707a05bea5c94cc264be026e2e6f7e2b73 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 24 Jul 2026 09:30:56 -0700 Subject: [PATCH 3/4] Minor streamlining --- .../dataformat/xml/util/AnnotationUtil.java | 64 +++++++++---------- 1 file changed, 30 insertions(+), 34 deletions(-) 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 777897c9c..98c8c5d73 100644 --- a/src/main/java/tools/jackson/dataformat/xml/util/AnnotationUtil.java +++ b/src/main/java/tools/jackson/dataformat/xml/util/AnnotationUtil.java @@ -37,16 +37,15 @@ public static Boolean findIsAttributeAnnotation(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { - if (prop == null) { - return null; - } - 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; } @@ -55,14 +54,13 @@ public static Boolean findIsTextAnnotation(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { - if (prop == null) { - return null; - } - 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; + } } } } @@ -73,14 +71,13 @@ public static Boolean findIsCDataAnnotation(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { - if (prop == null) { - return null; - } - 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; + } } } } @@ -95,14 +92,13 @@ public static PropertyName findXmlPropertyInnerName(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { - if (prop == null) { - return null; - } - 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; + } } } } From 131cae060517c56a9b84b5a1c60ab1012ba12371 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 24 Jul 2026 09:32:58 -0700 Subject: [PATCH 4/4] ... --- .../dataformat/xml/util/AnnotationUtil.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) 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 98c8c5d73..04da2178b 100644 --- a/src/main/java/tools/jackson/dataformat/xml/util/AnnotationUtil.java +++ b/src/main/java/tools/jackson/dataformat/xml/util/AnnotationUtil.java @@ -19,14 +19,13 @@ public static String findNamespaceAnnotation(MapperConfig config, AnnotationIntrospector ai, AnnotatedMember prop) { - if (prop == null) { - return null; - } - 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; + } } } } @@ -44,7 +43,7 @@ public static Boolean findIsAttributeAnnotation(MapperConfig config, if (b != null) { return b; } - } + } } } return null;