Skip to content

Comparison & synthesis

The capstone of the serde-expressiveness survey: the surveyed systems side by side, the per-dimension consensus, the architectural trade-offs — and the delta table mapping each modern capability to where sparkles:wired stands today.

Last reviewed: August 9, 2026


At a glance

Tier is the artifact holding structural knowledge; Render argv asks whether anything in the ecosystem produces a command line from a value (the survey's motivating question — see argv as a codec).

SystemTierBidirectionalReified schemaValidationError accumulationUnknown-field preserveMigrationsRender argv
serde (Rust)1✅ per-type derives❌ (hence facet)❌ split off❌ (bolt-on eserde)flatten map hack
facet (Rust)3✅ runtime Shape◐ nascent◐ parse-only args
Effect Schema (TS)2/3✅ one parser, flipped AST✅ runtime AST✅ checkserrors: "all"preserve option
Pydantic v2 (Python)3✅ core schema (Rust)✅ built-in✅ error listmodel_extra
msgspec & cattrs1/3✅ Meta❌ fail-fast
zio-schema (Scala)3✅ lens/prism pairs✅ pattern-matchable ADTDynamicValuealgebra
autodocodec / tomland / unjson2✅ profunctor codecs✅ inspectable GADT✅ (tomland, unjson)
circe & aeson1✅ paired instances (drift)◐ circe (fragile)
OCaml ppx & ATD1/3◐ external .atd_v.mlopen_enum onlyatddiff lint
clap / optparse-applicative❌ parse-only✅ inspectable parser✅ runtime◐ trailing args
wired today1+◐ flat field table❌ (goal)

Per-dimension consensus

For each analysis-spine dimension: what the strongest surveyed design does, and who sets the bar.

Schema model & bidirectionality

The consensus among the strongest systems is schema-as-data interpreted in both directions: Effect runs one parser over a flipped AST; zio-schema holds a lens per field and a prism per case; pydantic-core compiles a schema value. Tier-1 systems generate both directions but can answer no questions about the type afterwards — the ceiling circe & aeson document as "no docs, no field enumeration, per-format re-derivation". facet is the proof the ceiling hurts enough that Rust rebuilt reflection at runtime. Bar-setter: zio-schema (serializable: Schema[Schema[A]]). D's position: the reified value can be a compile-time constant — tier-3 capability at tier-1 cost, unoccupied anywhere else.

Naming, optionality & defaults

Consensus vocabulary: per-symbol rename + per-type case convention + read-many/write-one aliases + direction-split renames (serde has all four). On presence, the strongest designs separate wire-presence, default channels, and provenance: Effect's decoding-default vs constructor-default split, Pydantic's model_fields_set + exclude_unset, ATD's ?/~ trichotomy, autodocodec's four optionality constructors. Every library with fewer distinctions has a documented bug class. Bar-setter: Pydantic (provenance) + Effect (the full matrix).

Sum types & discrimination

Consensus: all four representations declaratively selectable (serde, aeson), with three escalating discriminator designs — declared tags, Effect's inferred sentinels (any non-optional literal field discriminates, no annotation needed), and Pydantic's callable discriminators (the tag is what a function extracts — which is exactly a subcommand name in argv[0]). autodocodec adds the joint/disjoint declaration that changes the emitted schema. Untagged error quality is bad everywhere (serde's "did not match any variant", aeson keeping only the last error). Nobody proves disjointness at compile time — D can.

Transformations & validation

Consensus: fallible, direction-paired transformations as reusable values (Effect's Transformation, tomland's BiMap-with-reasons), plus a type-shaped adapter layer for representation choice (serde_with's SerializeAs — the composition lesson), plus validation inside the codec with constraint metadata feeding schema and docs (Pydantic, Effect checks with meta). serde's validation-as-a-separate-crate split is the corpus's named mistake: two vocabularies, forgettable second call, errors that don't unify, constraints invisible to the schema. Pydantic's wrap validators show the hook signature to standardize on: fn(value, next).

Errors & context

Consensus: structured errors with an automatically attached path and an accumulation modeunjson sets the bar (all problems, path-anchored, plus a usable partial value). The two structural hazards are manual path attachment (forgettable — aeson) and monadic composition silently killing accumulation (circe #837/#2062); a generated walk is immune to both. On context: Effect splits decode-services from encode-services at the type level; Pydantic threads an untyped context object; serde's DeserializeSeed shows the cost of bolting it on late. D can infer two typed Ctx parameters, void and free when unused.

Metadata, derivations & extensibility

Consensus among tier-2/3: one declaration, N artifacts — JSON Schema, docs, generators, equality, diffing (Effect's annotation→ derivation table; zio-schema's codec-per-format; autodocodec's docs-as-a-constructor). The load-bearing rule, learned from schemars' leaks: attributes must be data, not function pointers, or every generator goes blind exactly where the schema gets interesting. On unknown fields, only zio-schema (via DynamicValue) and Pydantic (model_extra) round-trip unmodelled data — no tier-1/2 library does. Constraint-guided test-data generation (Effect's Arbitrary) is the highest-value derivation to copy.

Architectural trade-offs

Trade-offPosition APosition BThe D resolution
Where structure livescompiled away (tier 1: fast, unanswerable)runtime value (tier 2/3: derivable, slower/hand-written)CTFE constant: derivable and compiled away
Data modelfixed universal model (serde's 29 types — round-trip trap #1556)per-format vocabulary (no shared policy)open core + format-declared extensions via DbI
Async/effects in the coreevery leaf effect-valued (Effect — uniform, costly)sync-only (serde — fast, no context)sync @nogc core; typed Ctx; async as per-converter escape hatch
Representation choice attachmenton the type (newtypes — serde try_from)at the use site (adapters, converter objects — serde_with, cattrs)both: UDAs on fields + a Policy parameter for types you don't own
Degradation on unsupported shapessilent (toCodecJsonnull; projections drop checks)hard errorstatic assert with a sentence of explanation
Unparsing/render directiondiscard the mapping (clap), nominal hacks (reverse_argparse)named struct fields are nominal by construction — render is a third fold

The delta table

Each capability row: the bar-setter, wired today (from the baseline), and the D-feasibility tag. Rows marked ⚠ not retrofittable constrain API decisions that must precede the CLI format.

#CapabilityBar-setterwired todayFeas.Notes
1Reified schema tree (arena, annotations on every node, ref-nodes)zio-schema, facet, pydantic-core◐ flat one-level FieldPolicy[](b)everything below walks it
2Schema-driven generic walk (new format = new walker)zio-schema codecs✘ hand-written JSON ladders(b)root cause of the probe-verified divergences
3Four tagged-union representations + @WireOtherserde, aeson✘ untagged only(a)blocks subcommands
4Inferred sentinels + callable discriminatorsEffect, Pydantic(b)subcommands stop being a parser special case
5Compile-time untagged-ambiguity proofnobody(b)novel capability
6Metadata axis + derivations (help, JSON Schema, generators)Effect annotations, autodocodec(a)attributes-as-data rule; __traits(docComment)
7Validation in-codec, cross-field, constraint metadataPydantic, Effect checks(a)avoid serde's split
8Error accumulation + partial value, paths unconditionalunjson✘ fail-fast, one slot(a/b)sink-based for @nogc
9Presence tri-state + wasSet provenance (Decoded!T)Pydantic✘ collapsed by design(b)⚠ not retrofittable; required for layering & argv render
10Decoding-default vs constructor-default channelsEffect✘ one channel(a) — else encode(decode(argv)) invents flags
11Aliases (read-many/write-one) + direction-split renamesserde✘ one name per (symbol, format)(a)also the short-flag mechanism
12Unknown-field knob + @WireExtra preservationPydantic, zio-schema◐ ignore only(a)beats every tier-1/2 library for one branch of work
13Presence-carrying converters, chains, @WireAs adaptersEffect Getter, serde_with◐ bare Wire→Domain(a/b) converter ABI
14Typed decode/encode contexts + sibling-field accessEffect RD/RE; nobody has siblings(b) hook ABI; "D can, serde can't" headline
15Wrap-hook middleware fn(value, next)Pydantic(b) hook ABI
16update/partial-merge (config layering)unjson(a)defaults ⊕ file ⊕ env ⊕ argv as one mechanism
17Typed DynValue + string-tree IRzio-schema; Effect toCodecStringTree✘ JSON arena only(b)the argv intermediate representation
18WireRoundTrippable!T compile-time predicatenobody(b)encode the corpus's round-trip-breaker checklist
19Schema digest golden → (later) migration algebraATD atddiff → zio-schema(b/c)digest first; CTFE-derived migrations when both schemas known
20Projections Pick/Omit/Partial/WithFieldsEffect/TS mapped types(b)the 90% four; skip the full algebra

Also owed regardless of the redesign: the three probe-verified divergences (static arrays, slot-targeted policies, per-variant union errors).

What deliberately not to copy

Convergent across the deep-dives:

  • serde's fixed 29-type data model and the Visitor pattern — keep the model open; argv needs vocabulary (count, arity, positional) no fixed model hosts.
  • An effect-shaped core — Effect pays allocation on every field for async almost no schema uses.
  • A runtime AST — D's compile-time type graph is the AST.
  • Silent degradation anywhere (unrepresentable → null, projections dropping checks, flatten forcing deserialize_any) — static assert instead.
  • aeson's closed global Options record; the full projection algebra; smart-union exactness scoring; stringly-typed annotation keys; BiArrow-style capability interfaces that declare methods they error on.

Consequence for the CLI-on-wired plan

The delta table's ⚠ rows (9, 10, 13, 14, 15) are API-shaping and precede any Cli format work; rows 1–8 are what make the format a walker rather than a second bespoke engine. The resulting sequencing — wired core upgrade first (with the JSON conformance suite and bench baseline as the regression gate), then the Cli format as the second walker (with the five example CLIs and their --help goldens as the oracle) — replaces the original "desugar the CLI UDAs onto today's six @Wire* attributes" plan, which would have recreated the bespoke engine under a different name. The competitive headlines all sit in the novel-capability rows: 5, 14 (sibling access), 18, and the render half of argv itself.