Table of Contents

Mapping attributes

The TOML serializer honours the attribute family from the shared Bodu.Text.Serialization package for shaping how a type maps to the wire: every attribute derives SerializationAttribute. The sibling libraries (Bodu.Text.Bencode, Bodu.Text.Yaml) honour the very same attributes - there is no per-format prefix - see the serializer guides hub. Each pattern below shows the TOML form and its output.

Pattern 1 - Rename a member

PropertyNameAttribute pins the serialized key for one member, beating any naming policy:

public sealed class Profile
{
    [PropertyName("display-name")]
    public string DisplayName { get; set; } = "Ada";
}
display-name = "Ada"

Pattern 2 - Apply a naming policy per type

NamingPolicyAttribute overrides the options-level PropertyNamingPolicy for one type:

[NamingPolicy(KnownNamingPolicy.SnakeCaseLower)]
public sealed class RetryPolicy
{
    public int MaxRetryCount { get; set; } = 5;
}
max_retry_count = 5

A member carrying [PropertyName] is unaffected - the explicit name always wins. KnownNamingPolicy.Unspecified defers back to the options.

Pattern 3 - Exclude members

IgnoreAttribute drops a member unconditionally, or under a condition:

public sealed class Account
{
    public string Name { get; set; } = "svc";

    [Ignore]
    public string? Secret { get; set; }

    [Ignore(Condition = IgnoreCondition.WhenWritingNull)]
    public string? Comment { get; set; }
}
Name = "svc"

Secret is never written; Comment appears only when non-null. WhenWritingDefault extends the rule to default value-type values.

Pattern 4 - Force a member in

IncludeAttribute binds non-public property accessors and surfaces public fields without turning on IncludeFields for the whole options:

public sealed class Counter
{
    [Include]
    public int Total { get; private set; }

    [Include]
    public int Retries;
}

Both members now round-trip - the private setter is assigned on read, and the field participates like a property, following the same naming, ordering, ignore, required, and converter rules.

Pattern 5 - Control write order

PropertyOrderAttribute reorders the emitted key/value lines; members without the attribute default to order zero and keep declaration order:

public sealed class Manifest
{
    [PropertyOrder(2)]
    public string Name { get; set; } = "demo";

    [PropertyOrder(1)]
    public int Version { get; set; } = 3;
}
Version = 3
Name = "demo"

Pattern 6 - Require a key

RequiredAttribute makes deserialization fail when the key is absent, with the same effect as declaring the member with the C# required keyword:

public sealed class ServerConfig
{
    [Required]
    public string Host { get; set; } = string.Empty;
}

// Input without a "Host" key throws TomlSerializationException.

Pattern 7 - Pick the deserialization constructor

When a type declares more than one constructor, ConstructorAttribute resolves the ambiguity:

public sealed class Endpoint
{
    [Constructor]
    public Endpoint(string host, int port) => (Host, Port) = (host, port);

    public Endpoint(Uri uri) : this(uri.Host, uri.Port) { }

    public string Host { get; }
    public int Port { get; }
}

Without the attribute the serializer prefers a public parameterless constructor, then a single declared constructor, then the constructor with the most parameters.

Pattern 8 - Capture unknown keys

ExtensionDataAttribute designates one member that collects every key that maps to no other member, and writes the collected entries back out on serialization:

public sealed class ServerConfig
{
    public int Port { get; set; }

    [ExtensionData]
    public Dictionary<string, TomlNode>? Extra { get; set; }
}

The member must be a TomlObject or an (I)Dictionary<string, TomlNode>, and a type may declare at most one.

Pattern 9 - Reject unknown keys

UnmappedMemberHandlingAttribute chooses, per type, between skipping a key that maps to no member (the default) and failing:

[UnmappedMemberHandling(UnmappedMemberHandling.Disallow)]
public sealed class StrictConfig
{
    public int Port { get; set; }
}

// Input containing an unrecognised key throws TomlSerializationException.

A type with an extension-data member (Pattern 8) still captures unmapped keys into that member regardless of this setting.

Pattern 10 - Populate instead of replace

ObjectCreationHandlingAttribute controls whether deserialization replaces a member's value with a fresh instance or populates the instance already held - useful for get-only collection properties:

public sealed class Pipeline
{
    [ObjectCreationHandling(ObjectCreationHandling.Populate)]
    public List<string> Steps { get; } = new() { "restore" };
}

Deserialized entries are appended to the existing list instead of replacing it. The attribute applies to a member or a whole type; member beats type, and both beat the options-level PreferredObjectCreationHandling.

Pattern 11 - Choose a converter

ConverterAttribute selects the converter for a member, or for every use of a type:

public sealed class Package
{
    [Converter(typeof(VersionConverter))]
    public Version Version { get; set; } = new(1, 2, 3);
}
Version = "1.2.3"

The referenced type must derive from the format's converter base and expose a public parameterless constructor. Writing converters - including the factory pattern and resolution order - is covered in Writing converters.

Pattern 12 - Name enum members

StringEnumMemberNameAttribute renames an individual enumeration member when the enum is serialized by name:

[Converter(typeof(TomlStringEnumConverter<Status>))]
public enum Status
{
    Active,

    [StringEnumMemberName("on-hold")]
    OnHold,
}
Status = "on-hold"

The attribute applies only to by-name serialization (the default enum handling, or an explicit string-enum converter); it is a no-op when the enum is written as an integer.

Precedence at a glance

When several settings could govern the same member, the closest one wins:

  1. a member-level attribute ([PropertyName], [Ignore], [Converter], [ObjectCreationHandling], …);
  2. a type-level attribute ([NamingPolicy], [Converter], [UnmappedMemberHandling], [ObjectCreationHandling]);
  3. the serializer options (PropertyNamingPolicy, Converters, UnmappedMemberHandling, PreferredObjectCreationHandling, IncludeFields).

See also