serde_with: custom de/serialization helpers for Rust's serde
This crate provides custom de/serialization helpers to use in combination with serde's `with`-annotation and with the improved `serde_as`-annotation.
At a glance
- What is it?
- serde_with is a Rust crate of adapter types and attribute macros that fill the gaps serde's own with and serde_as annotations leave open: Display and FromStr conversions, arrays beyond 32 elements, const generics, skipping empty Option fields and prefixing field names. It installs with cargo add serde_with and follows serde's own conventions.
- Who is it for?
- serde_with earns a place in any Rust project whose JSON or TOML shapes do not line up with its Rust types, because the adapter types are written once, documented on docs.rs and released on a fast cadence, while a hand written module of Display and FromStr wrappers duplicates work that serde's own maintainers have no reason to ship.
- 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 3 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 October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why serde's own annotations leave gaps
serde derives Serialize and Deserialize from the shape of your Rust types, and its field level attributes let you adjust that for special cases. Two of those attributes matter here. The with annotation attaches a custom serializer and deserializer to a field, and serde_as is the improved version of the same idea.
Both of them have the same limitation: they apply to a whole field, and they cannot express a conversion in the middle of a type. If you want a struct field of type u8 to appear in JSON as the string "12", you need a type that serializes through Display and deserializes through FromStr, and that type has to exist somewhere.
serde_with is the crate that collects those types. The README describes it as providing custom de/serialization helpers to use in combination with serde's with annotation and with the improved serde_as annotation.
The crate also covers a second class of gap. serde itself does not support arrays with more than 32 elements or arrays written with const generics, which is an awkward limit when a payload contains a fixed size blob. serde_as circumvents that restriction, including for nested arrays.
So the crate is not a serializer. It is a set of adapters and attribute macros that let serde handle the shapes your data actually has.
DisplayFromStr for types that already have a text form
The most common use case is a type that already implements Display and FromStr: the README names u8, url::Url and mime::Mime as examples. The adapter is called DisplayFromStr, and the pattern is to serialize with Display and deserialize with FromStr.
#[serde_as]
#[derive(Deserialize, Serialize)]
struct Foo {
// Serialize with Display, deserialize with FromStr
#[serde_as(as = "DisplayFromStr")]
bar: u8,
}
// This will serialize
Foo {bar: 12}
// into this JSON
{"bar": "12"}Note where the attribute sits. The README is explicit that the serde_as attribute must be placed before the derive, and that the as form is analogous to serde's with attribute. The as value is a type, not a module path to a function pair, and it mirrors the structure of the field: a Vec of u8 becomes Vec<DisplayFromStr>, and a map becomes a map type with the converter applied to the value position.
One detail saves a lot of awkwardness. The default de/serialization behaviour can be restored by using _ as a placeholder, so a map written as BTreeMap<_, DisplayFromStr> keeps the plain key behaviour and only converts the values. Without that placeholder you would have to name the key type and converter separately for every combination.
Arrays past 32 elements and const generic sizes
The second capability has no serde equivalent at all. serde does not support arrays with more than 32 elements or arrays using const generics, and the README states that the serde_as attribute allows circumventing this restriction, even for nested types and nested arrays.
The trick is the bracket syntax. A field declared as [bool; N] is annotated with a placeholder for the length, and the length comes from the type rather than from the annotation.
#[serde_as]
#[derive(Deserialize, Serialize)]
struct Arrays<const N: usize, const M: usize> {
#[serde_as(as = "[_; N]")]
constgeneric: [bool; N],
#[serde_as(as = "Box<[[_; 64]; N]>")]
nested: Box<[[u8; 64]; N]>,
#[serde_as(as = "Option<[_; M]>")]
optional: Option<[u8; M]>,
#[serde_as(as = "Bytes")]
bytes: [u8; M],
}Three things are happening there. The underscore takes the length from the field's own type, which is what makes const generics work. The nested case shows that the annotation can be applied inside a Box and inside another array, so a two dimensional buffer still serialises. And the Option case shows that the placeholder works inside a generic wrapper as well.
The last field uses the special Bytes adapter, and the README notes that it exists for efficiency much like the serde_bytes crate. That matters for real payloads: a byte array of any length is written as a compact byte sequence rather than as a JSON array of numbers.
The cost is compile time and a slightly more verbose annotation, since the size has to appear in the type rather than being inferred.
Skipping empty Option fields and renaming fields in bulk
The third group of helpers is about the shape of the output rather than the conversion of a value.
skip_serializing_none drops every field that is None from the serialised output. The README notes that this situation often occurs with JSON, and that other formats support optional fields too. The problem it solves is annotation fatigue: if a struct has twenty optional fields and you want them omitted rather than serialised as null, adding a per field attribute to each one is tedious.
#[skip_serializing_none]
#[derive(Deserialize, Serialize)]
struct Foo {
a: Option<usize>,
b: Option<usize>,
c: Option<usize>,
d: Option<usize>,
e: Option<usize>,
f: Option<usize>,
g: Option<usize>,
}
// This will serialize
Foo {a: None, b: None, c: None, d: Some(4), e: None, f: None, g: Some(7)}
// into this JSON
{"d": 4, "g": 7}Again the ordering rule applies: the attribute must be placed before the derive. The result is a payload that contains only the fields that carry information, which usually means a smaller request body and a cleaner log line.
The other two helpers work on names rather than values. The with_prefix! and with_suffix! macros apply a prefix or a suffix to each field name of a struct without changing the de/serialize implementations of the struct itself. That is the difference from writing a rename attribute on every field, and it is what makes the macros useful for generated code or for types you do not own.
A third helper in the same family, StringWithSeparator, handles the reverse direction: deserialising a comma separated list such as #hash,#tags,#are,#great into a Vec<String>, with the separator type supplied by the caller.
Type level converters inside a single field
The advanced examples show what serde_as can express that with cannot, and they are the reason to prefer serde_as over the older attribute in new code.
The first case converts a Vec of Duration into a list of numbers expressed in seconds, using a converter named DurationSeconds that the crate provides. The second case treats a Vec of pairs as a map with duplicate keys: JSON only allows string keys, so the integer keys are converted to strings with DisplayFromStr, and the byte values are hex encoded with the Hex adapter. The README notes that the resulting JSON map contains the key "1" twice, which is what a map with duplicate keys looks like once it has been forced through a format that cannot represent them.
The point is that serde_as works on the type structure of the field. The annotation is a type expression, so a variant of an enum can carry a completely different conversion from its siblings, and a nested field can be converted without touching the outer type.
The cost of that flexibility is that the annotation language is more to learn than serde's with attribute, and the crate answers that with a user guide on docs.rs that the README links to as the place for more tips and tricks, plus a discussion board on GitHub and the users.rust-lang.org forum for questions that the guide does not answer.
Adding the crate and building it
Installation is one command, given in the README under the section for using serde_with in your project.
# Add the current version to your Cargo.toml
cargo add serde_withThe crate carries feature flags for integration with other common crates, and the README points at the feature flags section of the documentation for the full list. The repository topics include base64, chrono and hex, which are the ecosystems the integrations target, and enabling them pulls those dependencies into your build. Being selective here matters, because every feature you turn on is a dependency your binary carries.
The source itself is a Cargo workspace with three members: serde_with for the runtime library, serde_with_macros for the attribute macros, and serde_with_test for the test crate. The shared workspace package metadata sets edition 2021, a minimum Rust version of 1.88, the version 3.24.0, and the licence as MIT OR Apache-2.0.
The workspace lints section is worth a look if you are copying the setup into your own project. It turns on missing_docs and several numeric and cast lints as warnings, and it allows a small number of clippy lints that are known to produce false positives in macro generated code. The repository also carries rustfmt.toml, clippy.toml, a .devcontainer directory, CONTRIBUTING.md and SECURITY.md.
Release cadence and what the licence allows
The project is actively releasing. The tags show v3.24.0 on 2026-09-26, v3.23.0 on 2026-09-07 and v3.22.0 on 2026-08-09, and the last push was on 2026-10-17. The workspace release metadata in Cargo.toml also shows that the version is shared across the three crates and that tags are signed.
That cadence has a practical effect on how you should depend on it. Minor versions arrive every few weeks, so a caret requirement in your Cargo.toml will pull in new adapters and, occasionally, behaviour changes inside an existing adapter. Pinning exactly is an option if you need reproducibility, and the release metadata means the three crates always move together, so there is no need to match versions across them by hand.
On licensing, the workspace metadata and the README agree: the crate is available under either the Apache License version 2.0 or the MIT licence, at your option, and the repository ships LICENSE-APACHE and LICENSE-MIT side by side. Both are permissive licences that allow use in proprietary software, with attribution and the Apache patent grant in the Apache case. Whether that fits your organisation is a question for your own legal review, not something the repository decides for you.
Editorial conclusion
serde_with earns a place in any Rust project whose JSON or TOML shapes do not line up with its Rust types, because the adapter types are written once, documented on docs.rs and released on a fast cadence, while a hand written module of Display and FromStr wrappers duplicates work that serde's own maintainers have no reason to ship. It is the wrong dependency for a codebase whose types already implement Serialize and Deserialize directly, and it adds a dependency to every crate that shares the types, so it belongs in the leaf crate that defines the format, not in the shared domain crate. Verify first that the adapter you need exists in the current release, since the crate is still moving between minor versions, and read the feature flags list before enabling the integrations for base64, chrono or hex.
Frequently asked questions
How do I add serde_with to a Rust project?
Run cargo add serde_with, which adds the current version to your Cargo.toml as the README shows. Optional integrations with other crates are enabled through feature flags.
What is the difference between serde's with annotation and serde_as?
The README describes serde_as as the improved annotation and says it is analogous to with, but it works on the type structure of a field, so converters can be applied to inner types and to nested types, and to variants of an enum.
Can serde_with serialise arrays with more than 32 elements?
Yes. serde itself does not support arrays over 32 elements or const generics, and the serde_as attribute circumvents that restriction, even for nested types, with a Bytes adapter for byte arrays.
What licence does the serde_with crate use?
It is available under either the Apache License version 2.0 or the MIT licence at your option, and the workspace metadata in Cargo.toml records it as MIT OR Apache-2.0.
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/jonasbb-serde-with)Was this useful?