Skip to content

[API Proposal]: System.Text.Xml — modern XML serializer with span support, source generation, and familiar API surface #125299

@nathanAjacobs

Description

@nathanAjacobs

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 namesSystem.Text.Xml with types like XmlDataSerializer, XmlDataSerializerOptions, XmlDataSerializerContext, and a low-level Utf8XmlDataReader/Utf8XmlDataWriter analogous to Utf8JsonReader/Utf8JsonWriter.
  • Span support — The low-level reader operates on ReadOnlySpan<byte> and ReadOnlySequence<byte> (UTF-8), exactly like Utf8JsonReader. The high-level serializer accepts ReadOnlySpan<char>, ReadOnlySpan<byte>, and string for deserialization.
  • Source generation — A Roslyn source generator that produces compile-time serialization/deserialization logic, identical in concept to System.Text.Json's JsonSerializerContext / [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.Json as closely as possible.

Why a new namespace / new types?

  • Avoid confusion — The legacy System.Xml.Serialization.XmlSerializer has a massive existing API surface and behavioral contract. A clean break under System.Text.Xml with distinctly named types makes it unmistakably clear this is a new, modern implementation.
  • Ref struct reader — The Utf8XmlDataReader ref struct (analogous to Utf8JsonReader) is necessary for ReadOnlySequence<byte> support and zero-allocation parsing. This is fundamentally different from System.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.Serialization attribute 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

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 existing XmlSerializer requires [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 ExtendedXmlSerializer exist but lack tight BCL integration, official source generator support, and the performance characteristics of a first-party implementation operating directly on Span<T> and IBufferWriter<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.Xml namespace is new and close to the existing System.Xml namespace. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.XmluntriagedNew issue has not been triaged by the area owner

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions