diff --git a/README.md b/README.md index 3b85745c..87187392 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,7 @@ Currently, following limitations exist beyond general Jackson (JSON) limitations * Note: over time some level of support has been added, and `Collection`s, for example, often work. * Lists and arrays are "wrapped" by default, when using Jackson annotations, but unwrapped when using JAXB annotations (if supported, see below) * `@JacksonXmlElementWrapper.useWrapping` can be set to 'false' to disable wrapping - * `JacksonXmlModule.setDefaultUseWrapper()` can be used to specify whether "wrapped" or "unwrapped" setting is the default + * `XmlMapper.builder().defaultUseWrapper()` can be used to specify whether "wrapped" or "unwrapped" setting is the default * Polymorphic Type Handling works, but only some inclusion mechanisms are supported (`WRAPPER_ARRAY`, for example is not supported due to problems with reference to mapping of XML, Arrays) * JAXB-style "compact" Type Id where property name is replaced with Type Id is not supported. * Mixed Content (elements and text in same element) is not supported in databinding: child content must be either text OR element(s) (attributes are fine) diff --git a/release-notes/CREDITS b/release-notes/CREDITS index c6457589..c0f60c0a 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -212,6 +212,9 @@ Christian Beikov (@beikov) * Fixed #911: Verify Stax factory type before instantiating in `XmlFactory.readResolve()` (3.1.7) + * Fixed #913: `XmlMapper.Builder.defaultUseWrapper()` changes mapper that + builder was created from (via `rebuild()`), or has already built + (3.3.0) * Fixed #914: `XmlFactoryBuilder` `enable()`/`disable()`/`configure()` for `XmlReadFeature`, `XmlWriteFeature` have no effect on `XmlFactory` built (3.1.7) diff --git a/release-notes/VERSION b/release-notes/VERSION index c98588b9..cb38a76c 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -47,6 +47,9 @@ Version: 3.x (for earlier see VERSION-2.x) #909: Clear forced `xsi:type` attribute state in `ToXmlGenerator.writeName()` (`Map`/`JsonNode` key `xsi:type` leaked attribute mode onto following siblings) (fix by @Sahana2524) +#913: `XmlMapper.Builder.defaultUseWrapper()` changes mapper that builder was + created from (via `rebuild()`), or has already built + (fix by @Sahana2524) 3.2.3 (21-Sep-2026) diff --git a/src/main/java/tools/jackson/dataformat/xml/JacksonXmlAnnotationIntrospector.java b/src/main/java/tools/jackson/dataformat/xml/JacksonXmlAnnotationIntrospector.java index 397c6217..196d56ae 100644 --- a/src/main/java/tools/jackson/dataformat/xml/JacksonXmlAnnotationIntrospector.java +++ b/src/main/java/tools/jackson/dataformat/xml/JacksonXmlAnnotationIntrospector.java @@ -7,6 +7,7 @@ import tools.jackson.databind.PropertyName; import tools.jackson.databind.cfg.MapperConfig; import tools.jackson.databind.introspect.*; +import tools.jackson.databind.util.ClassUtil; import tools.jackson.dataformat.xml.annotation.*; /** @@ -55,16 +56,54 @@ public JacksonXmlAnnotationIntrospector(boolean defaultUseWrapper) { _cfgDefaultUseWrapper = defaultUseWrapper; } + /** + * Copy constructor for sub-classes to use when overriding + * {@link #withDefaultUseWrapper}: copies settings of {@code src} other + * than default for List wrapping, which is set to given value. + * + * @since 3.3 + */ + protected JacksonXmlAnnotationIntrospector(JacksonXmlAnnotationIntrospector src, + boolean defaultUseWrapper) + { + super(src); + _cfgDefaultUseWrapper = defaultUseWrapper; + } + /* /********************************************************************** /* Extended API XML format module requires /********************************************************************** */ + /** + * @deprecated Since 3.3 use {@link #withDefaultUseWrapper} instead: modifying + * an introspector in place also affects any mapper that is already using it + * (including ones created via {@code XmlMapper.rebuild()}) + */ + @Deprecated // since 3.3 public void setDefaultUseWrapper(boolean b) { _cfgDefaultUseWrapper = b; } + /** + * Sub-classes MUST override this method (usually using copy constructor + * {@link #JacksonXmlAnnotationIntrospector(JacksonXmlAnnotationIntrospector, boolean)}) + * to retain their type and settings; otherwise an {@link IllegalStateException} + * is thrown when a re-configured copy would be needed. + * + * @since 3.3 + */ + @Override + public JacksonXmlAnnotationIntrospector withDefaultUseWrapper(boolean b) { + if (_cfgDefaultUseWrapper == b) { + return this; + } + ClassUtil.verifyMustOverride(JacksonXmlAnnotationIntrospector.class, this, + "withDefaultUseWrapper"); + return new JacksonXmlAnnotationIntrospector(this, b); + } + /* /********************************************************************** /* Overrides of JacksonAnnotationIntrospector impls diff --git a/src/main/java/tools/jackson/dataformat/xml/XmlAnnotationIntrospector.java b/src/main/java/tools/jackson/dataformat/xml/XmlAnnotationIntrospector.java index 7c8f9d82..1e469e7c 100644 --- a/src/main/java/tools/jackson/dataformat/xml/XmlAnnotationIntrospector.java +++ b/src/main/java/tools/jackson/dataformat/xml/XmlAnnotationIntrospector.java @@ -5,6 +5,7 @@ import tools.jackson.databind.cfg.MapperConfig; import tools.jackson.databind.introspect.Annotated; import tools.jackson.databind.introspect.AnnotationIntrospectorPair; +import tools.jackson.databind.util.ClassUtil; /** * Additional extension interface used above and beyond @@ -13,6 +14,32 @@ public interface XmlAnnotationIntrospector extends AnnotationIntrospector.XmlExtensions { + /** + * Mutant factory for getting an introspector that uses given default for + * List wrapping (for Lists and arrays without explicit wrapper annotation): + * returns this instance if there is no change (or no such setting), a + * re-configured copy otherwise. Must never modify this instance, as an + * introspector may be shared by multiple (immutable) mappers; see + * {@code XmlMapper.rebuild()}. + *
+ * Default implementation returns {@code this}, for introspectors that have + * no such setting. + *
+ * NOTE: implementations are expected to extend {@link AnnotationIntrospector},
+ * and value returned MUST also be an {@link AnnotationIntrospector} (since it
+ * is used as the replacement introspector by
+ * {@code XmlMapper.Builder.defaultUseWrapper()}).
+ *
+ * @param defaultUseWrapper Whether to use wrapping by default or not
+ *
+ * @return Introspector that uses given default for wrapping
+ *
+ * @since 3.3
+ */
+ default XmlAnnotationIntrospector withDefaultUseWrapper(boolean defaultUseWrapper) {
+ return this;
+ }
+
/*
/**********************************************************************
/* Replacement of 'AnnotationIntrospector.Pair' to use when combining
@@ -51,6 +78,35 @@ public Pair(AnnotationIntrospector p, AnnotationIntrospector s)
public static XmlAnnotationIntrospector.Pair instance(AnnotationIntrospector a1, AnnotationIntrospector a2) {
return new XmlAnnotationIntrospector.Pair(a1, a2);
}
+
+ /**
+ * Sub-classes MUST override this method to retain their type; otherwise
+ * an {@link IllegalStateException} is thrown when a re-configured copy
+ * would be needed.
+ *
+ * @since 3.3
+ */
+ @Override
+ public XmlAnnotationIntrospector withDefaultUseWrapper(boolean defaultUseWrapper)
+ {
+ AnnotationIntrospector p = _withDefaultUseWrapper(_primary, defaultUseWrapper);
+ AnnotationIntrospector s = _withDefaultUseWrapper(_secondary, defaultUseWrapper);
+ if ((p == _primary) && (s == _secondary)) {
+ return this;
+ }
+ ClassUtil.verifyMustOverride(XmlAnnotationIntrospector.Pair.class, this,
+ "withDefaultUseWrapper");
+ return new XmlAnnotationIntrospector.Pair(p, s);
+ }
+
+ protected static AnnotationIntrospector _withDefaultUseWrapper(AnnotationIntrospector ai,
+ boolean defaultUseWrapper)
+ {
+ if (ai instanceof XmlAnnotationIntrospector xmlAi) {
+ return (AnnotationIntrospector) xmlAi.withDefaultUseWrapper(defaultUseWrapper);
+ }
+ return ai;
+ }
@Override
public String findNamespace(MapperConfig> config, Annotated ann)
diff --git a/src/main/java/tools/jackson/dataformat/xml/XmlMapper.java b/src/main/java/tools/jackson/dataformat/xml/XmlMapper.java
index 25f6bccb..dd55d91f 100644
--- a/src/main/java/tools/jackson/dataformat/xml/XmlMapper.java
+++ b/src/main/java/tools/jackson/dataformat/xml/XmlMapper.java
@@ -276,16 +276,33 @@ public boolean defaultUseWrapper() {
* Jackson annotations have different default due to backwards compatibility.
*/
public Builder defaultUseWrapper(boolean b) {
- if (_defaultUseWrapper != b) {
- _defaultUseWrapper = b;
-
- AnnotationIntrospector ai0 = annotationIntrospector();
- for (AnnotationIntrospector ai : ai0.allIntrospectors()) {
- if (ai instanceof JacksonXmlAnnotationIntrospector xmlAi) {
- xmlAi.setDefaultUseWrapper(b);
- }
+ // Introspector may be shared with the mapper this builder was created
+ // from (see `XmlMapper.rebuild()`), as well as with mappers it has
+ // already built: so must not modify it in place but swap in
+ // re-configured copy (same as `nameForTextElement()` does for factory).
+ // NOTE: done even if builder setting is unchanged, since introspector
+ // may have been replaced with one that uses a different setting
+ AnnotationIntrospector ai = annotationIntrospector();
+ AnnotationIntrospector newAi = (ai instanceof XmlAnnotationIntrospector xmlAi)
+ ? (AnnotationIntrospector) xmlAi.withDefaultUseWrapper(b)
+ : ai;
+ // But not all introspectors can be re-configured (without changing
+ // structure), like ones within databind-provided `AnnotationIntrospectorPair`:
+ // must fail if any of those would still need change
+ for (AnnotationIntrospector curr : newAi.allIntrospectors()) {
+ if ((curr instanceof XmlAnnotationIntrospector xmlAi)
+ && (xmlAi.withDefaultUseWrapper(b) != xmlAi)) {
+ throw new IllegalStateException(String.format(
+"Cannot change `defaultUseWrapper` of `%s`: it is contained in a pair other than `%s` (like databind `AnnotationIntrospectorPair`); combine all introspectors using `%s` instead",
+ curr.getClass().getName(),
+ XmlAnnotationIntrospector.Pair.class.getName(),
+ XmlAnnotationIntrospector.Pair.class.getName()));
}
}
+ if (newAi != ai) {
+ annotationIntrospector(newAi);
+ }
+ _defaultUseWrapper = b;
return this;
}
diff --git a/src/test/java/tools/jackson/dataformat/xml/MapperCopyTest.java b/src/test/java/tools/jackson/dataformat/xml/MapperCopyTest.java
index 86a0c507..e88a610f 100644
--- a/src/test/java/tools/jackson/dataformat/xml/MapperCopyTest.java
+++ b/src/test/java/tools/jackson/dataformat/xml/MapperCopyTest.java
@@ -1,12 +1,14 @@
package tools.jackson.dataformat.xml;
import java.io.*;
+import java.util.*;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonRootName;
import tools.jackson.databind.*;
+import tools.jackson.databind.introspect.AnnotationIntrospectorPair;
import static org.junit.jupiter.api.Assertions.*;
@@ -18,6 +20,53 @@ static class Pojo282
public int a = 3;
}
+ // [dataformat-xml#913]: separate types per test so that no cached
+ // (de)serializer can hide effects of introspector changes
+ static class ListBean913A {
+ public List