Fix XmlSerializer consuming sibling elements when deserializing empty XmlElement member#126769
Fix XmlSerializer consuming sibling elements when deserializing empty XmlElement member#126769
Conversation
…rectly Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/a1d8fdd4-0c08-49e8-9258-3018c90b29f1 Co-authored-by: StephenMolloy <19562826+StephenMolloy@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes an XmlSerializer deserialization bug where a self-closing/empty element mapped to an XmlElement/XmlNode member could incorrectly consume the next sibling element as its content, causing subsequent members to deserialize as null.
Changes:
- Update
XmlSerializationReader.ReadXmlNode(bool wrapped)to treat wrapped empty elements as having no content (skip and returnnull) rather than advancing into the next sibling. - Add a regression test in
XmlSerializerTests.RuntimeOnly.cscovering non-empty and empty element forms. - Add a new test type
TypeWithXmlElementMemberAndSibling.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.cs | Adds TypeWithXmlElementMemberAndSibling test type (but see comment about compilation inclusion). |
| src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs | Adds regression coverage for empty XmlElement member not consuming a sibling. |
| src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs | Fixes wrapped empty-element handling in ReadXmlNode to avoid consuming siblings. |
| [InlineData(@"<TypeWithXmlElementMemberAndSibling xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema""><Description><p>text</p></Description><Name>Test</Name></TypeWithXmlElementMemberAndSibling>", "Test")] | ||
| [InlineData(@"<TypeWithXmlElementMemberAndSibling xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema""><Description /><Name>Test</Name></TypeWithXmlElementMemberAndSibling>", "Test")] | ||
| [InlineData(@"<TypeWithXmlElementMemberAndSibling xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema""><Description/><Name>Test</Name></TypeWithXmlElementMemberAndSibling>", "Test")] | ||
| public static void Xml_XmlElementMember_EmptyElement_SiblingNotConsumed(string xml, string expectedName) | ||
| { | ||
| var serializer = new XmlSerializer(typeof(TypeWithXmlElementMemberAndSibling)); | ||
| TypeWithXmlElementMemberAndSibling obj = (TypeWithXmlElementMemberAndSibling)serializer.Deserialize(new StringReader(xml)); | ||
| Assert.Equal(expectedName, obj.Name); |
There was a problem hiding this comment.
This test only asserts obj.Name, which checks that the sibling wasn’t consumed, but it doesn’t validate the intended XmlElement behavior: for the empty element cases obj.Description should be null, and for the non-empty case it should be non-null (and ideally have expected name/content). Adding those assertions would make the regression coverage tighter.
| [InlineData(@"<TypeWithXmlElementMemberAndSibling xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema""><Description><p>text</p></Description><Name>Test</Name></TypeWithXmlElementMemberAndSibling>", "Test")] | |
| [InlineData(@"<TypeWithXmlElementMemberAndSibling xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema""><Description /><Name>Test</Name></TypeWithXmlElementMemberAndSibling>", "Test")] | |
| [InlineData(@"<TypeWithXmlElementMemberAndSibling xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema""><Description/><Name>Test</Name></TypeWithXmlElementMemberAndSibling>", "Test")] | |
| public static void Xml_XmlElementMember_EmptyElement_SiblingNotConsumed(string xml, string expectedName) | |
| { | |
| var serializer = new XmlSerializer(typeof(TypeWithXmlElementMemberAndSibling)); | |
| TypeWithXmlElementMemberAndSibling obj = (TypeWithXmlElementMemberAndSibling)serializer.Deserialize(new StringReader(xml)); | |
| Assert.Equal(expectedName, obj.Name); | |
| [InlineData(@"<TypeWithXmlElementMemberAndSibling xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema""><Description><p>text</p></Description><Name>Test</Name></TypeWithXmlElementMemberAndSibling>", "Test", true, "Description", "<p>text</p>")] | |
| [InlineData(@"<TypeWithXmlElementMemberAndSibling xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema""><Description /><Name>Test</Name></TypeWithXmlElementMemberAndSibling>", "Test", false, null, null)] | |
| [InlineData(@"<TypeWithXmlElementMemberAndSibling xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema""><Description/><Name>Test</Name></TypeWithXmlElementMemberAndSibling>", "Test", false, null, null)] | |
| public static void Xml_XmlElementMember_EmptyElement_SiblingNotConsumed(string xml, string expectedName, bool expectDescription, string expectedDescriptionName, string expectedDescriptionInnerXml) | |
| { | |
| var serializer = new XmlSerializer(typeof(TypeWithXmlElementMemberAndSibling)); | |
| TypeWithXmlElementMemberAndSibling obj = (TypeWithXmlElementMemberAndSibling)serializer.Deserialize(new StringReader(xml)); | |
| Assert.Equal(expectedName, obj.Name); | |
| if (expectDescription) | |
| { | |
| Assert.NotNull(obj.Description); | |
| Assert.Equal(expectedDescriptionName, obj.Description.Name); | |
| Assert.Equal(expectedDescriptionInnerXml, obj.Description.InnerXml); | |
| } | |
| else | |
| { | |
| Assert.Null(obj.Description); | |
| } |
| { | ||
| var serializer = new XmlSerializer(typeof(TypeWithXmlElementMemberAndSibling)); | ||
| TypeWithXmlElementMemberAndSibling obj = (TypeWithXmlElementMemberAndSibling)serializer.Deserialize(new StringReader(xml)); | ||
| Assert.Equal(expectedName, obj.Name); | ||
| } |
There was a problem hiding this comment.
TypeWithXmlElementMemberAndSibling is defined in System.Runtime.Serialization.Xml/tests/SerializationTypes.cs, but System.Private.Xml.Tests.csproj includes that file as <None> (not <Compile>), so this test likely won’t compile in this project. Consider moving the new type into SerializationTypes.RuntimeOnly.cs (which is compiled here) or changing the csproj to compile SerializationTypes.cs.
Description
When deserializing a class with an
XmlElementmember followed by other members, a self-closing/empty element (e.g.,<Description/>) caused the serializer to consume the next sibling element as theXmlElement's content, leaving subsequent members null.Root Cause
ReadXmlNode(bool wrapped)called_r.ReadStartElement()without first checkingIsEmptyElement. For an empty element like<Description/>,ReadStartElement()advances the reader past the empty element to the next sibling. The subsequentMoveToContent()+NodeType != EndElementcheck then incorrectly read that sibling as child content.Fix
XmlSerializationReader.ReadXmlNode: AddIsEmptyElementguard beforeReadStartElement()— matching the pattern already used inReadNull(). If empty,Skip()and returnnull.TypeWithXmlElementMemberAndSiblingtoSerializationTypes.csand a[Theory]regression test (Xml_XmlElementMember_EmptyElement_SiblingNotConsumed) covering non-empty, space-before-slash, and compact empty element forms.