diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b80f1498f..33311def1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: strategy: fail-fast: false matrix: - java_version: ['17', '21', '24' ] + java_version: ['17', '21', '25' ] include: - java_version: '17' release_build: 'R' diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 5f903ce46..6672a38f0 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -6,61 +6,61 @@ XML format backend (`jackson-dataformat-xml`), version 3.x Tatu Saloranta, tatu.saloranta@iki.fi: author Morten Olav Hansen (mortenoh@github) - -* Reported #25: `ACCEPT_EMPTY_STRING_AS_NULL_OBJECT` not honored in xml module - for attributes - (3.0.0) + * Reported #25: `ACCEPT_EMPTY_STRING_AS_NULL_OBJECT` not honored in xml module + for attributes + (3.0.0) Ghenadii Batalski (@ghenadiibatalski) - -* Reported #793: `XmlSerializationContext`: NPE _writeCapabilities not set for - `SerializationContext` - (3.0.4) + * Reported #793: `XmlSerializationContext`: NPE _writeCapabilities not set for + `SerializationContext` + (3.0.4) PJ Fanning (@pjfanning) - -* Fixed #793: `XmlSerializationContext`: NPE _writeCapabilities not set for - `SerializationContext` - (3.0.4) + * Fixed #793: `XmlSerializationContext`: NPE _writeCapabilities not set for + `SerializationContext` + (3.0.4) Dai MIKURUBE (@dmikurube) - -* Reported #149: `@JacksonXmlElementWrapper` as a `@JsonCreator parameter` not working - (3.2.0) + * Reported #149: `@JacksonXmlElementWrapper` as a `@JsonCreator parameter` not working + (3.2.0) Christopher McVay (@mcvayc) - -* Fixed #149: `@JacksonXmlElementWrapper` as a `@JsonCreator parameter` not working - (3.2.0) -* Fixed #517: XML wrapper doesn't work with java records - (3.2.0) -* Fixed #615: Deserialization of Xml with `@JacksonXmlText` using `@JsonCreator` (into + * Fixed #149: `@JacksonXmlElementWrapper` as a `@JsonCreator parameter` not working + (3.2.0) + * Fixed #517: XML wrapper doesn't work with java records + (3.2.0) + * Fixed #615: Deserialization of Xml with `@JacksonXmlText` using `@JsonCreator` (into `java.util.Map`) fails - (3.2.0) -* Fixed #735: Java Record with `@JacksonXmlText` stopped working with 2.18 - (3.2.0) -* Fixed #795: `HttpHeader` object (= ) is not wrapped by the XML `` tag - (3.2.0) -* Fixed #306: Can not use `@JacksonXmlText` for Creator property (creator parameter) - (3.2.0) + (3.2.0) + * Fixed #735: Java Record with `@JacksonXmlText` stopped working with 2.18 + (3.2.0) + * Fixed #795: `HttpHeader` object (= ) is not wrapped by the XML `` tag + (3.2.0) + * Fixed #306: Can not use `@JacksonXmlText` for Creator property (creator parameter) + (3.2.0) Eduard Wirch (@ewirch) + * Reported #306: Can not use `@JacksonXmlText` for Creator property (creator parameter) + (3.2.0) -* Reported #306: Can not use `@JacksonXmlText` for Creator property (creator parameter) - (3.2.0) +Jiri Mikulasek (@jimirocks) + * Reported #455: Can't deserialize list in JsonSubtype when type property is visible + (3.2.0) Josip Antoliš (@Antolius) + * Reported #517: XML wrapper doesn't work with java records + (3.2.0) -* Reported #517: XML wrapper doesn't work with java records - (3.2.0) +Vitor Pamplona (@vitorpamplona) + * Reported #525: Order of XML Properties trigger different behaviors with + polymorphic nested objects + (3.2.0) Severin Kistler (@kistlers) - -* Reported #615: Deserialization of Xml with `@JacksonXmlText` using `@JsonCreator` (into - `java.util.Map`) fails - (3.2.0) + * Reported #615: Deserialization of Xml with `@JacksonXmlText` using `@JsonCreator` (into + `java.util.Map`) fails + (3.2.0) Charles Moulliard (@cmoulliard) - -* Reported #795: `HttpHeader` object (= ) is not wrapped by the XML `` tag - (3.2.0) + * Reported #795: `HttpHeader` object (= ) is not wrapped by the XML `` tag + (3.2.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index ee51c2323..554c437fc 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -15,9 +15,24 @@ Version: 3.x (for earlier see VERSION-2.x) #306: Can not use `@JacksonXmlText` for Creator property (creator parameter) (reported by Eduard W) (fix by Christopher M) +#426: `InvalidTypeIdException` when parsing XML to POJO containing nested `List<>`, + custom `TypeIdResolver` + (reported by @ankagar) + (fix by @cowtowncoder, w/ Claude code) +#455: Can't deserialize list in JsonSubtype when type property is visible + (reported by Jiri M) + (fix by @cowtowncoder, w/ Claude code) #517: XML wrapper doesn't work with java records (reported by Josip A) (fix by Christopher M) +#525: Order of XML Properties trigger different behaviors with + polymorphic nested objects + (reported by Vitor P) + (fix by @cowtowncoder, w/ Claude code) +#567: First element in unwrapped XML array is ignored during + deserialization to base class + (reported by @tetradon) + (fix by @cowtowncoder, w/ Claude code) #615: Deserialization of Xml with `@JacksonXmlText` using `@JsonCreator` (into `java.util.Map`) fails (reported by Severin K) diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java b/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java index 82e239580..9d108d9c9 100644 --- a/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java +++ b/src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java @@ -290,11 +290,16 @@ public void addVirtualWrapping(Set namesToWrap0, boolean caseInsensitive // problems with Lists-in-Lists properties // 12-May-2020, tatu: But as per [dataformat-xml#86] NOT for root element // (would still like to know why work-around needed ever, but...) + // 15-Mar-2026, tatu: [dataformat-xml#455] Relax the parent-root check when + // we're at an element's PROPERTY_NAME (_mayBeLeaf distinguishes elements + // from attributes). This handles polymorphic type resolution where the type + // deserializer consumed properties before wrapping was set up, so we need + // to wrap the current element retroactively. if (!_streamReadContext.inRoot() - && !_streamReadContext.getParent().inRoot()) { + && (!_streamReadContext.getParent().inRoot() + || (_currToken == JsonToken.PROPERTY_NAME && _mayBeLeaf))) { String name = _xmlTokens.getLocalName(); if ((name != null) && namesToWrap.contains(name)) { -//System.out.println("REPEAT from addVirtualWrapping() for '"+name+"'"); _xmlTokens.repeatStartElement(); } } diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/WrapperHandlingDeserializer.java b/src/main/java/tools/jackson/dataformat/xml/deser/WrapperHandlingDeserializer.java index 645f2062a..b30fc286a 100644 --- a/src/main/java/tools/jackson/dataformat/xml/deser/WrapperHandlingDeserializer.java +++ b/src/main/java/tools/jackson/dataformat/xml/deser/WrapperHandlingDeserializer.java @@ -147,13 +147,23 @@ public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, Typ @SuppressWarnings("resource") protected final void _configureParser(JsonParser p) throws JacksonException { + if (_namesToWrap == null) { + return; + } // 05-Sep-2019, tatu: May get XML parser, except for case where content is // buffered. In that case we may still have access to real parser if we // are lucky (like in [dataformat-xml#242]) - while (p instanceof JsonParserDelegate) { - p = ((JsonParserDelegate) p).delegate(); + // 15-Mar-2026, tatu: [dataformat-xml#455] Check for ElementWrappable at + // each delegation level, not just at the innermost parser. This handles + // the case where XmlTokenBuffer wraps the TokenBuffer.Parser with an + // ElementWrappable delegate during polymorphic type resolution. + while (p instanceof JsonParserDelegate jpd) { + if (p instanceof ElementWrappable) { + break; + } + p = jpd.delegate(); } - if ((p instanceof ElementWrappable) && (_namesToWrap != null)) { + if (p instanceof ElementWrappable ew) { // 03-May-2021, tatu: as per [dataformat-xml#469] there are special // cases where we get String token to represent XML empty element. // If so, need to refrain from adding wrapping as that would @@ -165,17 +175,17 @@ protected final void _configureParser(JsonParser p) throws JacksonException // is consumed during buffering, so need to consider that too // it seems (just hope we are at correct level and not off by one...) || t == JsonToken.PROPERTY_NAME) { - ((ElementWrappable) p).addVirtualWrapping(_namesToWrap, _caseInsensitive); + ew.addVirtualWrapping(_namesToWrap, _caseInsensitive); } } } protected BeanDeserializerBase _verifyDeserType(ValueDeserializer deser) { - if (!(deser instanceof BeanDeserializerBase)) { + if (!(deser instanceof BeanDeserializerBase bdb)) { throw new IllegalArgumentException("Can not change delegate to be of type " +deser.getClass().getName()); } - return (BeanDeserializerBase) deser; + return bdb; } } diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/XmlDeserializationContext.java b/src/main/java/tools/jackson/dataformat/xml/deser/XmlDeserializationContext.java index d6bf0c6a5..efd4f5b81 100644 --- a/src/main/java/tools/jackson/dataformat/xml/deser/XmlDeserializationContext.java +++ b/src/main/java/tools/jackson/dataformat/xml/deser/XmlDeserializationContext.java @@ -10,6 +10,7 @@ import tools.jackson.databind.deser.DeserializationContextExt; import tools.jackson.databind.deser.DeserializerCache; import tools.jackson.databind.deser.DeserializerFactory; +import tools.jackson.databind.util.TokenBuffer; import tools.jackson.dataformat.xml.XmlFactory; /** @@ -81,4 +82,17 @@ public String extractScalarFromObject(JsonParser p, ValueDeserializer deser, } return text; } + + /** + * Override to return XML-aware {@link XmlTokenBuffer} that produces + * parsers implementing {@link ElementWrappable}, allowing virtual wrapping + * to be configured even after content has been buffered (e.g., during + * polymorphic type resolution). + * + * @since 3.2 + */ + @Override + public TokenBuffer bufferForInputBuffering(JsonParser p) { + return XmlTokenBuffer.xmlBufferForInputBuffering(p, this); + } } diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenBuffer.java b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenBuffer.java new file mode 100644 index 000000000..a0f226021 --- /dev/null +++ b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenBuffer.java @@ -0,0 +1,94 @@ +package tools.jackson.dataformat.xml.deser; + +import java.util.Set; + +import tools.jackson.core.JsonParser; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.util.JsonParserDelegate; + +import tools.jackson.databind.util.TokenBuffer; + +/** + * XML-specific {@link TokenBuffer} sub-class that ensures parsers created + * from buffered content implement {@link ElementWrappable}, allowing + * virtual wrapping to be configured on the underlying XML parser even + * when content has been buffered (e.g., during polymorphic type resolution). + * + * @since 3.2 + */ +public class XmlTokenBuffer extends TokenBuffer +{ + /** + * Reference to the original XML parser that implements {@link ElementWrappable}, + * if one was found when this buffer was created. + */ + protected final ElementWrappable _wrappableParser; + + protected XmlTokenBuffer(JsonParser p, ObjectReadContext ctxt) + { + super(p, ctxt); + // Find the ElementWrappable parser by unwrapping delegates + JsonParser unwrapped = p; + while (unwrapped instanceof JsonParserDelegate del) { + unwrapped = del.delegate(); + } + _wrappableParser = (unwrapped instanceof ElementWrappable ew) ? ew : null; + } + + public static XmlTokenBuffer xmlBufferForInputBuffering(JsonParser p, + ObjectReadContext ctxt) { + return new XmlTokenBuffer(p, ctxt); + } + + /* + /********************************************************************** + /* Parser construction overrides + /********************************************************************** + */ + + @Override + public JsonParser asParser(ObjectReadContext readCtxt) + { + return _wrapIfNeeded(super.asParser(readCtxt)); + } + + @Override + public JsonParser asParser(ObjectReadContext readCtxt, JsonParser p0) + { + return _wrapIfNeeded(super.asParser(readCtxt, p0)); + } + + protected JsonParser _wrapIfNeeded(JsonParser p) { + return (_wrappableParser == null) ? p + : new ElementWrappableParser(p, _wrappableParser); + } + + /* + /********************************************************************** + /* Helper classes + /********************************************************************** + */ + + /** + * A thin parser delegate that implements {@link ElementWrappable} by + * forwarding wrapping configuration to the original XML parser. + * This allows {@link WrapperHandlingDeserializer} to find and configure + * virtual wrapping even when the active parser is reading from a + * {@link TokenBuffer}. + */ + static class ElementWrappableParser extends JsonParserDelegate + implements ElementWrappable + { + protected final ElementWrappable _wrappable; + + ElementWrappableParser(JsonParser delegate, ElementWrappable wrappable) { + super(delegate); + _wrappable = wrappable; + } + + @Override + public void addVirtualWrapping(Set namesToWrap, boolean caseInsensitive) { + _wrappable.addVirtualWrapping(namesToWrap, caseInsensitive); + } + } +} diff --git a/src/test/java/tools/jackson/dataformat/xml/tofix/TypeInfoOrder525Test.java b/src/test/java/tools/jackson/dataformat/xml/deser/TypeInfoOrder525Test.java similarity index 93% rename from src/test/java/tools/jackson/dataformat/xml/tofix/TypeInfoOrder525Test.java rename to src/test/java/tools/jackson/dataformat/xml/deser/TypeInfoOrder525Test.java index 5e44627d4..68e3dd51f 100644 --- a/src/test/java/tools/jackson/dataformat/xml/tofix/TypeInfoOrder525Test.java +++ b/src/test/java/tools/jackson/dataformat/xml/deser/TypeInfoOrder525Test.java @@ -1,4 +1,4 @@ -package tools.jackson.dataformat.xml.tofix; +package tools.jackson.dataformat.xml.deser; import java.util.List; @@ -9,8 +9,6 @@ import tools.jackson.databind.ObjectMapper; import tools.jackson.dataformat.xml.XmlTestUtil; -import tools.jackson.dataformat.xml.testutil.failure.JacksonTestFailureExpected; - import static org.junit.jupiter.api.Assertions.assertEquals; // Tests for [dataformat-xml#525], related to relative order of "type" @@ -49,7 +47,7 @@ static class BindingInfo525 { .defaultUseWrapper(false) .build(); - @JacksonTestFailureExpected + // [dataformat-xml#525] @Test public void testTypeAfterOtherProperties() throws Exception { String xml = diff --git a/src/test/java/tools/jackson/dataformat/xml/tofix/PolymorphicList426Test.java b/src/test/java/tools/jackson/dataformat/xml/lists/PolymorphicList426Test.java similarity index 96% rename from src/test/java/tools/jackson/dataformat/xml/tofix/PolymorphicList426Test.java rename to src/test/java/tools/jackson/dataformat/xml/lists/PolymorphicList426Test.java index 9a3421506..47550a0b7 100644 --- a/src/test/java/tools/jackson/dataformat/xml/tofix/PolymorphicList426Test.java +++ b/src/test/java/tools/jackson/dataformat/xml/lists/PolymorphicList426Test.java @@ -1,4 +1,4 @@ -package tools.jackson.dataformat.xml.tofix; +package tools.jackson.dataformat.xml.lists; import java.util.List; @@ -13,8 +13,6 @@ import tools.jackson.dataformat.xml.XmlTestUtil; import tools.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import tools.jackson.dataformat.xml.annotation.JacksonXmlProperty; -import tools.jackson.dataformat.xml.testutil.failure.JacksonTestFailureExpected; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -91,7 +89,6 @@ public JsonTypeInfo.Id getMechanism() { private final ObjectMapper MAPPER = newMapper(); // [dataformat-xml#426] - @JacksonTestFailureExpected @Test public void testPolymorphicList426() throws Exception { diff --git a/src/test/java/tools/jackson/dataformat/xml/tofix/PolymorphicList576Test.java b/src/test/java/tools/jackson/dataformat/xml/lists/PolymorphicList567Test.java similarity index 92% rename from src/test/java/tools/jackson/dataformat/xml/tofix/PolymorphicList576Test.java rename to src/test/java/tools/jackson/dataformat/xml/lists/PolymorphicList567Test.java index 4e6ecc082..226082564 100644 --- a/src/test/java/tools/jackson/dataformat/xml/tofix/PolymorphicList576Test.java +++ b/src/test/java/tools/jackson/dataformat/xml/lists/PolymorphicList567Test.java @@ -1,4 +1,4 @@ -package tools.jackson.dataformat.xml.tofix; +package tools.jackson.dataformat.xml.lists; import java.util.*; @@ -10,11 +10,10 @@ import tools.jackson.dataformat.xml.XmlTestUtil; import tools.jackson.dataformat.xml.annotation.*; -import tools.jackson.dataformat.xml.testutil.failure.JacksonTestFailureExpected; - import static org.junit.jupiter.api.Assertions.assertEquals; -public class PolymorphicList576Test extends XmlTestUtil +// For [dataformat-xml#567] +public class PolymorphicList567Test extends XmlTestUtil { @JsonRootName("wrapper") static class Wrapper extends Base { @@ -38,8 +37,6 @@ public void setItems(List items) { this.items = items; } - - @Override public String toString() { return "Wrapper{" + @@ -93,7 +90,6 @@ static class Base { private final ObjectMapper XML_MAPPER = newMapper(); - @JacksonTestFailureExpected @Test public void test_3itemsInXml_expect_3itemsInDeserializedObject() throws Exception { String xmlString = @@ -108,7 +104,6 @@ public void test_3itemsInXml_expect_3itemsInDeserializedObject() throws Exceptio assertEquals(3, ((Wrapper)base).getItems().size()); } - @JacksonTestFailureExpected @Test public void test_2itemsInObject_expect_2itemsInObjectAfterRoundTripDeserializationToBaseClass() throws Exception { Wrapper wrapper = new Wrapper();