Skip to content

Fix XmlSerializer consuming sibling elements when deserializing empty XmlElement member#126769

Open
Copilot wants to merge 2 commits intomainfrom
copilot/fix-xml-deserialize-empty-element
Open

Fix XmlSerializer consuming sibling elements when deserializing empty XmlElement member#126769
Copilot wants to merge 2 commits intomainfrom
copilot/fix-xml-deserialize-empty-element

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 10, 2026

Description

When deserializing a class with an XmlElement member followed by other members, a self-closing/empty element (e.g., <Description/>) caused the serializer to consume the next sibling element as the XmlElement's content, leaving subsequent members null.

public class Root {
    public XmlElement Description { get; set; }
    public string Name { get; set; }
}

// Bug: obj.Description = <Name>Test</Name>, obj.Name = null
var obj = serializer.Deserialize(new StringReader(
    @"<Root><Description/><Name>Test</Name></Root>"));

// Expected: obj.Description = null, obj.Name = "Test"

Root Cause

ReadXmlNode(bool wrapped) called _r.ReadStartElement() without first checking IsEmptyElement. For an empty element like <Description/>, ReadStartElement() advances the reader past the empty element to the next sibling. The subsequent MoveToContent() + NodeType != EndElement check then incorrectly read that sibling as child content.

Fix

  • XmlSerializationReader.ReadXmlNode: Add IsEmptyElement guard before ReadStartElement() — matching the pattern already used in ReadNull(). If empty, Skip() and return null.
if (_r.IsEmptyElement)
{
    _r.Skip();
    return null;
}
_r.ReadStartElement();
  • Test: Added TypeWithXmlElementMemberAndSibling to SerializationTypes.cs and a [Theory] regression test (Xml_XmlElementMember_EmptyElement_SiblingNotConsumed) covering non-empty, space-before-slash, and compact empty element forms.

Copilot AI requested review from Copilot and removed request for Copilot April 10, 2026 21:40
Copilot AI linked an issue Apr 10, 2026 that may be closed by this pull request
…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>
Copilot AI requested review from Copilot and removed request for Copilot April 10, 2026 22:30
Copilot AI changed the title [WIP] Fix XML deserialization for empty self-closing elements Fix XmlSerializer consuming sibling elements when deserializing empty XmlElement member Apr 10, 2026
Copilot AI requested a review from StephenMolloy April 10, 2026 22:31
@StephenMolloy StephenMolloy marked this pull request as ready for review April 11, 2026 06:54
Copilot AI review requested due to automatic review settings April 11, 2026 06:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 return null) rather than advancing into the next sibling.
  • Add a regression test in XmlSerializerTests.RuntimeOnly.cs covering 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.

Comment on lines +3671 to +3678
[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);
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
[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);
}

Copilot uses AI. Check for mistakes.
Comment on lines +3675 to +3679
{
var serializer = new XmlSerializer(typeof(TypeWithXmlElementMemberAndSibling));
TypeWithXmlElementMemberAndSibling obj = (TypeWithXmlElementMemberAndSibling)serializer.Deserialize(new StringReader(xml));
Assert.Equal(expectedName, obj.Name);
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XML deserialize raw empty-element

3 participants