nino-serialization · git:20261101.c3ed1e7 · 2026-11-01 · sha256 a1fb697b1b20afd5
nino-serialization git:20261101.c3ed1e7A
Immutable. This exact content never changes and is served at /api/v1/blob/a1fb697b1b20afd5.
---
name: nino-serialization
description: "Write correct, fast code with Nino v4 (the source-generated binary serializer for C# on NuGet as Nino, in Unity from OpenUPM) - [NinoType] classes, structs, records and interfaces, member order, constructors, positional / version-tolerant / keyed formats, polymorphism, reference tracking, [NinoUtf8], [NinoPooled] and reuse deserialization, custom formatters, NinoSettings for untrusted input, ASP.NET Core bodies, and TypeScript, Go and Rust codecs from nino-gen - and fix what the NINO analyzer reports. Use when writing, reviewing or migrating code that uses Nino, or choosing a binary format for .NET, Unity, Godot or HybridCLR data. Triggers: a project referencing Nino, Nino.Serialization, Nino.Core, Nino.AspNetCore or Nino.Codegen, [NinoType], NinoSerializer, NinoDeserializer, NinoDataException, a NINO0xx or NINOGEN diagnostic, application/x-nino, nino-gen, 用 Nino 序列化, Nino 反序列化, 二进制序列化, 给类型加 NinoType."
---
# Nino rules for AI assistants
Rules for writing code that uses Nino v4 (namespace `Nino.Core`). Docs: https://nino.xgamedev.net/en/docs/v4 (pages
for models: https://nino.xgamedev.net/llms.txt, full text: https://nino.xgamedev.net/llms-full.txt). Every page is
also served as Markdown: add `.md` to its URL (`https://nino.xgamedev.net/en/docs/v4/start.md`). Per-id diagnostics:
[references/diagnostics.md](references/diagnostics.md); TypeScript, Go and Rust: [references/other-languages.md](references/other-languages.md).
## Install
- .NET and Godot 4 (.NET): `dotnet add package Nino --prerelease` in **every project that declares `[NinoType]`s**, so
the generator runs there. v4 is a prerelease: without `--prerelease` NuGet installs 3.9.x, which cannot read or
write v4 data. `Nino` brings `Nino.Serialization` (the runtime, `Nino.Core.dll`) and `Nino.Generator` (generator,
analyzers, code fixes).
- Optional: `Nino.Immutable` (`System.Collections.Immutable` types), `Nino.AspNetCore` (`application/x-nino` bodies,
`net8.0`/`net10.0`), `Nino.Codegen` (TypeScript, Go or Rust after each build), and the `nino-gen` tool
(`dotnet tool install -g Nino.Codegen.Tool --prerelease`). All with `--prerelease`.
- Unity 2022.3+: `openupm add com.jasonxudeveloper.nino` (runtime, generator, code fixes and `Nino.UnityTypes.dll`).
- Requirements: Roslyn 4.3+ for the generator and `LangVersion` 9 or later in the declaring project (old .NET
Framework projects default to 7.3). `Nino.Core` targets `netstandard2.0`, `netstandard2.1`, `net8.0`, `net10.0`; every
target, runtime and CPU writes identical bytes.
- Nothing to register or initialize: generated registries load on first use. Optional startup calls only:
`NinoSettings`, `NinoPolymorphic.Register<T>()`, `NinoRegistry.EnsureInitialized(assembly)`.
## Declaring types
- `[NinoType]` on a `class`, `struct`, `record`, `record struct` or `interface`. The type and every type containing it
are `public` or `internal`, never a `static` class or `ref struct` (NINO001).
- Options: `[NinoType(autoCollect: true, includeNonPublicMembers: false, allowInheritance: true, format:
NinoFormat.Positional, trackReferences: false)]`; use named arguments.
- With `autoCollect: true` every public field and every public property with a getter and a `set`/`init` is written:
`[NinoType]` base classes first (root first), then the type's own members in declaration order. Get-only
auto-properties are skipped unless a constructor parameter has the same name (ignoring case); explicit interface
implementations are skipped; a member of a type Nino cannot write is skipped with NINO007.
- `[NinoIgnore]` drops a member. `[NinoType(false)]` writes only `[NinoMember(index)]` members, by index ascending
(`[property: NinoMember(n)]` on record parameters). Don't mix them up: NINO010 to NINO013.
- Non-public members need `includeNonPublicMembers: true` (a non-public `[NinoMember]` without it: NINO009). Before
.NET 8 (Unity, .NET Framework, `netstandard`), and for generic types before .NET 9, the type and every containing type
must be `partial` (NINO002). Write `partial` on such types anyway when the code may build for Unity.
- Subtypes inherit `[NinoType]` and its options from the nearest `[NinoType]` base class or interface. A subtype's own
`[NinoType]` replaces the inherited options (repeat `format:`); `trackReferences` is inherited regardless.
`allowInheritance: false` makes subtypes declare their own (NINO003).
- Generic types work (`Box<T>`); type arguments must be serializable. A closed generic type the process only *reads*
through a base and never names: `NinoPolymorphic.Register<Box<int>>()` at startup.
- Strings: one byte per char when every char is at most U+00FF, UTF-16 otherwise. `[NinoUtf8]` on a member writes
UTF-8 (its strings, and the strings of a collection or tuple member): for mostly-ASCII text with a few wider chars;
not for mostly-CJK text. Adding or removing it changes the bytes.
- Structs have no header. An unmanaged, sequential, positional struct whose fields are all raw is copied as raw memory
(one block per array or list): its field order, `Pack` and `Size` are the format. Order fields to avoid padding. A
struct that serializes no member is NINO023. `Nullable<T>`, tuples, `KeyValuePair`, `nint`, `DateTimeOffset` are
never raw.
```csharp
[NinoType]
public sealed class Order
{
public long Id;
public string Customer { get; set; }
public List<OrderLine> Lines;
public DateTime? ShippedAt;
[NinoIgnore] public decimal CachedTotal;
}
[NinoType]
public struct OrderLine { public int ProductId; public int Quantity; } // raw: 8 bytes, one block per list
[NinoType(includeNonPublicMembers: true)]
public partial class Account { private long _balance; public int Id; }
```
### Constructors
Order of choice: a constructor or static factory marked `[NinoConstructor]`; else, when every member is assignable, a
constructor callable without arguments (or a `[NinoFactory]`), then members are filled; else the accessible
constructor with the most parameters matching serialized members by name (ignoring case; a positional record's
primary constructor); structs fall back to `new T()`; otherwise NINO004.
- `[NinoConstructor(nameof(Id), nameof(Name))]` lists members when parameter names differ. Each must match a member
(NINO005); the constructor must be `public`/`internal` (NINO021). A read-only member that is not a parameter is read
and dropped (NINO006).
- `required` members: a `[SetsRequiredMembers]` constructor, or every required member serialized and settable.
- Types re-created through a constructor (records, `init`/get-only/`readonly` members) are re-created even by reuse
deserialization, and cannot use `trackReferences` (NINO016) or `[NinoPooled]` (NINO018). Hot-path types: a
parameterless constructor and settable members.
### Formats: choose before data is stored
| | `Positional` (default) | `VersionTolerant` | `Keyed` |
| --- | --- | --- | --- |
| Change members | nothing (renaming a member is free: names are not written) | append to the most derived type only; renaming is free | add, remove, reorder; rename with `[NinoFormerName("Old")]` |
| Cost | none | 4 bytes per object | 4 per object + 8 per member |
| For | same-build messages, rebuildable caches | messages between versions, saves that only grow | config, documents, data edited for years |
- Changing a type's `format` makes its existing data unreadable. Changing a member's type is never tolerated.
- Version-tolerant hierarchy: never append to a base class with subclasses (it shifts their members, silently).
Use `Keyed` for that. Missing members keep their initializer or existing value.
- Two keyed names hashing alike: NINO014. Member `[NinoFormerName]` on a non-keyed type: NINO024.
- Renaming or moving a type changes its type id (written whenever the slot is not sealed): keep it with
`[NinoFormerName("Old.Namespace.Type")]` on the type, runtime form (`+` for nested, no `global::`), one per type
(NINO026), not on generic types written by type id (NINO027). `NinoTypeId.Of(typeof(T))` returns the id.
- `WEAK_VERSION_TOLERANCE` makes the project's positional types version-tolerant; prefer the per-type option.
- Every build records each type's layout as `[assembly: NinoTypeLayout]`; compare `layout.ToString()` between releases
in CI to catch byte changes.
### Polymorphism and references
- A slot declared as a non-sealed class, abstract class or interface writes a 4-byte type id (FNV-1a of the full
name), and reads back the runtime type; a sealed class writes a presence byte. **Seal classes without subtypes.**
- Serialize and deserialize through the common `[NinoType]` base (`Serialize<Message>(msg)`,
`Deserialize<Message>(bytes)`), never `object`: `Serialize<object>` accepts only `[NinoType]` values and dispatches
through the global registry. `SerializeBoxed(value, type)` / `DeserializeBoxed(data, type)` only for a type known
only as a `Type` with no common base, read with the same type.
- Subtypes in other assemblies (plugins, HybridCLR) are found when the assembly is loaded; the generator must run in
that assembly too.
- Without tracking, a shared object is written twice and a cycle throws `InvalidOperationException` while writing.
`[NinoType(trackReferences: true)]` on the shared or cyclic class (classes only, NINO015) keeps identity: 4 bytes and
a lookup per tracked object. Collections are never tracked. Trees need no tracking; depth is unbounded (objects past
128 levels are deferred).
## The API
```csharp
byte[] bytes = NinoSerializer.Serialize(order); // a new byte[] of exactly the data's size
NinoSerializer.Serialize(order, bufferWriter); // any IBufferWriter<byte>; 0 B
Order copy = NinoDeserializer.Deserialize<Order>(bytes); // from any ReadOnlySpan<byte>
NinoDeserializer.Deserialize(bytes, ref copy); // reuse: fills copy in place (null: creates it)
```
- `NinoSerializer` / `NinoDeserializer` are static and thread safe. There are no `Stream`, `ReadOnlySequence<byte>` or
`out` overloads: copy a multi-segment sequence into one block (or pass `First.Span` when `IsSingleSegment`).
- Hot paths: one `NinoArrayBufferWriter` per thread or connection (not thread safe), `ResetWrittenCount()` before each
message, send `WrittenSpan`, `Dispose()` at the end (returns its array to the pool). `new
NinoArrayBufferWriter(capacity, pool)` rents from your own `ArrayPool<byte>`. Nino writes into it in place; any other
`IBufferWriter<byte>` (a `PipeWriter`) costs one copy. Call `Serialize(value)` only when an API needs a `byte[]`.
- Build settings: Release, tiered compilation and Dynamic PGO left on (never `TieredCompilation` false), Native AOT
with `<IlcInstructionSet>` for AVX2/AVX-512, Unity IL2CPP Release/Master with Burst; Rust, Go and TypeScript in
https://nino.xgamedev.net/en/docs/v4/advanced/build-configuration.
- Several values in one buffer: `var w = new Writer(buffer);` then `NinoSerializer.Serialize(a, ref w)`..., then
`w.Flush(); w.ReleaseReferences();`; read with `var r = new Reader(span);`, `Deserialize<T>(ref r)`, then
`r.ReleaseReferences()`. Release once per logical message.
- `NinoSerializer.GetFixedSize<T>()` gives the size of a fixed-size type; `NinoSerializer.IsSupported(type)` whether
Nino can write a type.
- Reuse deserialization fills the instance, its nested objects, lists (by index) and dictionaries (merged by key);
unchanged strings are kept. Dropped instances go to `NinoSettings.SetRecycler<T>(handler)` for the slot's declared
type, else `ReturnToPool()` of an `INinoPooled`, else the GC. Use `List<T>` for varying counts: an array is reused
only at the same length.
- `[NinoType, NinoPooled] public sealed partial class X` gets a built-in pool: Nino takes new instances from it and
returns dropped ones; you call `x.ReturnToPool()` once (twice throws). Implement both hooks (NINO019):
```csharp
[NinoType, NinoPooled]
public sealed partial class Alert
{
public int DeviceId;
public float Value;
private static partial IEnumerable<Alert> OnNinoPoolWarmup() => Enumerable.Range(0, 64).Select(_ => new Alert());
private partial void OnNinoPoolReturn() { } // release state Nino does not deserialize
}
```
- Pooled classes are sealed, partial and assignable (NINO018). A class holding pooled children should be pooled too.
`NinoPool<T>.Shared.Clear()` empties a pool (Unity with domain reload off).
- `[NinoFactory] public static T Rent()` (public, static, callable without arguments, returns the type, never null:
NINO017) is called whenever Nino needs a new instance; also for your own collection types (implement `INinoPooled`
so dropped ones come back). Not together with `[NinoConstructor]` (NINO025).
### Settings and untrusted input
Nino is built for untrusted input: lengths are checked against the bytes left before allocating, the stack is bounded,
data names only type ids of declared subtypes (never type names), and **every invalid input throws
`NinoDataException`** (derives from `InvalidOperationException`, message starts with `Nino:`). Catch it at the
boundary. Caller mistakes and write failures (untracked cycle, too deep) stay `InvalidOperationException`.
```csharp
// At startup, before the first Nino call on any thread: these freeze on first use; setting one later throws.
NinoSettings.Untrusted = true; // seeded hashing of int/char/Guid/string keys, raw value checks, no trailing bytes
NinoSettings.MaxCollectionLength = 100_000; // default int.MaxValue
NinoSettings.MaxDepth = 512; // default 512, at least 128
NinoSettings.MaxHashCollisions = 100; // default 100, at least 1
```
- Pool settings (`PoolMaxRetained`, `SetPoolMaxRetained<T>`, `SetRecycler<T>`) are also startup-only.
- Still your job: validate values (a negative health, an unnamed enum value), cap frame or request sizes before
buffering, and version payloads yourself (the bytes carry no schema: data for another type with a compatible layout
reads without error; put a magic number and version in front of stored files).
## Types Nino writes
- Primitives, enums, `decimal`, `DateTime`, `TimeSpan`, `Guid`, unmanaged structs (Unity and `System.Numerics`
vectors), `string`, `T?`, `ValueTuple`/`Tuple` (1-7), `KeyValuePair`, `DateTimeOffset`, `nint`, `BigInteger`,
`Version`, `Uri`, `StringBuilder`, `BitArray`, `CultureInfo`, `TimeZoneInfo`, `Lazy<T>`.
- `T[]`, `T[,]` to `T[,,,]`, `ArraySegment<T>`, `Memory<T>`, `ReadOnlyMemory<T>`, `ReadOnlySequence<T>`, `List<T>`,
`HashSet<T>`, `Queue<T>`, `Stack<T>`, `LinkedList<T>`, `SortedSet<T>`, `Dictionary`, `SortedDictionary`,
`SortedList`, the concurrent collections, `BlockingCollection<T>`, `PriorityQueue` (.NET 8+ builds only, not Unity),
`ILookup`, `IGrouping`, read-only wrappers, immutable collections (with `Nino.Immutable`), and any class with a
public parameterless constructor (or `[NinoFactory]`) implementing `ICollection<T>`, `IDictionary<K,V>` or
`IProducerConsumerCollection<T>`. Interfaces read back as `List<T>`, `HashSet<T>`, `Dictionary<K,V>`.
- `object` members hold any `[NinoType]` (by type id), nothing else.
- Not supported (NINO007, member skipped): delegates, `Task`, pointers, `Span<T>`, `System.Type` (on purpose: store
your own id), non-generic collections, abstract or struct collections, Unity/Godot classes except Unity's
`AnimationCurve`, `Gradient`, `RectOffset`. Fix with `[NinoIgnore]` or a custom formatter.
- Never serializable (NINO029, an error; `NotSupportedException` at run time): `System.Numerics.Vector<T>`, alone or
inside anything (arrays, collections and classes deriving from one, tuples, `T?`, plain structs): it is 16, 32 or 64
bytes by the CPU. `Vector64<T>` to `Vector512<T>` are fixed size and fine. Store 16 lanes of a 4-byte type (whole
vectors on every CPU) and view them as vectors ([why](https://nino.xgamedev.net/en/docs/v4/struct-layouts#vector-t)):
```csharp
[NinoType]
public sealed partial class Particle
{
public float[] Lanes = new float[16]; // serialized: 64 bytes on every host
[NinoIgnore] // 4, 2 or 1 vectors by the CPU
public Span<Vector<float>> Vectors => MemoryMarshal.Cast<float, Vector<float>>(Lanes.AsSpan());
}
```
### Custom formatters
A formatter is a **struct** implementing `INinoFormatter<T>` (`Serialize(T, ref Writer)`, `Deserialize(ref T, ref
Reader)`), called as `default(F)`: no instance state. Read exactly what you wrote, in order.
- One member: `[NinoMemberFormatter(typeof(F))]`, `F` for exactly the member type (NINO008).
- A type Nino does not know: `[assembly: NinoFormatter(typeof(F<,>))]`, generic formatters as open definitions; a
type parameter constrained `struct, INinoFormatter<E>` gets the element formatter; `[NinoRaw]` and `[NinoCreated]`
narrow what binds. Invalid registrations: NINO020. There is no runtime registration call.
- Use `writer.WriteLength(n)` / `WriteNullLength()` and `reader.ReadLength(minElementSize)` (checked against the bytes
left), and hand dropped instances to `NinoRecycler.Discard(ref value)` / `NinoRecycler.Replace(ref value, x)`.
Nested values: `NinoSerializer.Serialize(v, ref writer)`, `NinoDeserializer.Deserialize(ref v, ref reader)`.
```csharp
public struct ZigZagLongFormatter : INinoFormatter<long>
{
public void Serialize(long value, ref Writer writer)
{
ulong v = (ulong)((value << 1) ^ (value >> 63));
while (v >= 0x80) { writer.Write((byte)(v | 0x80)); v >>= 7; }
writer.Write((byte)v);
}
public void Deserialize(ref long value, ref Reader reader)
{
ulong v = 0; int shift = 0; byte b;
do { reader.Read(out b); v |= (ulong)(b & 0x7F) << shift; shift += 7; } while ((b & 0x80) != 0 && shift < 70);
value = (long)(v >> 1) ^ -(long)(v & 1);
}
}
[NinoType]
public sealed class BalanceChange
{
public int AccountId;
[NinoMemberFormatter(typeof(ZigZagLongFormatter))] public long DeltaCents;
}
```
## Unity, IL2CPP, HybridCLR, AOT
- Unity compiles against .NET Standard: no `[UnsafeAccessor]`, so private members and `[NinoPooled]` need `partial`
(NINO002); no interceptors (same bytes). Assembly definitions need `Nino.Core.dll` (Auto Referenced, or listed under
Override References; also `Nino.UnityTypes.dll` for the Unity classes).
- Install `com.unity.burst` 1.8+ for speed: Burst SIMD kernels then register on their own (`Nino.Burst.NinoBurst`) for
tuples, padded structs, Latin-1 strings and the ASCII text of `[NinoUtf8]` strings; include SSE4 and AVX2 in Burst
AOT Settings for x64 players. Without Burst, IL2CPP and Mono players take Nino's scalar paths (the same bytes);
WebGL players built with WebAssembly SIMD get the same kernels from `Nino.WebGL` with no setup.
- IL2CPP and Native AOT: no reflection at serialization time; types must appear in code the generator sees (a member
type, the type argument of a `NinoSerializer`/`NinoDeserializer` call, or the `typeof` argument of `SerializeBoxed`,
`DeserializeBoxed` or `IsSupported`; a `Type` known only at run time registers nothing). Native AOT on x64 takes
Nino's SSSE3 paths on CPUs that have SSSE3; its AVX2 and AVX-512 paths need `<IlcInstructionSet>` (`x86-64-v3`,
`x86-64-v4` or `native`), which makes the app require those CPUs. HybridCLR: `Assembly.Load` is enough, no init call;
`NinoRegistry.EnsureInitialized(asm)` / `EnsureAllInitialized()` only to pay the registry load on a loading screen.
- Godot 4 .NET: a normal NuGet project; engine objects (`Node`, `Resource`) are not serialized, math structs are raw.
## ASP.NET Core (Nino.AspNetCore)
```csharp
builder.Services.AddControllers().AddNinoFormatters(); // MVC: application/x-nino in and out, JSON stays the default
app.MapPost("/orders", (NinoBody<Order> order) => NinoResults.Ok(order.Value));
app.MapGet("/shape", () => NinoResults.Ok<Shape>(new Circle())); // give T to write a subtype as its base
```
Invalid bodies become 400 (`NinoBody<T>`: 415 for another content type). By hand:
`request.ReadFromNinoAsync<T>(ct)`, `response.WriteAsNinoAsync(value, ct)`, `NinoMediaTypes.Application`.
## Other languages
Nino.Codegen writes your `[NinoType]`s as TypeScript, Go or Rust types with codecs, from the built assembly's layouts:
`<NinoCodegen Include="typescript|go|rust" Out="..." />` items in the project (or `nino-gen --assembly x.dll --lang
ts|go|rust --out dir`). Runtimes: `nino.ts` (written next to the code, or npm `nino-serialization`), Go module
`github.com/JasonXuDeveloper/Nino/runtimes/go/v4`, Rust crate `nino-serialization` (library `nino`). Unsupported
types are left out with NINOGEN002. Details: [references/other-languages.md](references/other-languages.md).
## Diagnostics
`NINO0xx` come from the analyzer as you type; many have a code fix. Errors mean the type cannot work, warnings mean
data is silently dropped or an attribute does nothing: never suppress NINO006 or NINO007 blindly.
- **Type shape** (NINO001, 002, 003, 022, 023): make it public/internal, `partial`, add `[NinoType]`, rename a hidden
member, serialize at least one struct member.
- **Construction** (NINO004, 005, 006, 021, 025): give a usable constructor, match `[NinoConstructor]` names, make
read-only members parameters or settable, make it public, pick one of `[NinoConstructor]` / `[NinoFactory]`.
- **Members** (NINO007 to 013, 028, 029): an unsupported member type, a formatter for the wrong type, inconsistent
`[NinoMember]`/`[NinoIgnore]`, duplicate indices, `[NinoMember]` on what is never written, `Vector<T>` (below).
- **Features** (NINO014 to 017, 024, 026, 027): keyed name collisions, `trackReferences` on structs or re-created types,
invalid factories, misplaced `[NinoFormerName]`.
- **Pooling and formatters** (NINO018, 019, 020): pooled types must be sealed, partial, assignable and implement both
hooks; `[assembly: NinoFormatter]` must name a usable struct.
- **nino-gen** (NINOGEN001 error: bad option or unreadable assembly; 002: type left out; 003: old `NinoTypeScript*`
properties; 004: Nino.Codegen without a `<NinoCodegen>` item).
## Pitfalls
- A bare `[NinoType]` is positional: adding, removing or reordering members breaks stored data (throws, or worse,
wrong values). Pick `VersionTolerant` or `Keyed` before shipping anything stored or exchanged across versions.
- Raw structs: reordering fields or changing `StructLayout` changes the bytes. Their layout is 64-bit CoreCLR's on
every runtime (Mono, IL2CPP, .NET Framework and 32-bit hosts convert), so data moves between them.
- `Vector<T>` members: NINO029 (Types Nino writes). Never write `new Vector<float>(lanes)` assuming 8 lanes:
`Vector<float>.Count` is 4, 8 or 16 by the CPU.
- `Serialize<object>`, `object` members and the boxed API in message code: use a common `[NinoType]` base.
- Write and read with the same declared type: `Serialize<Base>` then `Deserialize<Base>`.
- `int?[]` / `List<int?>` and `IList<int?>` write different bytes; so do `[NinoUtf8]` and the default.
- `NinoSettings` changed after the first Nino call throws; set it at startup.
- Sharing one `NinoArrayBufferWriter` between threads, or using `WrittenSpan`/`WrittenMemory` after the next write or
`ResetWrittenCount()`.
- A recycler or `ReturnToPool()` on instances still referenced elsewhere: the pool hands them out again.
- If the project sets `InterceptorsNamespaces` itself, append (`$(InterceptorsNamespaces);Mine`), don't replace.
- Arrays or `List<T>` of classes much larger than the CPU cache, whose objects were replaced, shuffled or sorted:
`<NinoPrefetchLargeCollections>true</NinoPrefetchLargeCollections>` in the calling project makes their top-level
calls prefetch (same bytes; .NET 8+ through the interceptors, nothing in Unity). Short collections get a few % slower.
- v3 code: `Serializer.Serialize` / `Deserializer.Deserialize(data, out v)` are now `NinoSerializer.Serialize` /
`NinoDeserializer.Deserialize<T>(data)`; `[NinoExplicitOrder]` is gone (`[NinoType(false)]` + `[NinoMember]`). v4
cannot read v3 data. Beta names: `[NinoRefDeserialization]` is `[NinoFactory]`, `DeserializeRef` is
`Deserialize(data, ref value)`.
## What Nino does not do
- No `Stream`, async or multi-segment APIs (wrap `IBufferWriter<byte>` / `ReadOnlySpan<byte>`), no compression, no
callbacks, no `System.Type`, no schema in the bytes, no human-readable output, no reading of v3 data.
- No runtime registration of formatters and no reflection fallback: the generator must see every type.
- No languages besides .NET, TypeScript/JavaScript, Go and Rust (use Protocol Buffers or MessagePack there).
## Example
```csharp
using System;
using System.Collections.Generic;
using Nino.Core;
[NinoType(format: NinoFormat.Keyed)] // stored: members may change later
public abstract class Message { public long Seq; }
public sealed class Ping : Message { public long SentAt; } // inherits [NinoType]
public sealed class Chat : Message { [NinoUtf8] public string Text; public List<int> Mentions; }
public static class Program
{
public static void Main()
{
NinoSettings.Untrusted = true; // startup, before any Nino call
using var buffer = new NinoArrayBufferWriter(); // one per thread or connection
NinoSerializer.Serialize<Message>(new Chat { Seq = 1, Text = "hi “there”", Mentions = new() { 7 } }, buffer);
Message received = null;
try
{
NinoDeserializer.Deserialize(buffer.WrittenSpan, ref received); // a Chat; reused by later calls
}
catch (NinoDataException e) // the only exception invalid data throws
{
Console.WriteLine($"bad message: {e.Message}");
return;
}
var chat = (Chat)received;
Console.WriteLine($"{chat.Seq} {chat.Text} {chat.Mentions[0]}");
buffer.ResetWrittenCount(); // reuse the same array
NinoSerializer.Serialize<Message>(new Ping { Seq = 2, SentAt = 42 }, buffer);
NinoDeserializer.Deserialize(buffer.WrittenSpan, ref received); // type changed: a new Ping
Console.WriteLine(received is Ping { SentAt: 42 });
}
}
```