erased-serde: making Serialize and Deserializer usable as trait objects
Type-erased Serialize, Serializer and Deserializer traits
At a glance
- What is it?
- dtolnay/erased-serde is a low-level Rust crate that provides type-erased versions of Serde's Serialize, Serializer and Deserializer traits, so they can be stored as Box<dyn ...>. It solves object safety for generic traits, and it leaves tagging, formats and policy to whoever builds on top of it.
- Who is it for?
- erased-serde is the right choice when you need to store heterogeneous Serde implementations in one collection or pass them through a generic API, and the wrong choice when you want runtime type dispatch with a tag in the output, which is what typetag handles.
- 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 received new commits within the last day.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why Serde's traits cannot be Box<dyn Serialize>
Serde's three central traits, Serialize, Serializer and Deserializer, cannot be used as trait objects in the ordinary way. Writing &dyn Serialize or Box<dyn Serialize> does not compile, because Rust's object safety rules reject traits that contain generic methods, and all three of these traits do. The crate exists to work around exactly that, and it does so without changing Serde itself.
That framing matters for the design. This is not a reimplementation of Serde with a different API, and it is not a fork. The crate's own description calls it a set of type-erased versions of the Serde traits, and the README states that the traits work with any existing Serde Serialize and Deserialize type and any existing Serde Serializer and Deserializer format. A type you already have can be erased into a trait object without a derive macro, without a new trait, and without changing its implementation.
The payoff shows up when a value's concrete type is not known at compile time but its behavior still needs to be uniform. A map that holds several formats, a plugin registry that accepts any serializable payload, a cache layer that stores arbitrary values, all of these want a single collection type, and Serde's own traits cannot be the element type of that collection. erased_serde makes them work as the element type.
The erase entry point and how a value is written out
Erasure works by an associated method that takes a concrete implementation and produces a boxed trait object of the erased trait. The README's serialization example builds two serializers, one for JSON and one for CBOR, and erases both into a map of boxed trait objects:
let mut formats: Map<&str, Box<dyn Serializer>> = Map::new();
formats.insert("json", Box::new(<dyn Serializer>::erase(json)));
formats.insert("cbor", Box::new(<dyn Serializer>::erase(cbor)));Values get the same treatment, so a heterogeneous collection of serializable data can live in one map alongside a heterogeneous collection of output formats. The write itself goes through a method on the trait object rather than through the concrete type:
// These are boxed trait objects as well. Same thing here - type erasure
// makes this possible.
let mut values: Map<&str, Box<dyn Serialize>> = Map::new();
values.insert("vec", Box::new(vec!["a", "b"]));
values.insert("int", Box::new(65536));
// Pick a Serializer out of the formats map.
let format = formats.get_mut("json").unwrap();
// Pick a Serialize out of the values map.
let value = values.get("vec").unwrap();
// This line prints `["a","b"]` to stdout.
value.erased_serialize(format).unwrap();The important detail is that the erased trait object still owns the real implementation. A call to erased_serialize on the boxed value delegates to the concrete Serialize implementation it was erased from, which then drives the concrete serializer. Nothing about the output format changes, and nothing about the value's own Serialize implementation changes. The type information is dropped at the boundary and re-supplied by the implementation underneath.
Reading values back through erased_serde::deserialize
The read path mirrors the write path. A deserializer is erased into a boxed trait object, and then a free function in the crate takes that trait object and a target type to produce a concrete value:
let mut formats: Map<&str, Box<dyn Deserializer>> = Map::new();
formats.insert("json", Box::new(<dyn Deserializer>::erase(json)));
formats.insert("cbor", Box::new(<dyn Deserializer>::erase(cbor)));
// Pick a Deserializer out of the formats map.
let format = formats.get_mut("json").unwrap();
let data: Map<String, usize> = erased_serde::deserialize(format).unwrap();
println!("{}", data["A"] + data["B"]);The destination type is a normal generic parameter, so the compiler still checks it against the data at the call site. What has been erased is the format, not the target type. That is the asymmetry worth understanding before adopting the crate: the erased trait object is the variable part, and the concrete output type is the fixed part. If your use case needs the output type to be variable too, this crate does not solve that on its own, which is exactly the gap typetag fills.
There is also a detail in how the deserializer is constructed in the example. The JSON deserializer is created with from_slice over a byte slice, and the CBOR deserializer likewise, and both are then erased. Because the erased trait object borrows the underlying data, the lifetime of the buffer constrains how long the boxed deserializer can live. Nothing in the crate removes that constraint.
Three obstacles the crate has to work around
The README explains that the technique is not new, that it is a general method for building trait objects of traits with generic methods, and that the crate points to example code and a playground demonstration of a simplified version. The erased-serde case is harder than that example for three stated reasons, and each one explains a design choice in the API.
First, Serde traits have methods that take self by value. The approach used is to implement the object-safe trait for Option<T>, where T implements the real trait, so the by-value method can be expressed on the erased object.
Second, traits with associated types, such as Serializer::Ok and Visitor::Value, cannot name a type in an object-safe trait. The crate handles this by carefully stashing things behind a pointer for the short term. Any API you build on top will inherit that indirection.
Third, some trait methods have a generic type in the return position but none in the argument position, and SeqAccess::next_element is the example given. Those get flipped into a callback style, where the return value is passed on to a generic argument instead of being returned directly. This is the kind of transformation that changes how a call site reads, and it is the reason the erased traits do not feel like the originals even though they interoperate with them.
Wiring erased-serde into a Cargo project
Adding the crate is a normal dependency entry, and the README shows the two lines a project needs:
[dependencies]
serde = "1.0"
erased-serde = "0.4"The crate's own manifest pins the details. The package is named erased-serde, the current version recorded there is 0.4.10, the edition is 2021, and the minimum supported Rust version is 1.68. The manifest is also where the feature model lives, and it carries a comment worth reading before you enable anything:
[features]
# Note: at least one of "std" or "alloc" must be enabled.
default = ["std"]
std = ["alloc", "serde_core/std"]
alloc = ["serde_core/alloc"]
unstable-debug = []That note is the only real configuration trap. The default feature is std, which pulls in alloc, so a normal build is fine out of the box, but a no_std target that disables default features must turn on alloc or std explicitly or the build will fail. The manifest also lists no-std as a crate category, so the use case is supported rather than accidental. The manifest depends on serde_core with default features off and on typeid, and it depends on the full serde crate only under a target that is never built, which is how the crate stays usable with no_std while still linking Serde in normal builds.
A low-level block, with typetag as the higher-level option
The README is unambiguous about the intended audience. It says this library should be considered a low-level building block for interacting with Serde APIs in an object-safe way, and that most use cases will require higher level functionality such as typetag, which uses the crate internally.
That is the real alternative to consider, and the difference is one of scope rather than of technique. erased-serde gives you the ability to hold a trait object and call it. typetag builds the dispatch layer on top of what this crate provides. If your requirement is a uniform API over heterogeneous values with a fixed output type, the lower-level crate is enough; if you need more, the README points to typetag for most use cases.
The other documented alternative is plain generic programming. Serde itself works perfectly well when the type is a generic parameter. The moment the concrete type must vary at runtime, generics stop helping, and the choice becomes between an enum of variants, a boxed trait object, or a tagged representation. erased-serde covers the boxed trait object case only, and the README does not discuss the enum approach at all.
Licensing, versioning and what the last push covers
The crate is offered under a dual licence, Apache License Version 2.0 or MIT at your option, and the README states that contributions are dual licensed the same way unless stated otherwise. The manifest records the same choice as MIT OR Apache-2.0. That combination is the standard permissive pairing, and it means a downstream project can pick either terms, though choosing which license applies to your own distribution is a decision for your own legal review rather than something this crate decides for you.
On the version line, the manifest records 0.4.10 and the published releases run 0.4.10 on 2026-03-02, 0.4.9 on 2025-11-07 and 0.4.8 on 2025-09-14. The last push on the repository was on 2026-09-22, which is after the 0.4.10 release, so the working tree contains changes beyond the published crate even though the version number in the manifest is unchanged. The repository holds the usual crate furniture: a benches directory, a build.rs, an explanation directory with the worked example, a src directory and a tests directory, plus a workspace with an explanation member and a tests/crate member. The manifest also pins documentation build arguments for docs.rs and declares a x86_64-unknown-linux-gnu target for that build.
Editorial conclusion
erased-serde is the right choice when you need to store heterogeneous Serde implementations in one collection or pass them through a generic API, and the wrong choice when you want runtime type dispatch with a tag in the output, which is what typetag handles. Verify first that your Rust toolchain meets the 1.68 minimum stated in Cargo.toml, that at least one of the std or alloc features is enabled for your build, and that the trait objects you plan to store do not require associated types your own API needs to name. The crate version at the last push on 2026-09-22 was 0.4.10, published on 2026-03-02.
Frequently asked questions
What does the erased-serde crate actually do?
It provides type-erased versions of Serde's Serialize, Serializer and Deserializer traits so they can be used as trait objects. Serde's own traits contain generic methods, which Rust's object safety rules do not allow in a trait object, and this crate works around that.
How do I add erased-serde to a Cargo project?
Add the two dependencies the README shows, serde = "1.0" and erased-serde = "0.4", to the dependencies table in Cargo.toml. The crate requires Rust 1.68 or newer according to its manifest.
Does erased-serde work with no_std?
The crate lists no-std as a category and its manifest notes that at least one of the std or alloc features must be enabled. The default feature is std, so you only need to choose explicitly when you disable default features for a no_std target.
Should I use erased-serde or typetag?
The README describes erased-serde as a low-level building block and points to typetag for most use cases, noting that typetag uses this crate internally. Use erased-serde directly when you need a uniform API over heterogeneous values; reach for typetag when you need higher level functionality on top of it.
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-erased-serde)Was this useful?