Skip to content
Merged
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
7 changes: 6 additions & 1 deletion release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -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)
7 changes: 7 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ private Map<String, String> _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;

Expand Down
70 changes: 44 additions & 26 deletions src/main/java/tools/jackson/dataformat/xml/util/AnnotationUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*<p>
* 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;
}
}
}
}
Expand All @@ -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;
}
Expand All @@ -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;
}
}
}
}
Expand All @@ -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;
}
}
}
}
Expand All @@ -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;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}