diff --git a/Changelog.md b/Changelog.md
index 891ec1d46..89c40eff5 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -17,6 +17,7 @@ Fixes:
- Fix handling of empty `@XmlValue` members of string-like type. Also
collapse whitespace when parsing non-string primitives (per xml
schema). Strings never ignore whitespace.
+- Fix handling `XmlValue` members of collection type inside an empty tag.
- Fix parsing of `XmlDefault` attributes if the (effective) type is an
attribute and it is parsed using as serializable value (rather than)
directly as primitive.
diff --git a/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt b/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt
index 5476c22fc..cafd8bb57 100644
--- a/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt
+++ b/serialization/src/commonMain/kotlin/nl/adaptivity/xmlutil/serialization/XMLDecoder.kt
@@ -1014,9 +1014,13 @@ internal open class XmlDecoderBase internal constructor(
// Handle the case of an empty tag for a value child. This is not a nullable item (so shouldn't be
// treated as such).
if (valueChild >= 0 && input.peek() is XmlEvent.EndElementEvent && !seenItems[valueChild]) {
- // This code can rely on seenItems to avoid infinite item loops as it only triggers on an empty tag.
- seenItems[valueChild] = true
- return valueChild
+ val valueChildDesc = xmlDescriptor.getElementDescriptor(valueChild)
+ // Lists/maps need to be empty (treated as null/missing)
+ if (valueChildDesc.kind !is StructureKind.LIST && valueChildDesc.kind !is StructureKind.MAP) {
+ // This code can rely on seenItems to avoid infinite item loops as it only triggers on an empty tag.
+ seenItems[valueChild] = true
+ return valueChild
+ }
}
for (eventType in input) {
when (eventType) {
diff --git a/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/EmptyTagWithValueChild.kt b/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/EmptyTagWithValueChild.kt
new file mode 100644
index 000000000..924711585
--- /dev/null
+++ b/serialization/src/commonTest/kotlin/nl/adaptivity/xml/serialization/regressions/EmptyTagWithValueChild.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2024.
+ *
+ * This file is part of xmlutil.
+ *
+ * This file is licenced to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You should have received a copy of the license with the source distribution.
+ * Alternatively, you may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package nl.adaptivity.xml.serialization.regressions
+
+import io.github.pdvrieze.xmlutil.testutil.assertXmlEquals
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import nl.adaptivity.xmlutil.serialization.XML
+import nl.adaptivity.xmlutil.serialization.XmlValue
+import nl.adaptivity.xmlutil.util.CompactFragment
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class EmptyTagWithValueChild {
+
+ @Test
+ fun testSerializeStr() {
+ val actual = XML.encodeToString(OuterStr(InnerStr("")))
+ assertXmlEquals("", actual)
+ }
+
+
+ @Test
+ fun testSerializeCF() {
+ val actual = XML.encodeToString(OuterFrag(InnerFrag(emptyList())))
+ assertXmlEquals("", actual)
+ }
+
+ @Test
+ fun testDeserializeCF() {
+ val expected = OuterFrag(InnerFrag(emptyList()))
+ val actual = XML.decodeFromString("")
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun testDeserializeStr() {
+ val expected = OuterStr(InnerStr(""))
+ val actual = XML.decodeFromString("")
+ assertEquals(expected, actual)
+ }
+
+
+ @Serializable
+ @SerialName("Outer")
+ private data class OuterStr(val inner: InnerStr)
+
+ @Serializable
+ @SerialName("Inner")
+ private data class InnerStr(@XmlValue val value: String)
+
+ @Serializable
+ @SerialName("Outer")
+ private data class OuterFrag(val inner: InnerFrag)
+
+ @Serializable
+ @SerialName("Inner")
+ private data class InnerFrag(@XmlValue val values: List)
+}