Meziantou.Framework.Yaml 2.0.12

Prefix Reserved
dotnet add package Meziantou.Framework.Yaml --version 2.0.12
                    
NuGet\Install-Package Meziantou.Framework.Yaml -Version 2.0.12
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Meziantou.Framework.Yaml" Version="2.0.12" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Meziantou.Framework.Yaml" Version="2.0.12" />
                    
Directory.Packages.props
<PackageReference Include="Meziantou.Framework.Yaml" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Meziantou.Framework.Yaml --version 2.0.12
                    
#r "nuget: Meziantou.Framework.Yaml, 2.0.12"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Meziantou.Framework.Yaml@2.0.12
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Meziantou.Framework.Yaml&version=2.0.12
                    
Install as a Cake Addin
#tool nuget:?package=Meziantou.Framework.Yaml&version=2.0.12
                    
Install as a Cake Tool

Meziantou.Framework.Yaml

Meziantou.Framework.Yaml is a YAML parser, emitter, and serializer for .NET. It reads and writes YAML documents, serializes object graphs, deserializes typed models, and generates serialization metadata at compile time for NativeAOT and trimming scenarios.

The source generator ships inside the package. No additional package is required to use generated YamlSerializerContext types.

Install the package

dotnet add package Meziantou.Framework.Yaml

Table of contents

Overview

The package targets YAML 1.2. The parser accepts documents declaring %YAML 1.1, %YAML 1.2, or a higher minor version, using YAML 1.2 rules. Other major versions and YAML 1.0 are rejected with a SemanticErrorException. Multi-document streams, %TAG directives, block and flow collections, all five scalar styles (plain, single-quoted, double-quoted, literal, folded), anchors, aliases, merge keys, tags, and comments are all parsed.

Four API layers are available, from the lowest level to the highest:

Layer Entry points Use it for
Tokens and events Scanner<TBuffer>, Parser<TBuffer>, EventReader, Emitter Streaming over parsing events without materializing a tree
Syntax tree YamlSyntaxTree Lossless representation, including comments and whitespace, with source spans
Document Object Model YamlStream, YamlDocument, YamlMapping, YamlSequence, YamlValue Inspecting or transforming a document without binding to a CLR type
Serializer YamlSerializer, YamlReader, YamlWriter Binding YAML to CLR types

Serialize and deserialize objects

YamlSerializer and YamlSerializerOptions are in the Meziantou.Framework.Yaml namespace, and the attributes in Meziantou.Framework.Yaml.Serialization. Given this model:

public sealed class Product
{
    public int Id { get; set; }

    [YamlPropertyName("name")]
    public string DisplayName { get; set; } = "";

    public string[] Tags { get; set; } = [];
}

Serialize writes an instance as YAML, and Deserialize reads it back:

var options = new YamlSerializerOptions
{
    PropertyNamingPolicy = YamlNamingPolicy.KebabCaseLower,
};

var yaml = YamlSerializer.Serialize(new Product
{
    Id = 1,
    DisplayName = "Sample product",
    Tags = ["new", "featured"],
}, options);

Assert.Equal("""
    id: 1
    name: Sample product
    tags:
      - new
      - featured

    """, yaml);

var product = YamlSerializer.Deserialize<Product>(yaml, options);
Assert.Equal("Sample product", product!.DisplayName);

YamlSerializer reads from a string, a TextReader (streaming), or a UTF-8 Stream, and writes to a string, a TextWriter, a UTF-8 Stream, or an IBufferWriter<char>. Every overload has a generic and a Type-based form, and accepts either a YamlSerializerOptions, a YamlSerializerContext, or a YamlTypeInfo.

TryDeserialize returns false instead of throwing when the payload cannot be bound:

if (YamlSerializer.TryDeserialize<Product>("Id: 1", out var value))
{
    Assert.Equal(1, value!.Id);
}

Assert.False(YamlSerializer.TryDeserialize<Product>("Id: not a number", out _));

When a stream contains several documents, YamlSerializer.Deserialize binds the first one. Use YamlStream to read every document.

Supported types

Category Types
Text string, char, Rune
Boolean bool
Integers byte, sbyte, short, ushort, int, uint, long, ulong, nint, nuint, Int128, UInt128, BigInteger
Floating point float, double, decimal, Half
Date and time DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan
Other scalars Guid, Uri, CultureInfo, Version, enums
net11.0 only BFloat16, Decimal32, Decimal64, Decimal128
Collections arrays, List<>, HashSet<>, ImmutableArray<>, ImmutableList<>, ImmutableHashSet<>, FrozenSet<>, Queue<>, Stack<>, ConcurrentQueue<>, ConcurrentStack<>, ConcurrentBag<>, ArraySegment<>, IEnumerable<>, ICollection<>, IList<>, IReadOnlyCollection<>, IReadOnlyList<>, ISet<>, IReadOnlySet<>
Dictionaries Dictionary<,>, OrderedDictionary<,>, FrozenDictionary<,>, IDictionary<,>, IReadOnlyDictionary<,>, with string or non-string keys
Pairs KeyValuePair<,>, as a mapping with a Key and a Value entry
Complex numbers Complex, and on net11.0 Complex<T> (with float, double, Half, BFloat16 or Decimal* parts), as a mapping with a Real and an Imaginary entry
Untyped object, object[], List<object>, Dictionary<string, object>
Documents any YamlNode (YamlMapping, YamlSequence, YamlValue, …)
Unions C# unions

Every value type in the table also works as its Nullable<T> counterpart. Any type implementing a single IDictionary<TKey, TValue> and exposing a parameterless constructor (SortedDictionary<,>, ConcurrentDictionary<,>, a class deriving from Dictionary<,>, …) is treated as a dictionary. Any other type implementing a single ICollection<T> and exposing a parameterless constructor is treated as a collection. Anything else is serialized as a mapping of its properties, using the object contract described below.

A dictionary key is written as a scalar, so its type must be string, object, an enum, or one of the scalar types of the table. A key of any other type, which could not be read back, throws a NotSupportedException (the source generator reports MFY002 or MFY007), unless a custom converter handles the key type. The keys of a dictionary whose key type is object keep their type: a string key such as "1" or "true" is quoted so it is not read back as a number or a boolean, and YamlWriter.WriteDictionaryKey writes a key the same way from a custom converter.

Reading into object produces null, bool, long (or ulong beyond its range), double, string, List<object?> for a sequence, and Dictionary<object, object?> for a mapping. The keys of that dictionary are resolved like values, so 1 is a long and '1' is a string, and both can appear in the same mapping. Two keys that resolve to the same value, such as 1 and 0x1, are duplicates. A null key (~, null) and a key that is itself a sequence or a mapping cannot be represented and throw a YamlException.

A key of one of the supported key types is always written and read as a scalar by the dictionary itself. A custom converter registered for its type, in Converters, in YamlSourceGenerationOptionsAttribute.Converters, or with YamlConverterAttribute on the type, applies to the values of the dictionary, not to its keys.

The other collections of the base class library, such as ArrayList, Hashtable, ReadOnlyCollection<>, or the immutable collections not listed above, and the types that cannot be serialized at all, such as Type, delegates, and IAsyncEnumerable<>, are not written as a mapping of their properties, which could not be read back: they throw a NotSupportedException, and the source generator reports them.

A stack (Stack<>, ConcurrentStack<>) is written from its top and read back in the same order. ArraySegment<> is written as the elements of the segment, a default segment as null, and is read over a new array. KeyValuePair<,> entry names follow PropertyNamingPolicy. BigInteger is written as an integer, Version using its string representation, and Rune as a one-character scalar.

Enums are written using their member name and read case-insensitively, with a numeric fallback. Use YamlEnumMemberNameAttribute to control the name of an individual member.

Apart from a string written with PreferQuotedForAmbiguousScalars disabled, a value that is not null is never written as a null scalar: an enum member named Null, a relative Uri such as ~, a dictionary key, or a custom converter calling YamlWriter.WriteScalar("null") are quoted. Their text is still written plain when it reads as a boolean or a number (an enum member named True, the Version 1.2), which a typed member reads back unchanged, but which an object member or a union resolves as a boolean or a number.

Uri is written using its original string and read with UriKind.RelativeOrAbsolute, so both absolute and relative URIs round-trip. CultureInfo is written using its culture name (CultureInfo.InvariantCulture writes an empty name), and read back through CultureInfo.GetCultureInfo.

Object contracts support parameterless constructors, parameterized constructors, records, init accessors, the required keyword, and non-public members annotated with YamlIncludeAttribute. When a type declares several public constructors, annotate the one to use with YamlConstructorAttribute. A struct is created without calling a constructor unless one is annotated with YamlConstructorAttribute.

A null scalar, such as ~, assigns null to a member whose type accepts it. A custom converter of the member reads the null scalar itself, and a non-nullable value type rejects it, as it does for a root value or a collection element: Value: ~ throws a YamlException for an int or a struct member instead of assigning its default value.

Members are listed from the base-most type down to the type itself and, within a type, properties come before fields, each in declaration order. A member that overrides or hides a member of a base type takes its position, and inherits the attributes of the member it overrides. Two members mapping to the same YAML name throw an InvalidOperationException.

Configure serialization

YamlSerializerOptions is a record, so an existing instance can be adapted with with. YamlSerializerOptions.Default exposes the default instance.

Naming

Option Default Description
PropertyNamingPolicy null Converts CLR member names. Built-in policies: YamlNamingPolicy.CamelCase, SnakeCaseLower, SnakeCaseUpper, KebabCaseLower, KebabCaseUpper, PascalCase. Derive from YamlNamingPolicy for a custom policy.
DictionaryKeyPolicy null Converts dictionary keys when writing.
PropertyNameCaseInsensitive false Matches mapping keys to members ignoring case.

Member selection

Option Default Description
IncludeFields false Includes public fields.
IgnoreReadOnlyFields false Skips read-only fields when writing.
IgnoreReadOnlyProperties false Skips get-only properties when writing.
DefaultIgnoreCondition Never Never, WhenWritingNull, WhenWritingDefault, Always, WhenWriting, WhenReading.

Output shape

Option Default Description
WriteIndented true Writes block collections indented by IndentSize. When disabled, collections use the flow style and the document stays on a single line.
IndentSize 2 Number of spaces per indentation level. Ignored when WriteIndented is disabled.
IndentBlockSequences true Indents a block sequence that is the value of a mapping key. When disabled, the sequence dashes stay at the indentation of the parent mapping.
MappingOrder Declaration Declaration or Sorted.
BlockSequenceMappingStyle Compact How a mapping inside a block sequence is emitted.
BlockSequenceSequenceStyle Expanded How a nested sequence inside a block sequence is emitted.
ScalarStylePreferences PreferPlainStyle = true, PreferQuotedForAmbiguousScalars = true, StringStyle = Any Quotes scalars that would otherwise resolve to a boolean, a number, or null, and selects the style used for string values. PreferPlainStyle = false double-quotes every string value and mapping key the writer would otherwise write plain, unless StringStyle requests another style; a dictionary key that is not a string and reads as a number or a boolean stays plain, so that it keeps its type.

BlockSequenceMappingStyle controls whether the first key of a mapping shares the line of its sequence dash:

# Compact
items:
  - id: 1
    name: first

# Expanded
items:
  -
    id: 1
    name: first

IndentBlockSequences controls whether a sequence used as a mapping value gets its own indentation level:

# IndentBlockSequences = true
product:
  tags:
    - new
    - sale

# IndentBlockSequences = false
product:
  tags:
  - new
  - sale

A sequence nested inside another sequence is always indented past its parent dash, because YAML has no unindented form for it.

Block YAML expresses nesting through indentation, so WriteIndented = false switches collections to the flow style instead of writing block collections without indentation. The whole document stays on a single line, and IndentSize, BlockSequenceMappingStyle, and BlockSequenceSequenceStyle no longer apply:

# WriteIndented = true, IndentSize = 2
product:
  name: Table
  tags:
    - new
# WriteIndented = false
{product: {name: Table, tags: [new]}}

Flow output is read back by the deserializer like any other YAML, and scalars containing a flow indicator such as ,, :, [, or { are quoted so the round-trip stays faithful.

StringStyle selects the style used for string values. The default, Any, lets the writer choose between the plain and the double-quoted style. Literal writes a block scalar instead:

var options = new YamlSerializerOptions
{
    ScalarStylePreferences = new YamlScalarStylePreferences { StringStyle = ScalarStyle.Literal },
};

var yaml = YamlSerializer.Serialize(new { Script = "echo one\necho two\n" }, options);
Script: |
  echo one
  echo two

The chomping indicator follows the value: |- when it has no trailing line break, | for one, and |+ for more. Folded writes > instead, and Plain, SingleQuoted, and DoubleQuoted ask for those styles.

The requested style is used only when the value round-trips through it. A block scalar cannot represent an empty value, a carriage return, a control character, a line that ends with a blank, or a line that starts with a tab, and none can be written inside a flow collection; a folded scalar additionally cannot hold a line that starts with a space. When the style does not fit, the value falls back to the automatic style. The style applies to string values only — mapping keys and non-string scalars such as numbers, booleans, and dates keep their own representation.

[YamlStringStyle] overrides the style for one member and for the strings below it:

internal sealed class Job
{
    [YamlStringStyle(ScalarStyle.Literal)]
    public string? Script { get; set; }

    public string? Name { get; set; }
}
Script: |-
  echo one
  echo two
Name: build

PreferQuotedForAmbiguousScalars keeps a round-trip faithful when a string looks like another type:

var yaml = YamlSerializer.Serialize(new { A = "yes", B = "123", C = "plain" });
A: yes
B: "123"
C: plain

Reading

Option Default Description
UnmappedMemberHandling Skip Disallow throws when a mapping key matches no member.
RespectRequiredConstructorParameters true Requires a value for every non-optional constructor parameter.
RespectNullableAnnotations true Rejects null for a member declared non-nullable.
PreferredObjectCreationHandling Replace Populate adds to the collection or object already held by the member instead of replacing it.
DuplicateKeyHandling Error FirstWins or LastWins accept duplicate mapping keys.
SourceName null Name (typically a file path) used to annotate exception messages.

Metadata and extensibility

Option Default Description
Converters empty Custom converters, evaluated in order, ahead of built-in converters.
TypeClassifiers empty Factories selecting a union case or a derived type from the payload.
TypeInfoResolver null Metadata source; set it to a generated YamlSerializerContext to avoid reflection.
PolymorphismOptions see Polymorphism Discriminator style, property name, unknown-type handling, runtime mappings.
ReferenceHandling None See Anchors, aliases, and merge keys.
Schema / UseSchema Core / false See Schemas.

GetTypeInfo<T>() and TryGetTypeInfo<T>() return the contract metadata an options instance resolves for a type.

Attributes

Attribute Target Description
YamlPropertyNameAttribute member Sets the YAML key. Takes precedence over every naming policy.
YamlNamingPolicyAttribute type, member Overrides PropertyNamingPolicy.
YamlPropertyOrderAttribute member Orders emitted keys.
YamlIgnoreAttribute type, member Overrides DefaultIgnoreCondition.
YamlIncludeAttribute member Includes a non-public or otherwise excluded member.
YamlRequiredAttribute member Requires the key to be present when reading.
YamlConstructorAttribute constructor Selects the deserialization constructor, including a non-public one.
YamlConverterAttribute type, member Uses a custom converter.
YamlExtensionDataAttribute member Collects unmatched mapping keys.
YamlEnumMemberNameAttribute enum member Sets the YAML value of an enum member.
YamlNumberHandlingAttribute type, member AllowReadingFromString, WriteAsString, AllowNamedFloatingPointLiterals.
YamlObjectCreationHandlingAttribute type, member Replace or Populate.
YamlUnmappedMemberHandlingAttribute type Skip or Disallow for this type.
YamlBlockSequenceItemStyleAttribute type, member Block sequence item style for this type or member.
YamlPolymorphicAttribute type Enables and configures polymorphism.
YamlDerivedTypeAttribute type Registers a derived type on its base type.
YamlDerivedTypeMappingAttribute assembly, type Registers a derived type from outside the base type's assembly.
YamlSerializableAttribute context Declares a root type for source generation.
YamlSourceGenerationOptionsAttribute context Configures a generated context.

YamlNumberHandlingAttribute applies to byte, sbyte, short, ushort, int, uint, long, ulong, nint, nuint, float, double, decimal, and, on net11.0, BFloat16, Decimal32, Decimal64, and Decimal128, plus their Nullable<T> counterparts. AllowNamedFloatingPointLiterals reads and writes the quoted NaN, Infinity, and -Infinity literals for the floating-point types instead of the .nan, .inf, and -.inf scalars of the YAML core schema; it has no effect on the integer types.

Naming policies

YamlNamingPolicyAttribute overrides YamlSerializerOptions.PropertyNamingPolicy. On a type it applies to every member; on a member it applies to that member only. YamlPropertyNameAttribute takes precedence over both.

[YamlNamingPolicy(YamlKnownNamingPolicy.SnakeCaseLower)]
internal sealed class Product
{
    public int Id { get; set; }                       // id

    [YamlNamingPolicy(YamlKnownNamingPolicy.CamelCase)]
    public string? DisplayName { get; set; }          // displayName

    public DateTime CreationDate { get; set; }        // creation_date
}

Ignoring members

YamlIgnoreAttribute overrides YamlSerializerOptions.DefaultIgnoreCondition. On a type it sets the default condition for every property and field; on a member it applies to that member only.

[YamlIgnore(Condition = YamlIgnoreCondition.WhenWritingNull)]
internal sealed class Product
{
    public string? Name { get; set; }                 // omitted when null

    [YamlIgnore(Condition = YamlIgnoreCondition.Never)]
    public string? Description { get; set; }          // always written

    [YamlIgnore]
    public string? Secret { get; set; }               // never serialized nor deserialized
}

Ordering members

internal sealed class Product
{
    [YamlPropertyOrder(1)]
    public int Id { get; set; }

    [YamlPropertyOrder(2)]
    public string? Name { get; set; }
}

Enum member names

internal enum Level
{
    Low,

    [YamlEnumMemberName("very-high")]
    VeryHigh,                                         // very-high
}

Populating existing values

YamlObjectCreationHandling.Populate adds to the value a member already holds instead of replacing it. It applies to collections and to nested objects.

internal sealed class Settings
{
    [YamlObjectCreationHandling(YamlObjectCreationHandling.Populate)]
    public List<string> Tags { get; set; } = ["default"];
}
var settings = YamlSerializer.Deserialize<Settings>("Tags:\n  - extra\n");
Assert.Equal(["default", "extra"], settings!.Tags);

Extension data

YamlExtensionDataAttribute collects the mapping keys that match no member. The annotated member must be a YamlMapping, or a dictionary whose keys are string and whose values are object or YamlNode:

internal sealed class Product
{
    public int Id { get; set; }

    [YamlExtensionData]
    public IReadOnlyDictionary<string, object?>? Extra { get; set; }
}

The supported dictionary types are Dictionary<string, TValue>, IDictionary<string, TValue>, and IReadOnlyDictionary<string, TValue>. A read-only dictionary cannot be mutated through its declared type, so the deserializer creates a Dictionary<string, TValue>, copies the entries of the current value into it, and assigns it to the member. The member must be settable, unless its value is already a Dictionary<string, TValue> which is then updated in place.

Lifecycle callbacks

Implement any of these interfaces to run code around serialization:

internal sealed class Product : IYamlOnSerializing, IYamlOnSerialized, IYamlOnDeserializing, IYamlOnDeserialized
{
    public void OnSerializing() { }
    public void OnSerialized() { }
    public void OnDeserializing() { }
    public void OnDeserialized() { }
}

An exception thrown from a callback is wrapped in a YamlException.

Custom converters

Custom converters derive from YamlConverter<T> and are registered through YamlSerializerOptions.Converters, YamlSourceGenerationOptionsAttribute.Converters, or YamlConverterAttribute.

internal sealed class UriConverter : YamlConverter<Uri>
{
    public override Uri Read(YamlReader reader) => new(reader.GetScalarValue(), UriKind.RelativeOrAbsolute);

    public override void Write(YamlWriter writer, Uri value) => writer.WriteString(value.ToString());
}
var options = new YamlSerializerOptions { Converters = [new UriConverter()] };

YamlReader walks the payload token by token (TokenType, Read(), Skip(), GetScalarValue(), Tag, Anchor, Alias), and YamlWriter emits it (WriteStartMapping(), WritePropertyName(), WriteScalar() overloads for every built-in type, WriteTag(), WriteAnchor(), WriteAlias()). The YamlScalar static class exposes the span-based scalar parsers used by the built-in converters (TryParseInt64, TryParseDouble, IsNull, ResolveObject, …).

Override CanPopulate and Populate to support YamlObjectCreationHandling.Populate.

Derive from YamlConverterFactory to create converters dynamically for a family of types.

YamlConverterAttribute also accepts an open generic converter type when the annotated type, or the type of the annotated member, is a generic type with the same number of generic parameters. The converter is closed over the type arguments of the target type.

[YamlConverter(typeof(BoxConverter<>))]
private sealed class Box<T>
{
    public Box(T? value) => Value = value;

    public T? Value { get; }
}

private sealed class BoxConverter<T> : YamlConverter<Box<T>?> // used for Box<int>, Box<string>, ...
{
    public override Box<T>? Read(YamlReader reader)
    {
        if (reader.TokenType is YamlTokenType.Scalar && YamlScalar.IsNull(reader.ScalarValue.AsSpan()))
        {
            reader.Read();
            return null;
        }

        var scalar = reader.GetScalarValue();
        reader.Read();
        return new Box<T>((T)Convert.ChangeType(scalar, typeof(T), CultureInfo.InvariantCulture));
    }

    public override void Write(YamlWriter writer, Box<T>? value)
    {
        if (value is null)
        {
            writer.WriteNullValue();
            return;
        }

        writer.WriteScalar(Convert.ToString(value.Value, CultureInfo.InvariantCulture));
    }
}

Polymorphism

Annotate a base type with YamlPolymorphicAttribute and register its derived types with YamlDerivedTypeAttribute.

[YamlPolymorphic]
[YamlDerivedType(typeof(Dog), "dog")]
[YamlDerivedType(typeof(Cat), "cat")]
internal abstract class Animal
{
    public string Name { get; set; } = "";
}

internal sealed class Dog : Animal;

internal sealed class Cat : Animal;
$type: dog
Name: Rex
Setting Values Description
DiscriminatorStyle Property (default), Tag, Both Whether the type is identified by a mapping key or by a YAML tag.
TypeDiscriminatorPropertyName $type Key used by the Property style.
UnknownDerivedTypeHandling Fail (default), FallBackToBase Behavior when the discriminator matches no registration.
InferClosedTypePolymorphism false See Closed hierarchies.

Discriminators can be strings or integers. With the Tag style, the discriminator is a YAML tag:

[YamlPolymorphic(DiscriminatorStyle = YamlTypeDiscriminatorStyle.Tag)]
[YamlDerivedType(typeof(Dog), Tag = "!dog")]
internal abstract class Animal
{
    public string Name { get; set; } = "";
}

internal sealed class Dog : Animal;
!dog
Name: Rex

The same settings are available on YamlSerializerOptions.PolymorphismOptions for the whole serializer.

The discriminator is consumed while reading: the selected type does not see it, so it is neither an unmapped member nor an extension data entry, and a selected type that is polymorphic itself does not resolve it a second time. Such a type is read as itself unless the mapping also carries a discriminator or tag of its own: its default derived type and type classifiers do not replace a type the discriminator selected explicitly. With the Property and Both styles, a member or extension data entry named like the discriminator property is not written. A base type registered as one of its own derived types is written with its discriminator.

The discriminator is only read from the mapping itself: a discriminator provided by a mapping merged into it with << is not taken into account.

A type cannot register the same derived type, discriminator, or tag twice, nor two derived types with neither a discriminator nor a tag. Reflection-based serialization throws an InvalidOperationException when the type is first used, and the source generator reports the MFY026 diagnostic.

UnknownDerivedTypeHandling applies while reading. While writing, the runtime type must match a registration exactly: serializing a subclass of a registered derived type that is not registered itself throws NotSupportedException rather than writing it under the discriminator of its closest registered base, which would silently drop the members it declares. This holds for both the reflection-based serializer and a source-generated YamlSerializerContext.

Registering derived types at runtime

PolymorphismOptions.DerivedTypeMappings registers derived types without touching the base type, which enables polymorphism across assemblies (plugin systems, clean architecture). Runtime entries are merged with attribute-based ones, and attributes win on conflicts. The mappings are copied when the YamlPolymorphismOptions instance is assigned to YamlSerializerOptions.PolymorphismOptions, and the instance becomes read-only: changing the mappings afterwards throws NotSupportedException, so options already in use, and the copies made from them with with, cannot change.

var options = new YamlSerializerOptions
{
    PolymorphismOptions = new YamlPolymorphismOptions
    {
        DerivedTypeMappings =
        {
            [typeof(Animal)] = [new YamlDerivedType(typeof(Dog), "dog")],
        },
    },
};

YamlDerivedTypeMappingAttribute does the same declaratively, and is the form the source generator understands.

Open generic derived types

YamlDerivedTypeAttribute accepts an open generic derived type on a generic base type. The derived type is closed for each instantiation of the base type by matching the base type it declares, so a single attribute covers every instantiation.

[YamlPolymorphic]
[YamlDerivedType(typeof(Dog<>), "dog")]
internal abstract class Animal<T>
{
}

internal sealed class Dog<T> : Animal<T>  // Animal<string> uses Dog<string>, Animal<int> uses Dog<int>
{
}

Closed hierarchies

A closed class hierarchy already lists its derived types in metadata, so they do not need to be repeated with YamlDerivedTypeAttribute. Set InferClosedTypePolymorphism to register every derived type of a closed base type automatically, using the derived type name, without the generic arity suffix, as its discriminator.

public closed class Shape
{
    public string Name { get; set; } = "";
}

public sealed class Circle : Shape { public double Radius { get; set; } }
public sealed class Square : Shape { public double Side { get; set; } }
var options = new YamlSerializerOptions
{
    PolymorphismOptions = new YamlPolymorphismOptions { InferClosedTypePolymorphism = true },
};

Shape shape = new Circle { Name = "circle", Radius = 3 };
var yaml = YamlSerializer.Serialize(shape, options);
Assert.Equal("$type: Circle\nName: circle\nRadius: 3.0\n", yaml);

Inference is opt-in. It can also be enabled for a single type with YamlPolymorphicAttribute, which takes precedence over the serializer options, so InferClosedTypePolymorphism = false excludes a type from a global opt-in:

[YamlPolymorphic(InferClosedTypePolymorphism = true)]
public closed class Shape
{
}

Explicit registrations replace inference: a type declaring YamlDerivedTypeAttribute or a runtime mapping registers only those derived types. Enabling YamlPolymorphicAttribute.InferClosedTypePolymorphism on a type that is not declared closed throws an InvalidOperationException and reports the MFY023 diagnostic. A derived type that is itself closed brings its own hierarchy along: its derived types are registered under the root of the hierarchy too, so the most derived type of a fully closed hierarchy is the one written and read back. A derived type that is not closed ends the inference, as its own derived types are not known.

A derived type must be at least as visible as the base type it is registered under, and two derived types cannot share a name. Reflection-based serialization throws an InvalidOperationException in those cases, and the source generator skips the derived type and reports the MFY025 diagnostic.

Merge keys

A merge key (<<) copies the entries of another mapping into the current one. Its value can be an inline mapping, an alias to an anchored mapping, or a sequence mixing both. Keys written in the mapping itself always win over merged keys, and in a merge sequence the earlier entries win over the later ones:

defaults: &defaults
  timeout: 30
  retries: 2

prod:
  <<: *defaults
  timeout: 60   # wins over the merged value

Merge keys are part of the Core and Extended schemas; they are plain keys for YamlSchemaKind.Json. An alias as the merge value requires ReferenceHandling = YamlReferenceHandling.Preserve, as aliases are only resolved in that mode. Reading the document above:

var options = new YamlSerializerOptions { ReferenceHandling = YamlReferenceHandling.Preserve };
var config = YamlSerializer.Deserialize<Dictionary<string, Dictionary<string, int>>>(yaml, options);

Assert.Equal(60, config!["prod"]["timeout"]);
Assert.Equal(2, config["prod"]["retries"]);

An alias merges the entries of the anchored mapping node as they appear in the document, the same way as an inline mapping, whatever type the anchored node was deserialized into: a member the anchored mapping leaves out is not merged. The nodes a merge alias replays count towards MaxAliasExpansionNodeCount.

C# unions

A C# union is serialized as its selected case, without a wrapper:

internal union Setting(bool, int, string);
var yaml = YamlSerializer.Serialize(new Setting(42));
Assert.Equal("42\n", yaml);

Cases are selected by YAML shape when deserializing, so a union whose cases use distinct shapes needs no configuration. A case matching the shape of the value exactly wins: Setting reads 42 as an int and "42" as a string. When no case matches exactly, the cases that can still read the value are used instead: a string case reads a plain 42 or true, a char or enum case reads a number, and a case whose type has a custom converter — declared with [YamlConverter], on the options, or on the generation options — reads any value. A nested union case matches the shapes of its own cases, and a case referencing the union itself, such as union Node(bool, Node?), never matches. The YamlSequence, YamlMapping, and YamlValue cases match sequences, mappings, and scalars; object, YamlNode, and YamlElement cases match any value.

A payload matching several cases — two cases that both serialize as a mapping, for instance — fails unless a type classifier is registered. The classifier can only select one of the matching cases. YamlUnionTypeStructuralClassifier tells mapping cases apart by their keys:

internal union Shape(Circle, Rectangle);
internal sealed class Circle { public int Radius { get; set; } }
internal sealed class Rectangle { public int Width { get; set; } public int Height { get; set; } }
var options = new YamlSerializerOptions
{
    TypeClassifiers = [new YamlUnionTypeStructuralClassifier()],
};

var shape = YamlSerializer.Deserialize<Shape>("Radius: 3", options);
Assert.Equal(3, Assert.IsType<Circle>(shape.Value).Radius);

The classifier starts with every mapping case as a candidate and eliminates the cases that do not declare a key present in the payload. Keys no case declares eliminate only the cases that reject unmapped members, and a case missing a required key is eliminated once the mapping has been read. Deserialization succeeds when exactly one candidate remains. A union declaring cases that can never be told apart is rejected when the classifier is created.

A union declaring a nullable case round-trips null to that case. When no case is nullable, null produces the default value of the union.

YamlNumberHandlingAttribute on a union applies to its numeric cases. A quoted scalar normally selects a string case, but with AllowReadingFromString a quoted number such as "42" also matches the numeric cases, and with AllowNamedFloatingPointLiterals a quoted "NaN", "Infinity", or "-Infinity" also matches the floating-point cases. A scalar matching several cases is ambiguous and needs a type classifier:

[YamlNumberHandling(YamlNumberHandling.AllowReadingFromString)]
internal union IdOrName(int, string);
Assert.Equal("alice", YamlSerializer.Deserialize<IdOrName>("\"alice\"").Value);
Assert.Equal(42, YamlSerializer.Deserialize<IdOrName>("42").Value);
Assert.Throws<YamlException>(() => YamlSerializer.Deserialize<IdOrName>("\"42\"")); // both cases match

Type classifiers

Implement YamlTypeClassifierFactory to select a union case, or a derived type, with your own rules. CanClassify receives a context whose Kind is either YamlTypeClassifierKind.Union or YamlTypeClassifierKind.PolymorphicType:

internal sealed class ShapeClassifier : YamlTypeClassifierFactory
{
    public override bool CanClassify(YamlTypeClassifierContext context)
        => context.Kind is YamlTypeClassifierKind.PolymorphicType && context.DeclaringType == typeof(Shape);

    public override YamlTypeClassifier CreateYamlClassifier(YamlTypeClassifierContext context, YamlSerializerOptions options)
        => reader =>
        {
            // The classifier reads a copy of the value: select the derived type from the first key of the mapping
            if (reader.TokenType is not YamlTokenType.StartMapping)
                return null;

            reader.Read();
            var type = reader.GetScalarValue() switch
            {
                "Radius" => typeof(Circle),
                "Side" => typeof(Square),
                _ => null,
            };

            return context.DerivedTypes.Any(derivedType => derivedType.DerivedType == type) ? type : null;
        };
}

The classifier reads a private copy of the value, so it may consume the reader. It never overrides an explicit discriminator or tag: it runs only once those have failed to resolve a type, and returning null falls back to the default derived type or the usual failure.

YamlTypeClassification.Classify and YamlTypeClassification.ClassifyBufferedNode expose the buffering logic used internally, so a classifier can inspect a node and let the deserializer replay it afterwards.

Anchors, aliases, and merge keys

The parser, the syntax tree, and the DOM handle anchors (&name), aliases (*name), and merge keys (<<) natively. The serializer applies extra rules.

Anchors

Declaring an anchor is always allowed when reading. Set AllowAnchors = false to reject any document that declares one.

Aliases

Resolving an alias requires object reference tracking, so ReferenceHandling must be Preserve or PreserveMinimal. The examples of this section use this model:

internal sealed class Config
{
    public Section? First { get; set; }

    public Section? Second { get; set; }
}

internal sealed class Section
{
    public int Timeout { get; set; }

    public int Retries { get; set; }
}
var options = new YamlSerializerOptions { ReferenceHandling = YamlReferenceHandling.Preserve };

var config = YamlSerializer.Deserialize<Config>("""
    First: &shared
      Timeout: 30
    Second: *shared
    """, options);

Assert.Same(config!.First, config.Second);

Without it, an alias throws YamlException: Aliases are not supported when deserializing into '…' unless ReferenceHandling is Preserve. Set AllowAliases = false to reject aliases outright.

When writing, ReferenceHandling decides whether shared objects become anchors and aliases:

Value Behavior
None (default) References are expanded. A cycle exceeds MaxDepth and throws.
Preserve Every object gets an anchor; repeated references become aliases. Cycles are supported.
PreserveMinimal A pre-serialization pass identifies shared and cyclic references, and only those get an anchor.
var shared = new Section { Timeout = 1 };
var yaml = YamlSerializer.Serialize(new Config { First = shared, Second = shared },
    new YamlSerializerOptions { ReferenceHandling = YamlReferenceHandling.PreserveMinimal });
First: &id001
  Timeout: 1
  Retries: 0
Second: *id001

With PreserveMinimal, IYamlOnSerializing and IYamlOnSerialized are still invoked once per object. Converters are called by both passes, though: a converter with side effects can check YamlWriter.IsCollectingReferences, which is true during the pre-serialization pass, whose output is discarded.

Merge keys

A merge key whose value is a mapping, or a sequence of mappings, is applied when reading. Later keys win over merged ones, and earlier entries of a merge sequence win over later ones.

var section = YamlSerializer.Deserialize<Section>("""
    <<: { Timeout: 30, Retries: 2 }
    Timeout: 60
    """);

Assert.Equal(60, section!.Timeout);
Assert.Equal(2, section.Retries);

Merge keys are only recognized under the Core and Extended schemas; the Json and Failsafe schemas treat << as an ordinary key. A merge key whose value is an alias additionally requires ReferenceHandling to be Preserve or PreserveMinimal.

Schemas

YamlSchemaKind selects the rules used to resolve untagged scalars:

Kind Description
Failsafe YAML 1.2 §10.1. Every plain scalar is a string.
Json YAML 1.2 §10.2. JSON-compatible resolution: a plain scalar must be null, true, false, or a JSON number.
Core YAML 1.2 §10.3. The default; adds ~, Null, TRUE, hexadecimal and octal integers, .inf, .nan.
Extended Adds y/yes/on booleans, !!timestamp, !!merge, and _ digit separators.

With Schema = Json, the writer double-quotes every scalar that is not null, a boolean, or a number (strings, keys, enum names, dates, identifiers, ...), and writes the non-finite floating-point values with an explicit !!float tag. With UseSchema = true as well, reading rejects an untagged plain scalar that is not null, true, false, or a JSON number, as the JSON schema does, so name: text throws while "name": "text" is accepted.

A binary floating-point value (double, float, Half, BFloat16) is always written so it resolves to a float: a whole number keeps a fraction (1.0), negative zero is -0.0, and the special values are .inf, -.inf, and .nan. decimal and the IEEE 754 decimal types keep the exact text of their value, whose scale is significant.

Scalar resolution goes through the selected schema only when UseSchema = true. Otherwise built-in converters use a faster, permissive span-based path, which also accepts binary integers and digit separators while honoring quoted scalars as strings. Null, booleans, .inf and .nan keep the core schema spellings in both paths, and an explicit !!null, !!bool, !!int or !!float tag is honored when reading into object; a scalar that does not match its tag throws a YamlException. Set UseSchema = true to apply the selected schema’s scalar spelling rules.

var options = new YamlSerializerOptions { UseSchema = true, Schema = YamlSchemaKind.Extended };

var values = YamlSerializer.Deserialize<Dictionary<string, object>>("enabled: yes", options);
Assert.True(values!["enabled"] is true); // true, not "yes"

The schema types (FailsafeSchema, JsonSchema, CoreSchema, ExtendedSchema) are public, derive from SchemaBase, and implement IYamlSchema. They expose tag expansion and shortening, scalar rules, and tag registration, so a custom schema can be built by deriving from one of them and overriding PrepareScalarRules.

Source generation

Declare a partial context derived from YamlSerializerContext and annotate each root type with YamlSerializableAttribute. Both are in the Meziantou.Framework.Yaml.Serialization namespace. This context serializes the Product type of Serialize and deserialize objects:

[YamlSerializable(typeof(Product))]
[YamlSourceGenerationOptions(
    PropertyNamingPolicy = YamlKnownNamingPolicy.KebabCaseLower,
    WriteIndented = true)]
public sealed partial class AppYamlContext : YamlSerializerContext
{
}
var product = new Product { Id = 1, DisplayName = "Sample product", Tags = ["new"] };

var yaml = YamlSerializer.Serialize(product, AppYamlContext.Default.Product);
Assert.Equal("id: 1\nname: Sample product\ntags:\n  - new\n", yaml);

var copy = YamlSerializer.Deserialize(yaml, AppYamlContext.Default.Product);
Assert.Equal("Sample product", copy!.DisplayName);

The context itself can be passed to the serializer:

var product = new Product { Id = 1, DisplayName = "Sample product" };

var yaml = YamlSerializer.Serialize(product, AppYamlContext.Default);
var copy = YamlSerializer.Deserialize<Product>(yaml, AppYamlContext.Default);
Assert.Equal("Sample product", copy!.DisplayName);

YamlSourceGenerationOptionsAttribute mirrors most of YamlSerializerOptions, including Converters. A context can also be constructed with a YamlSerializerOptions instance, and CreateOptions derives an options instance that keeps the context as its resolver:

var options = AppYamlContext.Default.CreateOptions(o => o with { SourceName = "config.yaml" });

The attribute values initialize the options of AppYamlContext.Default, and the generated code reads most options at run time, so an options instance with other values is honored. A few options shape the generated code instead, and an options instance cannot change them:

  • PropertyNamingPolicy is always applied at build time: the member names are generated from the policy set on the attribute, and the policy of an options instance is ignored. DictionaryKeyPolicy is read at run time.
  • DefaultIgnoreCondition, IncludeFields, IgnoreReadOnlyFields, IgnoreReadOnlyProperties, UnmappedMemberHandling, PreferredObjectCreationHandling, DuplicateKeyHandling, MappingOrder, PropertyNameCaseInsensitive, DiscriminatorStyle, TypeDiscriminatorPropertyName, UnknownDerivedTypeHandling, InferClosedTypePolymorphism, and whether Schema applies merge keys are applied at build time when the attribute sets them, and read from the options at run time otherwise. A closed hierarchy is always known at build time, so its derived types are generated even when only the options can enable inference.

YamlSerializableAttribute.TypeInfoPropertyName renames the generated property when the default name collides.

Non-public members annotated with YamlIncludeAttribute and non-public constructors annotated with YamlConstructorAttribute are supported: the generated context reaches them through UnsafeAccessorAttribute, which NativeAOT and trimming understand.

Diagnostics

Id Severity Description
MFY001 Error The context type must be declared partial.
MFY002 Error A member uses a type the generator cannot serialize.
MFY003 Error An extension data member uses an unsupported type.
MFY004 Error A type declares several [YamlExtensionData] members.
MFY005 Error A [YamlSourceGenerationOptions] value is invalid.
MFY006 Error A converter type is invalid.
MFY007 Error A serializable type, or the element or key type of a serializable collection, is not supported.
MFY020 Error [YamlDerivedTypeMapping] declares a type that is not assignable to the base type.
MFY021 Warning The base type of a [YamlDerivedTypeMapping] has no [YamlPolymorphic]; serializer defaults are used.
MFY022 Warning An open generic derived type cannot be closed for the base type and is ignored.
MFY023 Error InferClosedTypePolymorphism is enabled on a type that is not closed.
MFY024 Warning Inference is replaced by the explicit [YamlDerivedType] registrations of the type.
MFY025 Warning An inferred derived type is ignored (visibility, or duplicate discriminator).
MFY026 Error A type registers the same derived type, discriminator, or tag twice, or two default derived types.

NativeAOT and trimming

The package is annotated IsAotCompatible and IsTrimmable. Source generation avoids reflection-based metadata discovery and is the preferred mode for NativeAOT and trimming-sensitive applications.

Reflection-based serialization can be disabled entirely for applications that only use source-generated metadata, via the MeziantouFrameworkYamlIsReflectionEnabledByDefault MSBuild property:

<PropertyGroup>
  <MeziantouFrameworkYamlIsReflectionEnabledByDefault>false</MeziantouFrameworkYamlIsReflectionEnabledByDefault>
</PropertyGroup>

The property is published as a runtime host configuration option and as a trimmer feature switch, so the reflection code paths are removed from the trimmed output. It defaults to false when PublishAot or NativeAot is true; set it explicitly to override that. YamlSerializer.IsReflectionEnabledByDefault reports the effective value at runtime.

When reflection is disabled, use source-generated YamlSerializerContext metadata for typed serialization and deserialization.

Document Object Model

Use the DOM when you need to inspect or transform YAML without binding to a CLR type. Its types are in the Meziantou.Framework.Yaml.Model namespace.

var stream = YamlStream.Load(new StringReader("""
    product:
      id: 1
      name: Sample product
    """));

var document = stream[0];
var root = (YamlMapping)document.Contents!;
var product = (YamlMapping)root["product"]!;
var name = ((YamlValue)product["name"]!).Value;
Assert.Equal("Sample product", name);

YamlStream is a list of YamlDocument, so a multi-document stream is read in one pass. YamlMapping implements both IDictionary<YamlElement, YamlElement> and IList<KeyValuePair<…>>, so entries keep their document order and can be inserted at a given index. YamlMapping and YamlSequence expose Style (Block or Flow), and YamlElement exposes Anchor and Tag.

Every node supports DeepClone(), WriteTo(TextWriter), EnumerateEvents(), and bridges to the serializer:

var product = new Product { Id = 1, DisplayName = "Sample product" };

var node = YamlNode.FromObject(product);
var back = node.ToObject<Product>();
Assert.Equal("Sample product", back!.DisplayName);

A YamlNode can also be used directly as a member type, which is how [YamlExtensionData] keeps unmatched content in its original form.

Syntax tree

YamlSyntaxTree produces a lossless representation: with IncludeTrivia, comments and whitespace are preserved and ToFullString() returns the original text byte for byte. Every node and token carries a Span and a FullSpan. The syntax tree types are in the Meziantou.Framework.Yaml.Syntax namespace.

var yaml = """
    # deployment settings
    replicas: 3 # keep in sync with the chart
    """;

var tree = YamlSyntaxTree.Parse(yaml, new YamlSyntaxOptions { IncludeTrivia = true });

var tokens = new List<string>();
foreach (var token in tree.Tokens)
{
    tokens.Add($"{token.Kind} {token.Span} {token.Text}");
}

Assert.Equal(
[
    "StreamStart 0:0 ",
    "CommentTrivia 0:21 # deployment settings",
    "NewLineTrivia 21:22 \n",
    "BlockMappingStart 22:22 ",
    "Key 22:22 ",
    "Scalar 22:30 replicas",
    "Value 30:31 :",
    "WhitespaceTrivia 31:32  ",
    "Scalar 32:33 3",
    "WhitespaceTrivia 33:34  ",
    "CommentTrivia 34:63 # keep in sync with the chart",
    "BlockEnd 63:63 ",
    "StreamEnd 63:63 ",
], tokens);
Assert.Equal(yaml, tree.ToFullString());

This is the layer to use for tooling: linters, formatters, or edits that must not disturb the rest of the file.

Parser and emitter

The event-based API mirrors libyaml and is available for streaming scenarios. Parser and EventReader are in the Meziantou.Framework.Yaml namespace, and the events in Meziantou.Framework.Yaml.Events. This example lists the keys of the root mapping of each document:

var yaml = """
    name: first
    items: [1, 2]
    ---
    name: second
    """;

var parser = Parser.CreateParser(new StringReader(yaml));
var reader = new EventReader(parser);

var keys = new List<string>();
reader.Expect<StreamStart>();
while (!reader.Accept<StreamEnd>())
{
    reader.Expect<DocumentStart>();
    reader.Expect<MappingStart>();
    while (reader.Allow<MappingEnd>() is null)
    {
        keys.Add(reader.Expect<Scalar>().Value);
        reader.Skip(); // Skip the value, and its children for a collection
    }

    reader.Expect<DocumentEnd>();
}

Assert.Equal(["name", "items", "name"], keys);

EventReader also provides Peek<T>(), which reads the next event without consuming it, and Skip(untilDepth).

Emitter writes parsing events to a TextWriter and accepts an indentation width, a maximum line width, and a canonical mode.

Error handling

Every parse and binding failure derives from YamlException, which carries the Start and End marks (line, column, character index) of the offending node. SyntaxErrorException and SemanticErrorException cover scanning and parsing failures.

Set SourceName to prefix messages with the origin of the document:

var options = new YamlSerializerOptions { SourceName = "config.yaml" };

var exception = Assert.Throws<YamlException>(() => YamlSerializer.Deserialize<Product>("Id: [1, 2]", options));
Assert.Equal("config.yaml: (Lin: 0, Col: 4, Chr: 4) - (Lin: 0, Col: 4, Chr: 4): Expected a Scalar token but found 'StartSequence'.", exception.Message);

Use TryDeserialize when a failure is expected and an exception is not wanted.

Hardening untrusted input

Option Default Purpose
MaxDepth 64 Caps the nesting depth of mappings and sequences while reading and writing. 0 means the default.
AllowAnchors true Rejects documents declaring anchors.
AllowAliases true Rejects documents using aliases, which prevents alias-expansion amplification.
MaxAliasExpansionNodeCount 100000 Caps how many nodes aliases may materialize when building a YamlNode model. 0 means the default.
DuplicateKeyHandling Error Rejects mappings with duplicate keys.
UnsafeAllowDeserializeFromTagTypeName false Allows a YAML tag to name a CLR type to instantiate. Enable it only for trusted input.
var options = new YamlSerializerOptions
{
    MaxDepth = 32,
    AllowAnchors = false,
    AllowAliases = false,
    UnmappedMemberHandling = YamlUnmappedMemberHandling.Disallow,
};

Assert.Throws<YamlException>(() => YamlSerializer.Deserialize<Product>("Id: &id 1", options));
Assert.Throws<YamlException>(() => YamlSerializer.Deserialize<Product>("Price: 10", options));

The document object model expands each alias into a copy of the anchored subtree, so nesting anchors makes the node count grow exponentially while the document grows linearly. MaxDepth does not bound this, because the growth is in breadth rather than depth. MaxAliasExpansionNodeCount bounds it directly, and only counts nodes produced by alias expansion, so documents that do not use aliases are never affected.

Pass the options to YamlStream.Load when reading untrusted input into the model; the parameterless overload uses YamlSerializerOptions.Default:

var options = new YamlSerializerOptions { MaxAliasExpansionNodeCount = 1_000 };

using var reader = new StringReader("""
    base: &base { name: default }
    copy: *base
    """);

var stream = YamlStream.Load(reader, options);
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed.  net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net11.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Meziantou.Framework.Yaml:

Package Downloads
Meziantou.Framework.DependencyScanning

Find dependencies in source files. Support multiple package managers such as NuGet, npm, Docker, PyPi, and so on

Meziantou.Framework.Mermaid

Reads and writes the Mermaid text format: the 25 diagram types of Meziantou.Framework.Diagrams, and the pie, XY, quadrant, sankey and radar charts and treemaps of Mermaid drawn with Meziantou.Framework.Charts, with the front matter, the directives and the themes of Mermaid.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.12 71 11/13/2026
2.0.11 168 11/8/2026
2.0.10 416 10/28/2026
2.0.9 243 10/24/2026
2.0.8 144 10/21/2026
2.0.7 210 10/16/2026
2.0.6 219 10/11/2026
2.0.5 302 9/27/2026
2.0.4 316 9/20/2026
2.0.3 174 9/15/2026
2.0.2 132 9/13/2026
2.0.1 164 9/11/2026
2.0.0 190 9/8/2026
1.0.10 140 9/7/2026
1.0.9 179 9/6/2026
1.0.8 298 9/3/2026
1.0.7 134 9/3/2026
1.0.6 125 9/3/2026
1.0.5 341 8/23/2026
1.0.4 286 8/16/2026
Loading failed