Background and motivation
XmlSerializer currently has inconsistent and non-configurable behavior when serializing array-like members as XML text versus XML attributes.
For [XmlAttribute] string[], XmlSerializer serializes values as a whitespace-separated list:
<element values="a b c d" />
For [XmlText] string[], XmlSerializer serializes values by concatenating them with no separator:
This behavior is longstanding and changing the default would be breaking. However, it makes it difficult to model XML Schema xs:list-style element text values, where users often expect list items to be serialized with a separator, commonly a space.
Today, users must work around this by hiding the actual collection from XmlSerializer and exposing a shadow string property that manually joins and splits the values:
[XmlIgnore]
public string[] Items { get; set; }
[XmlText]
public string ItemsSerialized
{
get => string.Join(" ", Items);
set => Items = value?.Split(' ') ?? Array.Empty<string>();
}
This workaround is repetitive, error-prone, and requires users to change their object model purely for serialization purposes.
This proposal adds an opt-in separator property to the existing XML serialization attributes so users can preserve existing behavior by default while configuring list serialization when needed.
Related issue: dotnet/runtime#115837
Prototype PR: dotnet/runtime#126767
API Proposal
namespace System.Xml.Serialization;
public class XmlTextAttribute : Attribute
{
public char Separator { get; set; }
}
public class XmlAttributeAttribute : Attribute
{
public char Separator { get; set; }
}
API Usage
XmlTextAttribute.Separator
XmlTextAttribute.Separator controls how array-like XML text values are serialized and deserialized.
The default value is '\0'.
When Separator == '\0', existing behavior is preserved:
public class Example
{
[XmlText]
public string[] Items { get; set; }
}
Serializing:
new Example { Items = new[] { "a", "b", "c" } }
continues to produce:
Deserialization likewise preserves existing behavior: the full text content is treated as one string value.
When Separator is set to a non-default character, adjacent string values are separated using that character during serialization, and text content is split on that character during deserialization:
public class Example
{
[XmlText(Separator = ' ')]
public string[] Items { get; set; }
}
Serializing:
new Example { Items = new[] { "a", "b", "c" } }
produces:
Deserializing:
produces:
Items = new[] { "a", "b", "c" };
A non-whitespace separator can also be used:
public class Example
{
[XmlText(Separator = ',')]
public string[] Items { get; set; }
}
XmlAttributeAttribute.Separator
XmlAttributeAttribute.Separator controls how array-like XML attribute values are serialized and deserialized.
The default value is '\0'.
When Separator == '\0', existing behavior is preserved:
public class Example
{
[XmlAttribute]
public string[] Items { get; set; }
}
Serializing:
new Example { Items = new[] { "a", "b", "c" } }
continues to produce:
<Example Items="a b c" />
Deserialization continues to use the existing whitespace-splitting behavior.
When Separator is set to a non-default character, that character is used instead:
public class Example
{
[XmlAttribute(Separator = ',')]
public string[] Items { get; set; }
}
Serializing:
new Example { Items = new[] { "a", "b", "c" } }
produces:
<Example Items="a,b,c" />
Deserializing:
<Example Items="a,b,c" />
produces:
Items = new[] { "a", "b", "c" };
Alternative Designs
No response
Risks
Values containing the separator
This API does not introduce escaping for separator characters inside individual list values.
For example:
[XmlText(Separator = ' ')]
public string[] Values { get; set; } = new[] { "a b", "c d" };
would serialize as:
<Example>a b c d</Example>
and deserialize as:
new[] { "a", "b", "c", "d" }
rather than preserving the original two values.
This is consistent with delimiter-based list serialization generally: users must choose a separator that cannot appear in individual values, or use a different serialization shape.
This limitation should be documented.
Different default semantics between the two attributes
The default '\0' value preserves existing behavior, but that means the effective defaults differ:
[XmlText] with no separator continues to concatenate values.
[XmlAttribute] with no separator continues to use existing whitespace-separated behavior.
This asymmetry already exists today. The proposed API preserves it for compatibility.
Exact-character splitting versus existing whitespace splitting
For [XmlAttribute], existing deserialization splits on whitespace. With a custom separator, deserialization would split on the exact configured character.
For example:
[XmlAttribute(Separator = ' ')]
public string[] Values { get; set; }
would split on the literal space character, not on all whitespace characters.
This is a behavior distinction from the default Separator == '\0' path and should be called out in documentation.
Background and motivation
XmlSerializercurrently has inconsistent and non-configurable behavior when serializing array-like members as XML text versus XML attributes.For
[XmlAttribute] string[],XmlSerializerserializes values as a whitespace-separated list:For
[XmlText] string[],XmlSerializerserializes values by concatenating them with no separator:This behavior is longstanding and changing the default would be breaking. However, it makes it difficult to model XML Schema
xs:list-style element text values, where users often expect list items to be serialized with a separator, commonly a space.Today, users must work around this by hiding the actual collection from
XmlSerializerand exposing a shadow string property that manually joins and splits the values:This workaround is repetitive, error-prone, and requires users to change their object model purely for serialization purposes.
This proposal adds an opt-in separator property to the existing XML serialization attributes so users can preserve existing behavior by default while configuring list serialization when needed.
Related issue:
dotnet/runtime#115837Prototype PR:
dotnet/runtime#126767API Proposal
API Usage
XmlTextAttribute.SeparatorXmlTextAttribute.Separatorcontrols how array-like XML text values are serialized and deserialized.The default value is
'\0'.When
Separator == '\0', existing behavior is preserved:Serializing:
continues to produce:
Deserialization likewise preserves existing behavior: the full text content is treated as one string value.
When
Separatoris set to a non-default character, adjacent string values are separated using that character during serialization, and text content is split on that character during deserialization:Serializing:
produces:
Deserializing:
produces:
A non-whitespace separator can also be used:
XmlAttributeAttribute.SeparatorXmlAttributeAttribute.Separatorcontrols how array-like XML attribute values are serialized and deserialized.The default value is
'\0'.When
Separator == '\0', existing behavior is preserved:Serializing:
continues to produce:
Deserialization continues to use the existing whitespace-splitting behavior.
When
Separatoris set to a non-default character, that character is used instead:Serializing:
produces:
Deserializing:
produces:
Alternative Designs
No response
Risks
Values containing the separator
This API does not introduce escaping for separator characters inside individual list values.
For example:
would serialize as:
and deserialize as:
rather than preserving the original two values.
This is consistent with delimiter-based list serialization generally: users must choose a separator that cannot appear in individual values, or use a different serialization shape.
This limitation should be documented.
Different default semantics between the two attributes
The default
'\0'value preserves existing behavior, but that means the effective defaults differ:[XmlText]with no separator continues to concatenate values.[XmlAttribute]with no separator continues to use existing whitespace-separated behavior.This asymmetry already exists today. The proposed API preserves it for compatibility.
Exact-character splitting versus existing whitespace splitting
For
[XmlAttribute], existing deserialization splits on whitespace. With a custom separator, deserialization would split on the exact configured character.For example:
would split on the literal space character, not on all whitespace characters.
This is a behavior distinction from the default
Separator == '\0'path and should be called out in documentation.