-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Open
Labels
api-suggestionEarly API idea and discussion, it is NOT ready for implementationEarly API idea and discussion, it is NOT ready for implementationarea-System.XmluntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
Description
Background and motivation
The existing System.Xml.Serialization.XmlSerializer does not leverage modern .NET constructs. Every constructor and factory method on it requires [RequiresDynamicCode] and [RequiresUnreferencedCode], making it fundamentally incompatible with AOT and trimming. It also requires public class types.
This proposes a new XML serializer implementation under the System.Text.Xml namespace (to avoid confusion with the legacy System.Xml.Serialization.XmlSerializer), modeled closely after the design and API surface of System.Text.Json.
Requirements & Desired Features
- New namespace and type names —
System.Text.Xmlwith types likeXmlDataSerializer,XmlDataSerializerOptions,XmlDataSerializerContext, and a low-levelUtf8XmlDataReader/Utf8XmlDataWriteranalogous toUtf8JsonReader/Utf8JsonWriter. - Span support — The low-level reader operates on
ReadOnlySpan<byte>andReadOnlySequence<byte>(UTF-8), exactly likeUtf8JsonReader. The high-level serializer acceptsReadOnlySpan<char>,ReadOnlySpan<byte>, andstringfor deserialization. - Source generation — A Roslyn source generator that produces compile-time serialization/deserialization logic, identical in concept to
System.Text.Json'sJsonSerializerContext/[JsonSerializable]/JsonSourceGenerationOptions. - XML namespace support — Namespace handling including prefix management, default namespace declarations, and namespace-qualified attribute mapping.
- Familiar API surface — Method signatures, options, and configuration should mirror
System.Text.Jsonas closely as possible.
Why a new namespace / new types?
- Avoid confusion — The legacy
System.Xml.Serialization.XmlSerializerhas a massive existing API surface and behavioral contract. A clean break underSystem.Text.Xmlwith distinctly named types makes it unmistakably clear this is a new, modern implementation. - Ref struct reader — The
Utf8XmlDataReaderref struct (analogous toUtf8JsonReader) is necessary forReadOnlySequence<byte>support and zero-allocation parsing. This is fundamentally different fromSystem.Xml.XmlReader. - Source generation — The
XmlDataSerializerContext+[XmlDataSerializable]pattern enables compile-time serialization that is fully AOT and trimming compatible, unlike the legacy serializer which requires[RequiresDynamicCode]. - New mapping attributes — The
System.Text.Xml.Serializationattribute set ([XmlDataRoot],[XmlDataElement],[XmlDataAttribute], etc.) provides the same XML shape control as the legacy attributes but is designed to work with the source generator and modern serializer pipeline.
Impact
This would allow modern .NET apps to use XML serialization with high performance and AOT compatibility. It would also allow the use of declaring XML model classes as internal rather than public.
Related
System.Text.JsonAPI referenceSystem.Xml.Serialization.XmlSerializerAPI reference- System.Text.Json source generation docs
API Proposal
Enums
XmlTokenType (analogous to JsonTokenType)
namespace System.Text.Xml;
public enum XmlTokenType : byte
{
None = 0,
StartElement = 1,
EndElement = 2,
Attribute = 3,
Text = 4,
CData = 5,
Comment = 6,
ProcessingInstruction = 7,
XmlDeclaration = 8,
}XmlCommentHandling (analogous to JsonCommentHandling)
public enum XmlCommentHandling : byte
{
Disallow = 0,
Skip = 1,
Allow = 2,
}Low-Level Reader
Utf8XmlDataReaderOptions / Utf8XmlDataReaderState (analogous to JsonReaderOptions / JsonReaderState)
public struct Utf8XmlDataReaderOptions
{
public XmlCommentHandling CommentHandling { get; set; }
public int MaxDepth { get; set; }
public bool AllowMultipleRoots { get; set; }
}
public readonly struct Utf8XmlDataReaderState
{
public Utf8XmlDataReaderState(Utf8XmlDataReaderOptions options = default);
public Utf8XmlDataReaderOptions Options { get; }
}Utf8XmlDataReader (ref struct — direct analogue of Utf8JsonReader)
public ref struct Utf8XmlDataReader
{
// --- Constructors (mirrors Utf8JsonReader) ---
public Utf8XmlDataReader(ReadOnlySpan<byte> utf8Xml, Utf8XmlDataReaderOptions options = default);
public Utf8XmlDataReader(ReadOnlySpan<byte> utf8Xml, bool isFinalBlock, Utf8XmlDataReaderState state);
public Utf8XmlDataReader(ReadOnlySequence<byte> utf8Xml, Utf8XmlDataReaderOptions options = default);
public Utf8XmlDataReader(ReadOnlySequence<byte> utf8Xml, bool isFinalBlock, Utf8XmlDataReaderState state);
// --- Properties (mirrors Utf8JsonReader) ---
public readonly long BytesConsumed { get; }
public readonly int CurrentDepth { get; }
public readonly Utf8XmlDataReaderState CurrentState { get; }
public readonly bool HasValueSequence { get; }
public readonly bool IsFinalBlock { get; }
public readonly SequencePosition Position { get; }
public readonly long TokenStartIndex { get; }
public readonly XmlTokenType TokenType { get; }
public readonly bool ValueIsEscaped { get; }
public readonly ReadOnlySequence<byte> ValueSequence { get; }
public readonly ReadOnlySpan<byte> ValueSpan { get; }
// --- XML element properties ---
public readonly bool IsEmptyElement { get; }
public readonly int AttributeCount { get; }
// --- XML namespace properties ---
public readonly ReadOnlySpan<byte> LocalNameSpan { get; }
public readonly ReadOnlySpan<byte> NamespaceUriSpan { get; }
public readonly ReadOnlySpan<byte> PrefixSpan { get; }
// --- Navigation ---
public bool Read();
public void Skip();
public bool TrySkip();
public bool MoveToNextAttribute();
public void MoveToElement();
// --- Copy methods ---
public readonly int CopyString(Span<byte> utf8Destination);
public readonly int CopyString(Span<char> destination);
// --- Value accessors (mirrors Utf8JsonReader) ---
public bool GetBoolean();
public byte GetByte();
public byte[] GetBytesFromBase64();
public DateTime GetDateTime();
public DateTimeOffset GetDateTimeOffset();
public decimal GetDecimal();
public double GetDouble();
public Guid GetGuid();
public short GetInt16();
public int GetInt32();
public long GetInt64();
[CLSCompliant(false)] public sbyte GetSByte();
public float GetSingle();
public string? GetString();
[CLSCompliant(false)] public ushort GetUInt16();
[CLSCompliant(false)] public uint GetUInt32();
[CLSCompliant(false)] public ulong GetUInt64();
public string GetComment();
// --- TryGet methods (mirrors Utf8JsonReader) ---
public bool TryGetByte(out byte value);
public bool TryGetBytesFromBase64([NotNullWhen(true)] out byte[]? value);
public bool TryGetDateTime(out DateTime value);
public bool TryGetDateTimeOffset(out DateTimeOffset value);
public bool TryGetDecimal(out decimal value);
public bool TryGetDouble(out double value);
public bool TryGetGuid(out Guid value);
public bool TryGetInt16(out short value);
public bool TryGetInt32(out int value);
public bool TryGetInt64(out long value);
[CLSCompliant(false)] public bool TryGetSByte(out sbyte value);
public bool TryGetSingle(out float value);
[CLSCompliant(false)] public bool TryGetUInt16(out ushort value);
[CLSCompliant(false)] public bool TryGetUInt32(out uint value);
[CLSCompliant(false)] public bool TryGetUInt64(out ulong value);
// --- Value comparison ---
public readonly bool ValueTextEquals(ReadOnlySpan<byte> utf8Text);
public readonly bool ValueTextEquals(ReadOnlySpan<char> text);
public readonly bool ValueTextEquals(string? text);
// --- Namespace-qualified value comparison ---
public readonly bool LocalNameEquals(ReadOnlySpan<byte> utf8LocalName);
public readonly bool LocalNameEquals(ReadOnlySpan<char> localName);
public readonly bool LocalNameEquals(string? localName);
public readonly bool NamespaceUriEquals(ReadOnlySpan<byte> utf8NamespaceUri);
public readonly bool NamespaceUriEquals(ReadOnlySpan<char> namespaceUri);
public readonly bool NamespaceUriEquals(string? namespaceUri);
}Low-Level Writer
XmlDataWriterOptions (analogous to JsonWriterOptions)
public struct XmlDataWriterOptions
{
public bool Indented { get; set; }
public char IndentCharacter { get; set; }
public int IndentSize { get; set; }
public string NewLine { get; set; }
public int MaxDepth { get; set; }
public bool SkipValidation { get; set; }
public bool OmitXmlDeclaration { get; set; }
}Utf8XmlDataWriter (analogous to Utf8JsonWriter)
public sealed class Utf8XmlDataWriter : IDisposable, IAsyncDisposable
{
// --- Constructors ---
public Utf8XmlDataWriter(IBufferWriter<byte> bufferWriter, XmlDataWriterOptions options = default);
public Utf8XmlDataWriter(Stream utf8Xml, XmlDataWriterOptions options = default);
// --- Properties (mirrors Utf8JsonWriter) ---
public long BytesCommitted { get; }
public int BytesPending { get; }
public int CurrentDepth { get; }
public XmlDataWriterOptions Options { get; }
// --- Lifecycle ---
public void Dispose();
public ValueTask DisposeAsync();
public void Flush();
public Task FlushAsync(CancellationToken cancellationToken = default);
public void Reset();
public void Reset(IBufferWriter<byte> bufferWriter);
public void Reset(Stream utf8Xml);
// --- XML Declaration ---
public void WriteXmlDeclaration();
// --- Elements (no namespace) ---
public void WriteStartElement(ReadOnlySpan<byte> utf8LocalName);
public void WriteStartElement(ReadOnlySpan<char> localName);
public void WriteStartElement(string localName);
// --- Elements (with namespace URI) ---
public void WriteStartElement(ReadOnlySpan<byte> utf8LocalName, ReadOnlySpan<byte> utf8NamespaceUri);
public void WriteStartElement(ReadOnlySpan<char> localName, ReadOnlySpan<char> namespaceUri);
public void WriteStartElement(string localName, string? namespaceUri);
// --- Elements (with prefix + namespace URI) ---
public void WriteStartElement(ReadOnlySpan<byte> utf8Prefix, ReadOnlySpan<byte> utf8LocalName, ReadOnlySpan<byte> utf8NamespaceUri);
public void WriteStartElement(string? prefix, string localName, string? namespaceUri);
public void WriteEndElement();
public void WriteFullEndElement();
// --- Combined element + string value (analogous to WriteString(propertyName, value)) ---
public void WriteElementString(ReadOnlySpan<byte> utf8LocalName, ReadOnlySpan<byte> utf8Value);
public void WriteElementString(ReadOnlySpan<byte> utf8LocalName, ReadOnlySpan<char> value);
public void WriteElementString(ReadOnlySpan<byte> utf8LocalName, string? value);
public void WriteElementString(ReadOnlySpan<byte> utf8LocalName, DateTime value);
public void WriteElementString(ReadOnlySpan<byte> utf8LocalName, DateTimeOffset value);
public void WriteElementString(ReadOnlySpan<byte> utf8LocalName, Guid value);
public void WriteElementString(ReadOnlySpan<char> localName, ReadOnlySpan<byte> utf8Value);
public void WriteElementString(ReadOnlySpan<char> localName, ReadOnlySpan<char> value);
public void WriteElementString(ReadOnlySpan<char> localName, string? value);
public void WriteElementString(ReadOnlySpan<char> localName, DateTime value);
public void WriteElementString(ReadOnlySpan<char> localName, DateTimeOffset value);
public void WriteElementString(ReadOnlySpan<char> localName, Guid value);
public void WriteElementString(string localName, ReadOnlySpan<byte> utf8Value);
public void WriteElementString(string localName, ReadOnlySpan<char> value);
public void WriteElementString(string localName, string? value);
public void WriteElementString(string localName, DateTime value);
public void WriteElementString(string localName, DateTimeOffset value);
public void WriteElementString(string localName, Guid value);
// --- Combined element + number value (analogous to WriteNumber(propertyName, value)) ---
public void WriteElementNumber(ReadOnlySpan<byte> utf8LocalName, int value);
public void WriteElementNumber(ReadOnlySpan<byte> utf8LocalName, long value);
public void WriteElementNumber(ReadOnlySpan<byte> utf8LocalName, double value);
public void WriteElementNumber(ReadOnlySpan<byte> utf8LocalName, float value);
public void WriteElementNumber(ReadOnlySpan<byte> utf8LocalName, decimal value);
[CLSCompliant(false)] public void WriteElementNumber(ReadOnlySpan<byte> utf8LocalName, uint value);
[CLSCompliant(false)] public void WriteElementNumber(ReadOnlySpan<byte> utf8LocalName, ulong value);
public void WriteElementNumber(ReadOnlySpan<char> localName, int value);
public void WriteElementNumber(ReadOnlySpan<char> localName, long value);
public void WriteElementNumber(ReadOnlySpan<char> localName, double value);
public void WriteElementNumber(ReadOnlySpan<char> localName, float value);
public void WriteElementNumber(ReadOnlySpan<char> localName, decimal value);
[CLSCompliant(false)] public void WriteElementNumber(ReadOnlySpan<char> localName, uint value);
[CLSCompliant(false)] public void WriteElementNumber(ReadOnlySpan<char> localName, ulong value);
public void WriteElementNumber(string localName, int value);
public void WriteElementNumber(string localName, long value);
public void WriteElementNumber(string localName, double value);
public void WriteElementNumber(string localName, float value);
public void WriteElementNumber(string localName, decimal value);
[CLSCompliant(false)] public void WriteElementNumber(string localName, uint value);
[CLSCompliant(false)] public void WriteElementNumber(string localName, ulong value);
// --- Combined element + boolean value (analogous to WriteBoolean(propertyName, value)) ---
public void WriteElementBoolean(ReadOnlySpan<byte> utf8LocalName, bool value);
public void WriteElementBoolean(ReadOnlySpan<char> localName, bool value);
public void WriteElementBoolean(string localName, bool value);
// --- Combined element + base64 value (analogous to WriteBase64String(propertyName, bytes)) ---
public void WriteElementBase64String(ReadOnlySpan<byte> utf8LocalName, ReadOnlySpan<byte> bytes);
public void WriteElementBase64String(ReadOnlySpan<char> localName, ReadOnlySpan<byte> bytes);
public void WriteElementBase64String(string localName, ReadOnlySpan<byte> bytes);
// --- Combined element + nil (analogous to WriteNull(propertyName)) ---
public void WriteNilElement(ReadOnlySpan<byte> utf8LocalName);
public void WriteNilElement(ReadOnlySpan<char> localName);
public void WriteNilElement(string localName);
// --- Attributes (no namespace) ---
public void WriteAttributeString(ReadOnlySpan<byte> utf8Name, ReadOnlySpan<byte> utf8Value);
public void WriteAttributeString(ReadOnlySpan<char> name, ReadOnlySpan<char> value);
public void WriteAttributeString(string name, string? value);
// --- Attributes (with namespace URI) ---
public void WriteAttributeString(ReadOnlySpan<byte> utf8LocalName, ReadOnlySpan<byte> utf8NamespaceUri, ReadOnlySpan<byte> utf8Value);
public void WriteAttributeString(string localName, string? namespaceUri, string? value);
// --- Attributes (with prefix + namespace URI) ---
public void WriteAttributeString(string? prefix, string localName, string? namespaceUri, string? value);
// --- Attribute start/end (for streaming attribute values) ---
public void WriteStartAttribute(ReadOnlySpan<char> name);
public void WriteStartAttribute(string name);
public void WriteStartAttribute(string? prefix, string localName, string? namespaceUri);
public void WriteEndAttribute();
// --- Namespace declarations ---
public void WriteNamespaceDeclaration(ReadOnlySpan<byte> utf8Prefix, ReadOnlySpan<byte> utf8NamespaceUri);
public void WriteNamespaceDeclaration(string prefix, string namespaceUri);
public void WriteDefaultNamespace(ReadOnlySpan<byte> utf8NamespaceUri);
public void WriteDefaultNamespace(string namespaceUri);
// --- String values (standalone) ---
public void WriteStringValue(ReadOnlySpan<byte> utf8Value);
public void WriteStringValue(ReadOnlySpan<char> value);
public void WriteStringValue(string? value);
public void WriteStringValue(DateTime value);
public void WriteStringValue(DateTimeOffset value);
public void WriteStringValue(Guid value);
// --- Number values (standalone) ---
public void WriteNumberValue(int value);
public void WriteNumberValue(long value);
public void WriteNumberValue(double value);
public void WriteNumberValue(float value);
public void WriteNumberValue(decimal value);
[CLSCompliant(false)] public void WriteNumberValue(uint value);
[CLSCompliant(false)] public void WriteNumberValue(ulong value);
// --- Boolean values (standalone) ---
public void WriteBooleanValue(bool value);
// --- Base64 values (standalone) ---
public void WriteBase64StringValue(ReadOnlySpan<byte> bytes);
// --- Nil value (standalone — writes xsi:nil="true") ---
public void WriteNilValue();
// --- Streaming value segments (analogous to WriteStringValueSegment / WriteBase64StringSegment) ---
public void WriteStringValueSegment(ReadOnlySpan<byte> value, bool isFinalSegment);
public void WriteStringValueSegment(ReadOnlySpan<char> value, bool isFinalSegment);
public void WriteBase64StringSegment(ReadOnlySpan<byte> value, bool isFinalSegment);
// --- CDATA / Comments / Raw ---
public void WriteCData(ReadOnlySpan<byte> utf8Value);
public void WriteCData(ReadOnlySpan<char> text);
public void WriteCData(string text);
public void WriteComment(ReadOnlySpan<byte> utf8Value);
public void WriteComment(ReadOnlySpan<char> value);
public void WriteComment(string value);
public void WriteRawValue(ReadOnlySpan<byte> utf8Xml, bool skipInputValidation = false);
public void WriteRawValue(ReadOnlySpan<char> xml, bool skipInputValidation = false);
public void WriteRawValue(string xml, bool skipInputValidation = false);
}Serialization Mapping Attributes
namespace System.Text.Xml.Serialization;
/// <summary>
/// Specifies the XML root element name and/or namespace for a type.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class XmlDataRootAttribute : Attribute
{
public XmlDataRootAttribute() { }
public XmlDataRootAttribute(string elementName) { }
public string? ElementName { get; set; }
public string? Namespace { get; set; }
public bool IsNullable { get; set; }
}
/// <summary>
/// Maps a property/field to an XML element.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
public sealed class XmlDataElementAttribute : Attribute
{
public XmlDataElementAttribute() { }
public XmlDataElementAttribute(string elementName) { }
public XmlDataElementAttribute(string elementName, string? namespaceUri) { }
public string? ElementName { get; set; }
public string? Namespace { get; set; }
public int Order { get; set; }
public bool IsNullable { get; set; }
public Type? Type { get; set; }
}
/// <summary>
/// Maps a property/field to an XML attribute.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class XmlDataAttributeAttribute : Attribute
{
public XmlDataAttributeAttribute() { }
public XmlDataAttributeAttribute(string attributeName) { }
public XmlDataAttributeAttribute(string attributeName, string? namespaceUri) { }
public string? AttributeName { get; set; }
public string? Namespace { get; set; }
}
/// <summary>
/// Maps a property/field to XML text content (inner text of an element).
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class XmlDataTextAttribute : Attribute { }
/// <summary>
/// Specifies that a collection property should be wrapped in an outer element.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class XmlDataArrayAttribute : Attribute
{
public XmlDataArrayAttribute() { }
public XmlDataArrayAttribute(string elementName) { }
public string? ElementName { get; set; }
public string? Namespace { get; set; }
public int Order { get; set; }
public bool IsNullable { get; set; }
}
/// <summary>
/// Excludes a property/field from XML serialization.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class XmlDataIgnoreAttribute : Attribute { }
/// <summary>
/// Specifies the XML type name and namespace for a type (used in xsi:type scenarios).
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = false)]
public sealed class XmlDataTypeAttribute : Attribute
{
public XmlDataTypeAttribute() { }
public XmlDataTypeAttribute(string typeName) { }
public string? TypeName { get; set; }
public string? Namespace { get; set; }
}
/// <summary>
/// Specifies a known derived type for polymorphic serialization.
/// Analogous to System.Text.Json's [JsonDerivedType].
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)]
public sealed class XmlDataIncludeAttribute : Attribute
{
public XmlDataIncludeAttribute(Type type) { }
public Type Type { get; }
public string? TypeName { get; set; }
public string? Namespace { get; set; }
}
/// <summary>
/// Controls XML namespace prefix declarations for a type.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)]
public sealed class XmlDataNamespaceDeclarationAttribute : Attribute
{
public XmlDataNamespaceDeclarationAttribute(string prefix, string namespaceUri) { }
public string Prefix { get; }
public string NamespaceUri { get; }
}
/// <summary>
/// Maps a property/field to an XML CDATA section.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class XmlDataCDataAttribute : Attribute { }
/// <summary>
/// Specifies a default value for a property, controlling whether it is serialized.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class XmlDataDefaultValueAttribute : Attribute
{
public XmlDataDefaultValueAttribute(object? value) { }
public object? Value { get; }
}
/// <summary>
/// Specifies the XML name for an enum member.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public sealed class XmlDataEnumAttribute : Attribute
{
public XmlDataEnumAttribute() { }
public XmlDataEnumAttribute(string name) { }
public string? Name { get; set; }
}High-Level Serializer
XmlDataSerializerOptions (analogous to JsonSerializerOptions)
public class XmlDataSerializerOptions
{
public XmlDataSerializerOptions() { }
// --- Formatting ---
public bool WriteIndented { get; set; }
public char IndentCharacter { get; set; }
public int IndentSize { get; set; }
public string NewLine { get; set; }
public bool OmitXmlDeclaration { get; set; }
// --- Behavior ---
public int MaxDepth { get; set; }
public bool IgnoreReadOnlyProperties { get; set; }
public bool IgnoreReadOnlyFields { get; set; }
public bool IncludeFields { get; set; }
public XmlDataNumberHandling NumberHandling { get; set; }
public XmlDataUnmappedMemberHandling UnmappedMemberHandling { get; set; }
// --- Default namespace ---
public string? DefaultNamespace { get; set; }
// --- Converters ---
public IList<XmlDataConverter> Converters { get; }
// --- Type info / source gen ---
public XmlDataSerializerContext? TypeInfoResolver { get; set; }
}XmlDataSerializer (static, analogous to JsonSerializer)
public static class XmlDataSerializer
{
// Serialize to string
public static string Serialize<T>(T value, XmlDataSerializerOptions? options = null);
public static string Serialize<T>(T value, XmlTypeInfo<T> typeInfo);
// Serialize to UTF-8 bytes
public static byte[] SerializeToUtf8Bytes<T>(T value, XmlDataSerializerOptions? options = null);
public static byte[] SerializeToUtf8Bytes<T>(T value, XmlTypeInfo<T> typeInfo);
// Serialize to writer
public static void Serialize<T>(Utf8XmlDataWriter writer, T value, XmlDataSerializerOptions? options = null);
public static void Serialize<T>(Utf8XmlDataWriter writer, T value, XmlTypeInfo<T> typeInfo);
// Serialize to Stream
public static Task SerializeAsync<T>(Stream utf8Xml, T value, XmlDataSerializerOptions? options = null, CancellationToken ct = default);
public static Task SerializeAsync<T>(Stream utf8Xml, T value, XmlTypeInfo<T> typeInfo, CancellationToken ct = default);
// Deserialize from string
public static T? Deserialize<T>(string xml, XmlDataSerializerOptions? options = null);
public static T? Deserialize<T>(string xml, XmlTypeInfo<T> typeInfo);
// Deserialize from ReadOnlySpan<char>
public static T? Deserialize<T>(ReadOnlySpan<char> xml, XmlDataSerializerOptions? options = null);
public static T? Deserialize<T>(ReadOnlySpan<char> xml, XmlTypeInfo<T> typeInfo);
// Deserialize from ReadOnlySpan<byte> (UTF-8)
public static T? Deserialize<T>(ReadOnlySpan<byte> utf8Xml, XmlDataSerializerOptions? options = null);
public static T? Deserialize<T>(ReadOnlySpan<byte> utf8Xml, XmlTypeInfo<T> typeInfo);
// Deserialize from Stream
public static ValueTask<T?> DeserializeAsync<T>(Stream utf8Xml, XmlDataSerializerOptions? options = null, CancellationToken ct = default);
public static ValueTask<T?> DeserializeAsync<T>(Stream utf8Xml, XmlTypeInfo<T> typeInfo, CancellationToken ct = default);
}Custom Converters (analogous to JsonConverter<T>)
public abstract class XmlDataConverter
{
public abstract bool CanConvert(Type typeToConvert);
}
public abstract class XmlDataConverter<T> : XmlDataConverter
{
public abstract T? Read(ref Utf8XmlDataReader reader, Type typeToConvert, XmlDataSerializerOptions options);
public abstract void Write(Utf8XmlDataWriter writer, T value, XmlDataSerializerOptions options);
public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(T);
}Source Generation (analogous to JsonSerializerContext)
public abstract class XmlDataSerializerContext
{
public XmlDataSerializerOptions Options { get; }
public abstract XmlTypeInfo? GetTypeInfo(Type type);
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class XmlDataSerializableAttribute : Attribute
{
public XmlDataSerializableAttribute(Type type) { }
public string? TypeInfoPropertyName { get; set; }
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class XmlDataSourceGenerationOptionsAttribute : Attribute
{
public bool WriteIndented { get; set; }
public bool OmitXmlDeclaration { get; set; }
public string? DefaultNamespace { get; set; }
public bool IncludeFields { get; set; }
public bool IgnoreReadOnlyProperties { get; set; }
public bool IgnoreReadOnlyFields { get; set; }
}API Usage
Basic serialization / deserialization
var person = new Person { Name = "Alice", Age = 30 };
string xml = XmlDataSerializer.Serialize(person);
Person? p = XmlDataSerializer.Deserialize<Person>(xml);Span-based deserialization
ReadOnlySpan<byte> utf8XmlSpan = "<Person><Name>Bob</Name></Person>"u8;
Person? p = XmlDataSerializer.Deserialize<Person>(utf8XmlSpan);
ReadOnlySpan<char> xmlSpan = "<Person><Name>Alice</Name><Age>30</Age></Person>".AsSpan();
Person? p2 = XmlDataSerializer.Deserialize<Person>(xmlSpan);Utf8XmlDataReader with ReadOnlySequence<byte> (mirrors Utf8JsonReader pattern exactly)
ReadOnlySequence<byte> utf8XmlSequence = GetUtf8XmlFromNetwork();
var reader = new Utf8XmlDataReader(utf8XmlSequence);
Person? p = XmlDataSerializer.Deserialize<Person>(ref reader);Streaming / partial reads with Utf8XmlDataReaderState
var state = new Utf8XmlDataReaderState(new Utf8XmlDataReaderOptions { MaxDepth = 64 });
while (moreData)
{
ReadOnlySpan<byte> buffer = GetNextChunk(out bool isFinal);
var reader = new Utf8XmlDataReader(buffer, isFinalBlock: isFinal, state);
while (reader.Read())
{
Console.WriteLine($"Token: {reader.TokenType}, Depth: {reader.CurrentDepth}, Consumed: {reader.BytesConsumed}");
}
state = reader.CurrentState;
}Namespace-aware serialization with mapping attributes
[XmlDataRoot("Person", Namespace = "http://example.com/person")]
[XmlDataNamespaceDeclaration("addr", "http://example.com/address")]
public class Person
{
[XmlDataElement("FullName")]
public string Name { get; set; }
[XmlDataAttribute("age")]
public int Age { get; set; }
[XmlDataElement("Address", "http://example.com/address")]
public Address? HomeAddress { get; set; }
}
[XmlDataType("AddressType", Namespace = "http://example.com/address")]
public class Address
{
[XmlDataElement]
public string City { get; set; }
[XmlDataElement]
public string Country { get; set; }
}
// Produces:
// <Person xmlns="http://example.com/person" xmlns:addr="http://example.com/address" age="30">
// <FullName>Alice</FullName>
// <addr:Address>
// <addr:City>Seattle</addr:City>
// <addr:Country>US</addr:Country>
// </addr:Address>
// </Person>Polymorphic collection serialization (mirrors System.Text.Json's [JsonDerivedType] pattern)
[XmlDataInclude(typeof(Book), TypeName = "Book")]
[XmlDataInclude(typeof(DVD), TypeName = "DVD")]
public class Product
{
[XmlDataElement]
public string Title { get; set; }
}
public class Book : Product
{
[XmlDataElement]
public string Author { get; set; }
}
public class DVD : Product
{
[XmlDataElement]
public int Runtime { get; set; }
}
public class Catalog
{
[XmlDataArray("Items")]
public List<Product> Products { get; set; }
}
var catalog = new Catalog
{
Products = new List<Product>
{
new Book { Title = "Clean Code", Author = "Robert C. Martin" },
new DVD { Title = "The Matrix", Runtime = 136 }
}
};
string xml = XmlDataSerializer.Serialize(catalog);
// Produces:
// <Catalog>
// <Items>
// <Book>
// <Title>Clean Code</Title>
// <Author>Robert C. Martin</Author>
// </Book>
// <DVD>
// <Title>The Matrix</Title>
// <Runtime>136</Runtime>
// </DVD>
// </Items>
// </Catalog>Low-level reader with namespace comparison
ReadOnlySpan<byte> utf8Xml = """
<root xmlns:ns="http://example.com">
<ns:Child>value</ns:Child>
</root>
"""u8;
var reader = new Utf8XmlDataReader(utf8Xml);
while (reader.Read())
{
if (reader.TokenType == XmlTokenType.StartElement)
{
if (reader.LocalNameEquals("Child"u8) &&
reader.NamespaceUriEquals("http://example.com"u8))
{
reader.Read();
string? value = reader.GetString();
}
}
}Low-level writer with namespaces
using var stream = new MemoryStream();
using var writer = new Utf8XmlDataWriter(stream, new XmlDataWriterOptions { Indented = true });
writer.WriteXmlDeclaration();
writer.WriteStartElement("root");
writer.WriteNamespaceDeclaration("ns", "http://example.com");
writer.WriteDefaultNamespace("http://example.com/default");
writer.WriteStartElement("ns", "Child", "http://example.com");
writer.WriteAttributeString("ns", "id", "http://example.com", "42");
writer.WriteStringValue("Hello");
writer.WriteEndElement();
writer.WriteEndElement();
writer.Flush();
// Produces:
// <?xml version="1.0" encoding="utf-8"?>
// <root xmlns:ns="http://example.com" xmlns="http://example.com/default">
// <ns:Child ns:id="42">Hello</ns:Child>
// </root>Source generation (AOT / trimming safe)
[XmlDataSourceGenerationOptions(WriteIndented = true)]
[XmlDataSerializable(typeof(Person))]
[XmlDataSerializable(typeof(Address))]
partial class AppXmlContext : XmlDataSerializerContext { }
// No reflection, no dynamic code
string xml = XmlDataSerializer.Serialize(person, AppXmlContext.Default.Person);
Person? p = XmlDataSerializer.Deserialize<Person>(xml, AppXmlContext.Default.Person);
// Works with spans
ReadOnlySpan<byte> utf8 = Encoding.UTF8.GetBytes(xml);
Person? p2 = XmlDataSerializer.Deserialize<Person>(utf8, AppXmlContext.Default.Person);Alternative Designs
- Updating the existing
XmlSerializer— Not feasible. The existingXmlSerializerrequires[RequiresDynamicCode]and[RequiresUnreferencedCode]on every entry point. Its internal architecture relies on runtime IL generation, making it fundamentally incompatible with AOT/trimming. Retrofitting span support and source generation onto it would require breaking changes to its extensive public API surface and behavioral contract. - Third-party libraries — Libraries like
ExtendedXmlSerializerexist but lack tight BCL integration, official source generator support, and the performance characteristics of a first-party implementation operating directly onSpan<T>andIBufferWriter<byte>.
Risks
- Large API surface — This proposal introduces a substantial number of new types, methods, and attributes. The implementation and ongoing maintenance cost is significant.
- Namespace collision potential — The
System.Text.Xmlnamespace is new and close to the existingSystem.Xmlnamespace. Clear documentation and guidance will be needed to avoid confusion. - XML specification complexity — XML has significantly more features than JSON (namespaces, DTDs, entities, processing instructions, mixed content, etc.). Deciding which XML features to support and which to exclude will require careful design decisions.
- Source generator complexity — XML's namespace and attribute model is more complex than JSON's property model, which may make the source generator harder to implement and maintain compared to
System.Text.Json's generator.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
api-suggestionEarly API idea and discussion, it is NOT ready for implementationEarly API idea and discussion, it is NOT ready for implementationarea-System.XmluntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner