Table of Contents

Bodu.Text.Serialization

Bodu.Text.Serialization

Bodu.Text.Serialization is the shared, format-agnostic core that every Bodu text serializer builds on. It contains no parser and no writer - only the vocabulary a model declares once and every serializer honors: the attribute family, the naming policies, the ignore / object-creation / unmapped-member enums, and the four serialization-callback interfaces. Six packages reference it - the three System.Text.Json-shaped serializers, Bodu.Text.Bencode, Bodu.Text.Toml, and Bodu.Text.Yaml, and the three line formats, Bodu.Text.Delimited, Bodu.Text.DotEnv, and Bodu.Text.Ini.

Part of the Text & Serialization topic.

Why a separate package

A model annotated with [PropertyName], [Ignore], or [Required] is making a statement about its wire shape, not about any one format. Keeping those declarations in a package that depends only on Bodu.Core means:

  • Annotate once, serialize anywhere. The same DTO round-trips through TomlSerializer, YamlSerializer, BencodeSerializer, IniSerializer, DotEnvSerializer, or DelimitedSerializer with no per-format attributes.
  • Model libraries stay format-free. A project that only declares DTOs references Bodu.Text.Serialization alone; the application that chooses TOML today and YAML tomorrow references the format package. The model assembly never takes a dependency on a reader or writer it does not use.
  • One precedence story. Member attribute beats type attribute beats options - the same rule in every serializer, documented once in Core concepts.

The attribute family

Every attribute derives from the abstract SerializationAttribute, so the family can be discovered as a unit. All live in the Bodu.Text.Serialization namespace; add using Bodu.Text.Serialization; to the model file.

Attribute Applies to Effect
PropertyNameAttribute property, field Pins the wire key for one member, overriding the CLR name and any naming policy.
IgnoreAttribute property, field Excludes the member - unconditionally (Condition defaults to Always) or only when writing null / the default value.
IncludeAttribute property, field Forces a member in: binds non-public accessors ({ get; private set; }, { get; init; }) and surfaces a public field even when IncludeFields is off.
PropertyOrderAttribute property, field Sets the relative order in which members are presented to the writer (ascending; unannotated members are 0 and keep declaration order).
RequiredAttribute property, field Fails deserialization when the key is absent - the same effect as the C# required keyword.
ConverterAttribute property, field, class, struct, enum Names the converter type for a member (governs that member) or a type (governs every use).
ExtensionDataAttribute property, field Marks the one dictionary-shaped member that captures keys mapping to no other member, and writes them back out.
ConstructorAttribute constructor Selects the constructor used during deserialization when a type declares more than one.
NamingPolicyAttribute class, struct, interface Applies one of the built-in policies to the annotated type's members, overriding the options-level policy.
UnmappedMemberHandlingAttribute class, struct, interface Chooses, per type, whether an unknown key is skipped or rejected.
ObjectCreationHandlingAttribute class, struct, interface, property, field Chooses whether a collection member is replaced with a new instance or populated in place.
StringEnumMemberNameAttribute enum field Sets the string written for one enumeration member when the enum is serialized by name.

Naming policies

NamingPolicy is the abstract base with a single ConvertName(string) method and five ready-made singletons: CamelCase, SnakeCaseLower, SnakeCaseUpper, KebabCaseLower, and KebabCaseUpper. Assign one to a serializer's PropertyNamingPolicy option, or select it declaratively on a type through [NamingPolicy(KnownNamingPolicy.…)] - the KnownNamingPolicy enum names each singleton (plus Unspecified). Subclass NamingPolicy for a convention of your own.

NamingPolicy.SnakeCaseUpper.ConvertName("MaxRetryCount");   // "MAX_RETRY_COUNT"
NamingPolicy.KebabCaseLower.ConvertName("HTTPServerPort");   // "http-server-port"

The separator policies split at a lowercase-to-uppercase boundary and at the end of an acronym, so HTTPServer becomes http_server, not h_t_t_p_server. CamelCase lowercases only the first character.

Behavior enums

Enum Values Read through
IgnoreCondition Never, Always, WhenWritingDefault, WhenWritingNull [Ignore(Condition = …)] on a member, or the options-level DefaultIgnoreCondition.
ObjectCreationHandling Replace, Populate [ObjectCreationHandling(…)] on a member or type, or the options-level PreferredObjectCreationHandling.
UnmappedMemberHandling Skip, Disallow [UnmappedMemberHandling(…)] on a type, or the options-level UnmappedMemberHandling.

There is deliberately no number-handling enum in this package: how a numeric scalar is written is a per-format decision (YAML, for example, has its own YamlNumberHandling option), and the line formats carry every value as text.

Serialization callbacks

Four interfaces give a type a hook at each edge of the mapping pipeline: IOnSerializing (before members are written), IOnSerialized (after), IOnDeserializing (after construction, before members are assigned), and IOnDeserialized (after every member is assigned - the natural place for cross-member validation). Each has one parameterless void method; implement them explicitly to keep the hooks off the type's public surface. The exact position of each hook is tabulated in Core concepts.

Who uses what

The package is consumed in two distinct ways, and knowing which one your serializer takes explains which attributes it honors.

Feature Bencode · TOML · YAML Delimited · DotEnv · INI
[PropertyName], [Ignore], [PropertyOrder] yes yes (Delimited honors [Ignore] unconditionally; DotEnv and INI also honor the conditional forms and DefaultIgnoreCondition)
[Required] / C# required yes DotEnv and INI ([Required]); not Delimited
Options-level PropertyNamingPolicy, PropertyNameCaseInsensitive, IncludeFields yes yes
Type-level [NamingPolicy] yes no
[Include], [Constructor], [ExtensionData], [UnmappedMemberHandling], [ObjectCreationHandling] yes no
[Converter], custom converters, [StringEnumMemberName] yes no - scalars are strings converted with the invariant culture
The four callback interfaces yes yes

The structured serializers (Bencode, TOML, YAML) compile the recursive converter engine - the metadata resolver, the object / collection / dictionary / nullable / enum converter factories, and the options-resolution partial - from this package's shared/** source tree under a per-format symbol. That engine is what reads the full attribute family, selects constructors, captures extension data, and resolves converters. The line formats reference only the compiled assembly: their wire is string-only, so each ships a small serializer-local binder that reads the attributes and naming policy and converts scalars with InvariantCulture - there is no converter pipeline to plug into.

Headline types

Type Purpose
SerializationAttribute Abstract base of the whole attribute family.
PropertyNameAttribute / IgnoreAttribute / RequiredAttribute / PropertyOrderAttribute The member-shaping attributes every serializer honors.
NamingPolicy / KnownNamingPolicy / NamingPolicyAttribute Wire-name conventions: the policy base and singletons, the enum that names them, and the type-level selector.
IgnoreCondition / ObjectCreationHandling / UnmappedMemberHandling The behavior enums shared by the attributes and the per-format options.
IOnSerializing / IOnSerialized / IOnDeserializing / IOnDeserialized The lifecycle callbacks.
ConverterAttribute / ConstructorAttribute / ExtensionDataAttribute / IncludeAttribute The structural attributes read by the recursive engine.

Where to go next