Anchors, aliases and merge keys

edit

YAML can name a node with an anchor (&name) and repeat it with an alias (*name). The serializer uses them for shared references when ReferenceHandling asks for it. The examples of this page 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; }
}

Write shared references

By default, an object referenced twice is written twice, and a cycle fails when it reaches MaxDepth. ReferenceHandling changes this:

ValueBehavior
None (default)References are written in full every time. A cycle throws.
PreserveEvery object gets an anchor, and the next references to it become aliases. Cycles are supported.
PreserveMinimalOnly the objects referenced more than once, or part of a cycle, get an anchor.
var shared = new Section { Timeout = 1 };
var options = new YamlSerializerOptions { ReferenceHandling = YamlReferenceHandling.PreserveMinimal };

var yaml = YamlSerializer.Serialize(new Config { First = shared, Second = shared }, options);
Assert.Equal("""
    First: &id001
      Timeout: 1
      Retries: 0
    Second: *id001

    """, yaml);

var config = YamlSerializer.Deserialize<Config>(yaml, options);
Assert.Same(config!.First, config.Second);

PreserveMinimal gives the more readable output. To find the shared objects, it serializes the value twice: the first pass only collects the references, and its output is discarded. The serialization callbacks still run once per object, but converters run in both passes: a converter with side effects can check YamlWriter.IsCollectingReferences, which is true during the first pass.

Read aliases

Reading an alias requires ReferenceHandling to be Preserve or PreserveMinimal. The alias then gives the same instance as its anchor:

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);

With the default None, a document that contains an alias throws a YamlException that says that aliases need ReferenceHandling to be Preserve. An anchor without alias is always accepted. To reject documents that use anchors or aliases at all, see Reading untrusted YAML.

Merge keys

A merge key (<<) copies the entries of other mappings into the current one. Its value is a mapping, an alias to a mapping, or a sequence of them. The keys of the mapping itself win over the merged ones, and in a sequence the first mappings win over the next ones:

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

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

A merge key is usually an alias to a block of defaults:

var yaml = """
    defaults: &defaults
      timeout: 30
      retries: 2

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

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"]);