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) +}