dtolnay/typetag: Rust Trait Object Serialization
Serde serializable and deserializable trait objects
At a glance
- What is it?
- Typetag adds Serde serialization and deserialization to Rust trait objects by registering concrete trait implementations through procedural macros. It fits applications that use a fixed, compiled set of polymorphic values with JSON, Bincode, or other Serde formats.
- Who is it for?
- Adopt Typetag when the program needs a finite, discoverable set of Serde implementations behind a trait object and accepts registry-based discovery. Do not choose it for runtime-defined open-world plugins or targets where the required setup is unsuitable.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 97 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- dtolnay/typetag1,553 ★ · +2
| Repository | 2026-10-19 | 2026-11-07 |
|---|---|---|
| dtolnay/typetag | 1.6k | 1.6k |
2 Stars in the last 19 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Why Typetag Solves Rust Trait Object Serialization
Typetag addresses a specific limitation in Rust's object-oriented data model. Serde can serialize a concrete `PageLoad` or `Click`, but a function receiving `&dyn WebEvent` does not directly tell Serde which concrete implementation it holds. Deserialization has the related problem: the input may identify any implementation associated with the trait, while Rust needs to construct a `Box<dyn WebEvent>`.
Typetag treats each trait implementation as a variant of an enum. The trait defines the common interface, and its concrete implementations supply serializable payloads. The program can then pass events through a function boundary without converting them into an application-owned enum first.
The intended users are Rust developers building event pipelines, internal dispatch systems, or serialized configurations that need a trait object rather than one fixed struct. The supported implementations must exist in the compiled program. Typetag provides compile-time registration for a known set of implementations, not runtime discovery of arbitrary code.
How Typetag Builds Its Implementation Registry
Typetag is a macro layer over the `inventory`, `ctor`, and `erased-serde` crates. The `#[typetag::serde]` attribute on a trait and its implementation blocks registers each implementation in a shared registry. The README explains that `inventory` builds this registry through initialization functions supplied by `ctor`.
During the first `Box<dyn Trait>` deserialization, Typetag iterates over the registry and constructs a map from wire tags to deserialization functions. Subsequent deserializations use that map to select the matching function. This mechanism accounts for the library's cross-crate support: implementations anywhere in the final program's dependency graph can participate.
`erased-serde` provides the type-erased bridge needed to connect Serde with trait objects without breaking object safety. The important boundary is static registration. An implementation that is not compiled into the final binary cannot be selected, even if its tag appears in stored or transmitted data.
Selecting Externally, Internally, or Adjacently Tagged Data
The trait attribute determines the Serde enum representation. With the default external tagging, each implementation acts as an outer object key and its serialized value becomes the associated content.
#[typetag::serde]
trait WebEvent {
fn inspect(&self);
}For the README's two implementations, `PageLoad` is represented as `{"PageLoad":null}`, while `Click` is represented as `{"Click":{"x":10,"y":10}}`. This matches Serde's default enum representation and is a direct fit for consumers expecting that structure.
Internal tagging places a named tag beside the fields of the serialized value.
#[typetag::serde(tag = "type")]
trait WebEvent {
fn inspect(&self);
}The documented output is `{"type":"PageLoad"}` and `{"type":"Click","x":10,"y":10}`. Adjacent tagging separates the discriminator from the payload.
#[typetag::serde(tag = "type", content = "value")]
trait WebEvent {
fn inspect(&self);
}That produces `{"type":"PageLoad","value":null}` and `{"type":"Click","value":{"x":10,"y":10}}`. The receiver must use the same representation as the sender. Changing this decision can make existing stored data incompatible.
Adding Typetag and Defining a First WebEvent
The README adds Typetag through a Cargo dependency. This declares the crate in the application's package manifest using the documented version series.
[dependencies]
typetag = "0.2"The first application types are concrete, Serde-capable implementations of an object-safe trait. The README defines `PageLoad` as a unit struct and `Click` as a struct containing `x` and `y` coordinates.
trait WebEvent {
fn inspect(&self);
}
#[derive(Serialize, Deserialize)]
struct PageLoad;
#[derive(Serialize, Deserialize)]
struct Click {
x: i32,
y: i32,
}Each implementation still supplies its behavior through a normal Rust `impl` block, but Typetag must also see a marker on those blocks. Under internal tagging, the trait and both implementations use `#[typetag::serde]`.
#[typetag::serde(tag = "type")]
impl WebEvent for PageLoad {
fn inspect(&self) {
println!("200 milliseconds or bust");
}
}
#[typetag::serde]
impl WebEvent for Click {
fn inspect(&self) {
println!("negative space between the ads: x={} y={}", self.x, self.y);
}
}The serialized output for `Click { x: 10, y: 10 }` is `{"type":"Click","x":10,"y":10}`. The application needs `serde_json` when using JSON, although the repository lists it as a development dependency rather than a dependency of the library itself.
Deserializing Box<dyn WebEvent> from JSON
After registration, Serde can deserialize JSON into the boxed trait type. The README gives this receiver function:
fn process_event_from_clickfarm(json: &str) -> Result<()> {
let event: Box<dyn WebEvent> = serde_json::from_str(json)?;
overanalyze(event)?;
Ok(())
}`serde_json::from_str` receives the tagged JSON, Typetag selects the registered implementation, and the resulting `Box<dyn WebEvent>` is passed to application logic. For the `Click` example, deserialization reconstructs the concrete `Click` before the boxed value reaches the processing function.
This direction is what enables polymorphism across a process boundary or persistent-storage boundary. A concrete enum could provide similar behavior when its variants are centrally controlled. Typetag instead derives the variants from marked trait implementations, which is particularly useful when those implementations live in different crates.
The README describes application-specific delivery and processing functions without defining their implementations. Their behavior therefore comes from the surrounding application, while `serde_json::to_string`, `serde_json::from_str`, and the `Box<dyn WebEvent>` type provide the project-specific serialization path.
Stable Tags, Cross-Crate Support, and WebAssembly Boundaries
Typetag uses the concrete Rust type name as the default tag. That name is convenient inside one program, but it becomes part of the serialized contract when data leaves memory. Renaming or moving a type can therefore affect persisted or transmitted values. An implementation can set an explicit tag instead.
#[typetag::serde(name = "mouse_button_down")]
impl WebEvent for Click {
fn inspect(&self) {
println!("negative space between the ads: ({}, {})", self.x, self.y);
}
}With the internally tagged trait, the documented output is `{"type":"mouse_button_down","x":10,"y":10}`. An explicit tag gives the wire contract a deliberate name, but renaming that tag later still requires compatibility handling outside Typetag.
The README states that implementations can be spread across crates and that all three representations round-trip through Bincode. It also lists WebAssembly as supported, while directing readers to the `inventory` documentation for additional setup. That dependency detail matters because Typetag relies on constructor-based registration rather than ordinary function calls alone.
Typetag Trade-offs, Licensing, and Maintenance State
The main limitation is the boundary between compiled implementations and runtime extensions. Typetag can reconstruct implementations registered in the final dependency graph. It does not document a mechanism for loading an unknown implementation from a plugin directory, choosing a newly compiled binary at runtime, or updating the tag registry after startup. If those capabilities are required, a central enum or a separate extension protocol would be a better fit.
A hand-written Serde enum is the clearest alternative. Its variants are visible in one definition, its wire names are explicit, and adding a type does not depend on inventory initialization. Typetag offers the opposite tradeoff: concrete implementations can live in separate crates, and the macro registration reduces repetitive enum plumbing. The cost is less obvious control over registry discovery and additional machinery from `inventory`, `ctor`, `erased-serde`, and `once_cell`.
Version 0.2.23 declares Rust 1.71 as its minimum supported compiler version. Its license is `MIT OR Apache-2.0`, and the README states that contributions are dual licensed under those terms unless explicitly stated otherwise. This is licensing information, not legal advice. The repository is not archived, and the last push was on 2026-08-03. Releases include 0.2.23 on 2026-07-18 and 0.2.22 on 2026-05-19. Version pinning matters because the library uses an exact dependency on `typetag-impl` version 0.2.23 in the current manifest.
Editorial conclusion
Adopt Typetag when the program needs a finite, discoverable set of Serde implementations behind a trait object and accepts registry-based discovery. Do not choose it for runtime-defined open-world plugins or targets where the required setup is unsuitable. Verify first that every relevant trait implementation has `#[typetag::serde]`, that the selected tag representation matches existing data, and that the minimum Rust version is 1.71 or later.
Frequently asked questions
What is Typetag used for in Rust?
Typetag provides Serde serialization for `&dyn Trait` values and serialization plus deserialization for `Box<dyn Trait>` values. It models each registered trait implementation like an enum variant.
Does Typetag support trait implementations in other crates?
Yes. The README states that serialization and deserialization support implementations across the dependency graph of the final program binary.
Can Typetag round-trip trait objects through Bincode?
Yes. The README states that all three enum representations round-trip correctly through compact binary formats including Bincode.
What must be annotated for Typetag to register an implementation?
The trait definition and each relevant implementation block need the `#[typetag::serde]` attribute. Each concrete implementation must also provide the `Serialize` and `Deserialize` behavior required by the application.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/dtolnay-typetag)Star history chart for your README
[](https://hysenlabs.com/projects/dtolnay-typetag)Was this useful?