PicoToml 2026.10.0

dotnet add package PicoToml --version 2026.10.0
                    
NuGet\Install-Package PicoToml -Version 2026.10.0
                    
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="PicoToml" Version="2026.10.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PicoToml" Version="2026.10.0" />
                    
Directory.Packages.props
<PackageReference Include="PicoToml" />
                    
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 PicoToml --version 2026.10.0
                    
#r "nuget: PicoToml, 2026.10.0"
                    
#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 PicoToml@2026.10.0
                    
#: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=PicoToml&version=2026.10.0
                    
Install as a Cake Addin
#tool nuget:?package=PicoToml&version=2026.10.0
                    
Install as a Cake Tool

PicoSerDe

AOT-first, reflection-free serialization framework. Five formats, one unified API. Source-generated ref struct readers/writers with zero heap allocation on the hot path — deployable under NativeAOT and trimming where many serialization libraries cannot run.

CI NuGet License: MIT


Modules

Format Package Status AOT Readme
JSON PicoJetson ✅ Production
MessagePack PicoMsgPack ✅ Production
INI PicoIni ✅ Production
TOML PicoToml ✅ Production
YAML PicoYaml ✅ Production

PicoYaml is the only AOT-compatible YAML library on .NET.


Test Coverage

1330 tests across all 6 modules, with cross-validation against 5 competitor libraries:

Module Tests Competitor Cross-Validation
PicoJetson 559 System.Text.Json ✅ bidirectional, all 19 property types
PicoToml 143 Tomlyn ✅ bidirectional, 20 property types, NestedList via [[key]]
PicoYaml 157 YamlDotNet ✅ bidirectional, 19 property types, DateOnly/TimeOnly conerters
PicoIni 161 Microsoft.Extensions.Configuration.Ini ✅ bidirectional, 16 property types
PicoMsgPack 157 MessagePack-CSharp ✅ map/array dual-format, 14 property types
PicoSerDe.Core 65
Integration (cross-format) 88 Ignore-condition matrix, anon types, round-trips

91 of these are strictness/robustness regression tests added in the strict-deserialization hardening pass: wrong-typed input, trailing data, missing required members, malformed documents, chunked streaming, and comment handling all fail loudly instead of silently producing defaults.

Performance Summary

Numbers below are PicoSerDe on NativeAOT vs competitors on JIT — the competitors cannot run under NativeAOT at all. In a JIT environment, mature reflection-based parsers may still be faster; PicoSerDe's advantage is guaranteed deployability under trimming and self-contained publishing, not peak JIT throughput.

Benchmarks: AOT self-contained, .NET 10, 100K iterations, win-x64.

Module vs Competitor Avg Speedup Competitor AOT?
PicoJetson System.Text.Json 1.35x
PicoMsgPack MessagePack-CSharp 1.40x
PicoIni ini-parser 0.12x
PicoToml Tommy 0.30x
PicoYaml

JSON/MessagePack are faster than or competitive with JIT-based alternatives even in AOT mode. INI/TOML/YAML prioritize correct, reflection-free parsing over peak throughput — their JIT competitors benefit from years of runtime-level optimizations (cached keys, direct span writes, dynamic code gen) that are incompatible with NativeAOT. PicoSerDe is the only option that runs at all in a fully-trimmed, self-contained NativeAOT deployment for these formats.


Design

// One API across all formats
JsonSerializer.Serialize<T>(value)      // → byte[] via PicoJetson
MsgPackSerializer.Deserialize<T>(data)  // T ← byte[] via PicoMsgPack
IniSerializer.Serialize(config)         // → string via PicoIni

Attribute-Driven Registration

PicoSerDe source generators discover types through four independent pipelines:

  1. Usage-driven — calling Serialize<T>() or Deserialize<T>() triggers generation for T
  2. Generic attribute[PicoSerializable] marks a type for all referenced format modules
  3. Format-specific attribute[PicoJsonSerializable] / [PicoIniSerializable] / etc. marks a type for one format
  4. Shorthand attribute[GenerateSerializer(typeof(T))] for central registration
// All referenced formats generate serializers
[PicoSerializable]
public class UserDto { public string Name { get; set; } }

// JSON only (PicoJetson)
[PicoJsonSerializable]
public class JsonOnlyDto { public string Label { get; set; } }

// Indirect — target type from any assembly
[PicoIniSerializable(typeof(ExternalLibrary.SharedDto))]
class Config { }

// Shorthand — equivalent to PicoSerializable(typeof(T))
[GenerateSerializer(typeof(UserDto))]
[GenerateSerializer(typeof(ProductDto))]
class PicoSerDeConfig { }
Attribute Scope Defined in
[PicoSerializable] All formats — direct or typeof(T) PicoSerDe.Core
[GenerateSerializer] Shorthand for PicoSerializable(typeof(T)) PicoSerDe.Core
[PicoJsonSerializable] JSON only PicoJetson
[PicoIniSerializable] INI only PicoIni
[PicoTomlSerializable] TOML only PicoToml
[PicoMsgPackSerializable] MsgPack only PicoMsgPack
[PicoYamlSerializable] YAML only PicoYaml

No attributes are required for basic usage — calling Serialize<T>() automatically triggers generation.

Key Features

Strict Deserialization (fail-loud, STJ-compatible semantics)

Deserialization validates input shape and types instead of silently producing default values:

  • Wrong-typed values throw FormatException{"age":"abc"} into an int property throws instead of yielding 0
  • Invalid string escapes throw — RFC 8259 only allows \" \\ \/ \b \f \n \r \t \uXXXX; unknown escapes (e.g. \q) are rejected instead of being silently unescaped (\b and \f are now decoded correctly)
  • Leading commas throw,5 and [,1] are invalid JSON and are rejected
  • Top-level null returns null for reference-type targets (STJ semantics); value-type targets throw
  • Trailing data after the document root throws
  • Missing required members throw — C# required properties are enforced at runtime across object, nested, streaming, and polymorphic paths
  • PicoDocument.IsValid performs full structural validation — mismatched brackets, bare values in objects, missing property values, and multiple root values are all rejected (token-level checks are not enough)
  • Arrays: null elements are allowed for reference-type elements and throw for value-type elements; wrong-typed elements throw
  • Comments (ReadCommentHandling.Skip): malformed comment syntax (/x) and unterminated block comments throw instead of being silently swallowed
  • Streaming (DeserializeFromStreamAsync<T>) honors the same semantics — UnmappedMemberHandling.Disallow and top-level null handling are identical to the direct path (v2026.4.11+; both paths share one generated dispatch chain). Struct DTOs and propertyless types have no streaming delegate (DeserializeFromStreamAsync<Struct> throws InvalidOperationException)
  • MsgPack rejects empty payloads and trailing bytes — a zero-length buffer or bytes after the root value throw FormatException
  • INI list values parse strictly per element kind — malformed numbers, booleans, dates, etc. throw FormatException (never silently default to 0/ false), and formatting/parsing is culture-invariant

Polymorphic Deserialization (Type Discriminator)

Base types declare derived types at compile time. Zero reflection, AOT-safe. Since v2026.3.0.

[PicoSerializable]
[PicoDerivedType(typeof(MessageEntry), "message")]
[PicoDerivedType(typeof(CompactionEntry), "compaction")]
abstract class SessionEntry { }

class MessageEntry : SessionEntry { public string Content { get; set; } = string.Empty; }
class CompactionEntry : SessionEntry { public int From { get; set; } }

var json = """{"$type":"message","Content":"hello"}"""u8;
var result = JsonSerializer.Deserialize<SessionEntry>(json);
// result is MessageEntry at runtime
Feature Support
Serialization + Deserialization ✅ v2026.3.0
Streaming (PipeReader) ✅ v2026.3.2
Base class properties ✅ v2026.3.3
[JsonConstructor] on derived types
Record derived types ✅ v2026.3.23
Complex/collection ctor params ✅ v2026.3.24
TOML / YAML poly support ✅ v2026.3.24
INI / MsgPack poly support ✅ v2026.4.0

DOM Layer (PicoDocument / PicoElement)

Schema-less JSON inspection without System.Text.Json. Zero-copy.

var doc = PicoDocument.Parse("""{"name":"Alice","age":30}"""u8.ToArray());

var name = doc.RootElement["name"].GetString();         // "Alice"
var ok   = doc.RootElement.TryGetProperty("age", out _); // true
bool valid = PicoDocument.IsValid("{}"u8);               // true

// Numeric access
long big = doc.RootElement["count"].GetInt64();
double d = doc.RootElement["score"].GetDouble();
if (doc.RootElement["age"].TryGetInt32(out int age))
    Console.WriteLine(age);

C# Records

record and record struct types are fully supported. Primary constructor auto-detected — no [JsonConstructor] needed. init-only properties work correctly.

Top-Level Arrays

Serialize<T[]>(...) / Deserialize<T[]>(...) and streaming DeserializeFromStreamAsync<T[]>(stream) work directly.

Arrays and Dictionaries

Records (record/record struct primary constructors) work in every format — JSON, MsgPack, TOML, YAML and INI — without a format-specific constructor attribute.

Field-level object arrays (TObject[], List<TObject[]>, TObject[][]) round-trip in JSON, MsgPack, TOML and YAML (INI drops object collections because its format is flat). A YAML nested-object member that itself contains an object sequence is dropped with PICOSERDE004 (the inner helper cannot read it); TOML keeps its documented loud "nested tables deeper than one level" error. TOML arrays of tables have no chunk-resume strategy, so their streaming delegate is not registered — DeserializeFromStreamAsync then buffers the stream and uses the synchronous path (correct, just not incremental). Dictionaries accept scalar, object and nested-dictionary values; a collection dict value (Dictionary<string, List<T>>) is dropped with PICOSERDE004 instead of generating broken code.

Nullable Elements

List<int?>, List<string?>, List<TObject?>, Dictionary<string, TObject?> and nested lists of objects round-trip in JSON and MsgPack, including null elements. TOML and YAML have no null type: null scalar elements are skipped on write, while shapes they cannot express (List<int?>, nullable object/dict elements, nested lists) are dropped with the PICOSERDE004 warning. INI cannot represent any nullable element and drops those members the same way.

Streaming (incremental, chunked)

DeserializeFromStreamAsync<T>(stream, options, ct) reads a stream incrementally: consumed bytes are released as tokens complete and only the current token/member window is retained, so the document is never buffered wholesale before parsing. JSON/TOML/YAML/INI accept an options argument (JsonOptions/TomlOptions/YamlOptions/IniOptions); MsgPack takes (stream, ct) because it has no options type.

await using var stream = File.OpenRead("config.toml");
var config = await TomlSerializer.DeserializeFromStreamAsync<ServerConfig>(stream);

The generated delegate returns a ReadStatus:

Status Meaning
Success The value is complete
NeedMoreData The current chunk ends inside a token/section; the runner refills and resumes from the reader's mark
EndOfInput The document contains no value — the runner throws FormatException

Empty-stream semantics match the synchronous path: TOML/YAML/INI treat an empty stream as an empty object; JSON throws FormatException ("the document contains no value").

Recursive DTOs: JSON and MsgPack support self- and mutually-referencing types (cycle-safe extraction + seeded helpers), including recursive members on polymorphic bases — nested values keep their derived (discriminator) type in both the sync and chunked streaming paths. TOML/YAML/INI cannot express unbounded nesting, so the recursive member is skipped and the source generator reports PICOSERDE003 (never silent). Data-level object graph cycles fail loudly instead of overflowing the stack.

Polymorphic hierarchies: a concrete polymorphic base instance serializes with a synthesized discriminator (the base type name, collision-safe) so base instances round-trip as the base type; derived instances keep their declared discriminator. They also keep nested object/dict members and collection members (List<T>, Dictionary<string,T>) of derived types in JSON, MsgPack, TOML and YAML (previously dropped or non-compiling in the discriminator branches); INI ignores nested members because its format is flat.

Diagnostics (warnings, emitted by all five generators): PICOSERDE002 — two distinct types produced the same generated file name (main hint); internal helper names get a stable hash suffix (UniqueName). PICOSERDE003 — a recursive member was skipped by a section-based format. PICOSERDE004 — a member shape the format cannot represent (nested list, nullable value-type element, null object element) was skipped.

Custom/advanced registration (scripts or hand-written serializers): JsonSerializer.RegisterStreaming<T>(StreamingFunc<JsonReader, T> func) with where T : notnull; HasStreamingDelegate<T>() reports whether a delegate is registered. The source generator emits this registration in a ModuleInitializer for every discovered type.

Three-Layer Test Structure

PicoJetson tests are split into Unit / Integration / Functional projects with clear boundaries.

No non-generic Serialize(Type, object?) overloads. PicoSerDe is designed for AOT-first usage where all types are known at compile time. SerRegistry<TFormat, T> static fields (PicoSerDe.Core) are shared across assemblies and provide faster lookup than a ConcurrentDictionary<Type, ...>. Framework wrappers should call the generic API internally — the type's serializer is guaranteed to be registered via ModuleInitializer as long as the type was discovered by any pipeline (usage-driven, attribute, or shorthand).

┌──────────────────────────────────────────────┐
│                 User Code                     │
└──────────────────┬───────────────────────────┘
                   │  Static SerRegistry<TFormat, T>
┌──────────────────▼───────────────────────────┐
│           PicoSerDe.Core                      │
│  ISerializer<T>  │  IDeserializer<T>         │
│  SerRegistry     │  DesRegistry              │
│  TokenType       │  SimdHelpers (Vector128)  │
│  TextHelpers     │  SerializerExtensions     │
└────┬────────┬─────────┬─────────┬─────────┬──┘
     │        │         │         │         │
 PicoJetson  PicoIni  PicoMsgPack PicoToml PicoYaml
   ││       ││         ││        ││       ││
  .Gen     .Gen       .Gen      .Gen     .Gen      ← embedded in each runtime nupkg
  • One-package install: each format → runtime library (net10.0) with its source generator (netstandard2.0) embedded in the nupkg (analyzers/dotnet/cs + build/PicoX.targets) — a single PackageReference is enough; standalone .Gen packages are legacy/optional
  • ref struct readers/writers — stack-allocated, zero heap allocation on hot path
  • Static SerRegistry<TFormat, T> — per-format registries in PicoSerDe.Core; JIT/AOT inlineable, no dictionary lookups
  • file struct generated implementations — devirtualization without sealed class overhead
  • Ref struct serializationref struct types are supported as serializable types across all 5 formats. Source-generator-generated static methods + delegate dispatch bypass the ISerializer<T> interface constraint.
  • JsonOptions — runtime configuration (indentation, naming policy, ignore conditions, etc.) passed explicitly per call (no ambient ThreadStatic state; options thread through reader/writer instances and SG-generated code)
  • Polymorphic deserialization — type discriminator dispatch via [PicoDerivedType]; serialization + deserialization + streaming (v2026.3.0); record types (v2026.3.23); TOML/YAML poly (v2026.3.24); INI/MsgPack poly (v2026.4.0)
  • Anonymous type serializationSerialize(new { A = 1, B = "x" }) with nested types, collections, PropertyNamingPolicy.CamelCase, DefaultIgnoreCondition.WhenWritingNull, and MaxDepth enforcement. Works across all 5 formats via C# 12 interceptors + unsafe field access (serialization only, C# 12+, <AllowUnsafeBlocks>true</AllowUnsafeBlocks>) (v2026.4.1)
  • PicoDocument / PicoElement — zero-copy JSON DOM for schema-less inspection (v2026.3.4)
  • C# records — primary constructor auto-detection, init-only support (v2026.3.3); poly+record (v2026.3.23); complex/collection ctor params (v2026.3.24)
  • Top-level arraysSerialize<T[]>() / Deserialize<T[]>() with streaming (v2026.3.2)
  • Top-level scalars / Nullable<T> are not supportedSerialize<Guid>(...), Deserialize<int?>(...) etc. fail loudly with InvalidOperationException ("no serializer registered") instead of emitting an empty object or non-compiling generated code
  • Deep object nesting — JSON/YAML/MsgPack round-trip objects nested 3+ levels; INI/TOML currently support a single nested level and throw NotSupportedException beyond that (loudly, instead of silently losing data).
  • INI nested object lists — a List<SomeDto> property cannot be represented by INI's flat sections and is ignored by IniSerializer (no compile break); scalar lists (List<int>, List<string>, ...) round-trip.

PicoJetson JsonOptions

// Compact (default) — optimal for data transfer
byte[] data = JsonSerializer.SerializeToUtf8Bytes(model);

// Human-readable
byte[] data = JsonSerializer.SerializeToUtf8Bytes(model,
    new JsonOptions { Indented = true });

// CamelCase naming
byte[] data = JsonSerializer.SerializeToUtf8Bytes(model,
    new JsonOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });

// Skip null properties
byte[] data = JsonSerializer.SerializeToUtf8Bytes(model,
    new JsonOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });

// Allow NaN/Infinity
byte[] data = JsonSerializer.SerializeToUtf8Bytes(model,
    new JsonOptions { NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals });

Available options:

Option Default Description
Indented false Human-readable indented output
MaxDepth 63 Maximum nesting depth
PropertyNamingPolicy null Naming policy: CamelCase, SnakeCaseLower, KebabCaseLower, PascalCase
DefaultIgnoreCondition Never Skip null/default properties: WhenWritingNull, WhenWritingDefault
NumberHandling Strict Allow named floats: AllowNamedFloatingPointLiterals
PropertyNameCaseInsensitive true Property matching is case-insensitive by default; set false for exact case-sensitive matching
AllowTrailingCommas false Accept trailing commas in objects/arrays
ReadCommentHandling Disallow Skip // and /* */ comments
UnmappedMemberHandling Skip Throw on unknown properties: Disallow
Null handling across formats

Every format's options class (JsonOptions, YamlOptions, TomlOptions, IniOptions, MsgPackOptions) exposes DefaultIgnoreCondition, but what "writing a null" means depends on the wire format:

Format Default (Never) WhenWritingNull
JSON "key":null written omitted
MsgPack nil written (map count adjusts automatically) omitted
TOML / INI omitted — these formats have no null literal omitted
YAML omitted — the reader has no null-literal support yet; writing key: would read back as a default value and break round-trip fidelity omitted

The matrix applies to every emit path — top-level members, nested objects, collection elements, nullable collections, and polymorphic dispatch — and is locked by cross-format regression tests (IgnoreConditionMatrixTests).

Honored options: JSON and MsgPack read DefaultIgnoreCondition from their options objects. INI/TOML/YAML always omit nulls because those wire formats have no null literal; their DefaultIgnoreCondition property is accepted by the public API but the generated code treats nulls as always omitted. Indented = true is honored: YAML additionally indents sequence items under their key, TOML indents table contents, and INI indents section contents (the compact default is unchanged).

Per-property control is available via the cross-format [PicoIgnore] attribute (PicoSerDe.Core):

[PicoIgnore]                                                  // stripped everywhere (write + read)
public string Internal { get; set; } = "";

[PicoIgnore(Condition = PicoIgnoreCondition.WhenWritingNull)] // omitted only when null, regardless of global options
public string? Note { get; set; }

[PicoIgnore(Condition = PicoIgnoreCondition.Never)]           // exempt from the global DefaultIgnoreCondition
public string? Pinned { get; set; }

Conditions affect serialization only — deserialization still maps conditional properties. Format-specific markers ([JsonIgnore], [YamlIgnore], …) remain single-format unconditional ignores.

Custom serializers for nested types

Register applies at the top level only. To also override T wherever it appears as a nested value (object property, list element, dictionary value), use RegisterCustom — available on JSON and MessagePack:

JsonSerializer.RegisterCustom(new MySerializer(), new MyDeserializer());
// Outer { Foo Inner } now serializes Inner with MySerializer too.
// Deserialization override applies at the top level only.

Shared Attribute Hierarchy

Per-format attributes ([JsonIgnore], [IniKey], ...) inherit shared PicoSerDe.Core bases (PicoIgnoreAttribute, PicoSerializableAttribute, PicoCamelCaseAttribute, PicoConstructorAttribute, PicoDateTimeFormatAttribute, PicoConverterAttribute) — one implementation per concept, format-specific public names preserved. IniKeyAttribute.Key is the canonical property (Name is an obsolete alias).

Packages

Package NuGet
PicoSerDe.Core NuGet
PicoJetson NuGet
PicoMsgPack NuGet
PicoIni NuGet
PicoToml NuGet
PicoYaml NuGet

Each runtime package embeds its generator (analyzers/dotnet/cs); the standalone *.Gen packages are legacy/optional — installing both would load a duplicate analyzer.


CI/CD

Target Runner
win-x64 windows-latest
win-arm64 windows-latest
linux-x64 ubuntu-latest
linux-arm64 ubuntu-24.04-arm
osx-arm64 macos-latest

Every push: build + test (1330 tests) + 5 benchmarks smoke + 5 AOT sample publishes. Release: v* tag → packs 11 packages in dependency order → NuGet.org. Local feed: run ./scripts/release.ps1 -Version <ver> before pushing the tag — it runs the test suite, packs all 11 packages into artifacts/nupkg (declared as the local NuGet source in NuGet.config), then tags and pushes. Sibling PicoHex repos add the same folder path after nuget.org in their NuGet.config to consume the new version instantly, bypassing nuget.org's indexing window and 30-minute HTTP cache.

AOT tiers - declare <AotOptimizationLevel> per project (minimal default / aggressive for samples/benchmarks). Implementations: minimal in Directory.Build.props (PublishAot + TrimMode=full), aggressive in Directory.Build.targets (adds StackTraceSupport=false + UseSystemResourceKeys=true; classic Ilc* MSBuild properties are removed in .NET 10 SDK >= 10.0.400 and silently ignored) - the aggressive block lives in targets so a csproj-level declaration is visible when it evaluates. Libraries declare IsAotCompatible+IsTrimmable; only source generators (PicoXxx.Gen, netstandard2.0) never AOT.


Comparison

PicoSerDe S.T.Json YamlDotNet VYaml MsgPack-CS Tommy
Formats 5 1 1 1 1 1
AOT ⚠️
Zero-reflection
Zero annotations
ref struct readers
SIMD
JSON DOM
Polymorphic

License

MIT

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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on PicoToml:

Package Downloads
PicoCfg.Toml

TOML configuration source for PicoCfg via PicoToml

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2026.10.0 68 9/18/2026
2026.9.14 122 9/14/2026
2026.5.3 94 9/11/2026
2026.5.2 99 9/11/2026
2026.5.1 99 9/9/2026
2026.5.0 141 8/24/2026
2026.4.11 115 8/23/2026
2026.4.10 110 8/23/2026
2026.4.9 99 8/23/2026
2026.4.8 113 8/23/2026
2026.4.7 269 8/16/2026
2026.4.6 111 8/13/2026
2026.4.5 102 8/13/2026
2026.4.4 107 8/11/2026
2026.4.3 103 8/11/2026
2026.4.2 121 7/22/2026
2026.4.1 119 7/22/2026
2026.4.0 120 7/18/2026
2026.3.25 118 7/13/2026
2026.3.24 117 7/13/2026
Loading failed