typetag
A single attribute macro that gives Rust trait objects Serde serialization and deserialization support.
Repository Health
Technical Analysis
typetag solves a problem long considered impractical in idiomatic Rust: giving &dyn Trait and Box<dyn Trait> trait objects first-class Serde support without hand-writing a matching enum that lists every implementor. Adding #[typetag::serde] to a trait definition and to each impl block is enough to make the trait object serializable and deserializable, including across separate crates in the same dependency graph.
Under the hood the crate builds a runtime registry of every trait impl using the inventory crate (backed by ctor static initializers), so implementors register themselves at program startup regardless of which crate defines them. Serialization and deserialization are then routed through erased-serde to keep the trait object-safe. All three of Serde’s tagged enum representations — internally, externally, and adjacently tagged — are supported as a per-trait choice, along with default-variant fallback, custom tag names, and compatibility with binary formats like Bincode and Postcard.
What You Get
- A
#[typetag::serde]attribute macro applied to a trait definition and itsimplblocks, requiring no other boilerplate - Support for all three Serde tagged-enum representations (internally, externally, adjacently tagged), selectable per trait
- Cross-crate trait impl registration, so implementations defined anywhere in the final binary’s dependency graph are discovered automatically
- Compatibility with non-self-describing binary formats such as Bincode and Postcard, not just JSON
default_variantanddeny_unknown_fieldsoptions for controlling deserialization fallback and strictness- Separate
typetag::serializeandtypetag::deserializemacros for trait objects that only need one direction
Common Use Cases
- Serializing a plugin system’s heterogeneous event or command types to JSON without a central enum
- Persisting polymorphic domain objects (e.g. a
ShapeorWidgettrait) to disk or a database viaBox<dyn Trait> - Sending arbitrary trait-object payloads over a wire protocol using compact binary formats like Postcard or Bincode
- Building extensible config or rule systems where third-party crates can register their own deserializable trait impls
Under The Hood
Architecture
The crate splits cleanly into two halves: a proc-macro impl crate (typetag-impl, built on syn, quote, and proc-macro2) that expands the #[typetag::serde] attribute at compile time, and the main crate that owns the runtime logic in externally.rs, internally.rs, and adjacently.rs — one module per Serde tag convention — plus content.rs, which implements a buffered Content type so internally/adjacently tagged formats can be deserialized in two passes (peek the tag, then dispatch). ser.rs and de.rs wire the actual erased_serde::Serialize/Deserialize implementations for &dyn Trait/Box<dyn Trait>, and private.rs (assembled per-patch-version by build.rs) exposes a versioned hidden module so multiple point releases of typetag can coexist in one dependency graph without symbol collisions. The registry (Registry/Strictest in private.rs) is the seam between macro-expansion time and runtime dispatch — every generated impl across the ecosystem depends on its exact shape.
Tech Stack
A #![no_std] core crate (via extern crate alloc) built on serde and erased-serde for object-safe (de)serialization, inventory (itself backed by ctor) for link-time trait-impl registration, and once_cell for lazily building the tag-to-constructor map on first deserialization. The companion typetag-impl proc-macro crate depends on syn 3, quote, and proc-macro2. Dev-dependencies cover serde_json and postcard for round-trip testing, async-trait for async-trait interop tests, and trybuild plus rustversion for compile-fail and version-gated test coverage. CI runs a shared dtolnay/.github workflow across nightly/beta/stable/MSRV (1.71) on Linux, macOS, and Windows with warnings treated as errors.
Code Quality
The integration test suite in tests/test.rs is extensive, covering all three tag representations, default variants, deny_unknown_fields, marker-trait bounds (Send/Sync), generic and associated-type traits, macro-expanded impls, trait hierarchies, tag-mismatch error messages, and async-trait ordering — each exercised through both serde_json and postcard round-trips. tests/ui/ combined with trybuild covers compile-fail diagnostics for macro misuse. Module naming mirrors Serde’s own terminology precisely, clippy lints are explicitly allow-listed rather than silenced wholesale, and CI enforces cargo check/cargo test across five Rust channels and three operating systems with warnings as errors.
What Makes It Unique
typetag addresses a case widely considered awkward or unsolvable in idiomatic Rust — giving boxed trait objects first-class Serialize/Deserialize impls without a hand-maintained enum. It does this by combining inventory’s link-time distributed registration (so impls anywhere in the binary’s dependency graph self-register via static initializers) with erased-serde’s object-safe serialization trait, resolving a tag to a constructor through a registry built lazily on first use. Offering all three Serde tag conventions as a per-trait choice, plus default-variant fallback and true cross-crate registration, goes well beyond what a hand-written boxed-trait Serialize/Deserialize implementation could reasonably offer.
Used by 2 apps in this directory
ParadeDB
Search · Databases · Analytics
Born out of Y Combinator's S2023 batch, ParadeDB is a Postgres extension that delivers Elasticsearch-quality BM25 search and real-time analytics without a separate search cluster to manage.
PostgresML
Databases · AI Development
Run ML training and LLM inference natively inside PostgreSQL with GPU acceleration — no data movement required.