# nanoserde: Rust serialization with an empty dependency tree

> nanoserde is a serialization library for Rust with no dependencies at all, forked from makepad-tinyserde with syn, proc_macro2 and quote removed from the build tree, and it covers JSON, a compact binary format and RON. It is aimed at teams building on small or constrained targets where a proc macro dependency tree is a problem.

**not-fl3/nanoserde** — Serialization library with zero dependencies

- Repository: https://github.com/not-fl3/nanoserde
- Stars: 914 · Forks: 65
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/not-fl3-nanoserde

## The point is the cargo tree, not the feature list

nanoserde is described as a serialization library with zero dependencies, and the README makes the claim concrete by showing what the tree looks like:

```
> cargo tree
nanoserde v0.2.0 (/../nanoserde)
└── nanoserde-derive v0.2.0 (/../nanoserde/derive)
```

Two entries, and the second is the derive macro crate that ships with the project. The README states the reason plainly: this is a fork of makepad-tinyserde with all the dependencies removed, and there is no more syn, proc_macro2 or quote in the build tree.

That matters more than it might sound. The standard derive approach in Rust relies on proc_macro2 for token streams, quote for generating code, and syn for parsing the syntax tree. syn in particular is a large crate with a wide dependency list, and pulling it into a build means compiling it on every machine that builds your project, and vendoring it into any environment with a locked-down registry or a small toolchain image.

For a project building for a microcontroller, a WebAssembly module with a strict size budget, or a Linux distribution that maintains a minimal Rust stack, that build cost is the whole argument. The price is that you get a serialization library with a much smaller surface than serde, and the README is unusually honest about exactly where that surface ends.

The authors are listed in Cargo.toml as makepad and Fedor, the latter being not.fl3, and the project is published on crates.io as `nanoserde` with documentation on docs.rs.

## The derive macros are the whole API

The usage example in the README is short enough to be the entire mental model:

```rust
use nanoserde::{DeJson, SerJson};

#[derive(Clone, Debug, Default, DeJson, SerJson)]
pub struct Property {
    pub name: String,
    #[nserde(default)]
    pub value: String,
    #[nserde(rename = "type")]
    pub ty: String,
}
```

You derive `DeJson` and `SerJson` on a struct and the derive generates the serialization and deserialization implementations, using the `nserde` attribute namespace rather than `serde`. The `#[nserde(default)]` attribute fills a field with its `Default` value when it is missing on the way in, and `#[nserde(rename = "type")]` changes the key in the serialized form while leaving the Rust field name alone.

The naming choice is deliberate and worth understanding. Anyone arriving from serde will look for `#[serde(...)]` first, and the different prefix makes it obvious that the attribute set is not the serde one, even where the names match. It also means the attributes are parsed by the fork's own macro implementation rather than handed to serde's machinery.

The README points at the `tests` directory for more examples, which is the right place to look, because the attribute behaviour is where the real detail lives and the test suite is where the fork's author pinned it down.

Note the version in the tree output is 0.2.0 while Cargo.toml reads 0.2.2. The derive crate is pinned with an exact version requirement, `version = "=0.2.2"`, so the two crates in your build always match.

## The support matrix is the honest specification

The features support matrix is the most useful thing in the README, and it is the part to read before writing any code.

Across four formats, JSON, bin, RON and TOML, the matrix records what is supported. Structs, enums, `HashMap` fields, `Vec` fields, `Option` fields and the primitive and generic types are supported in JSON, binary and RON. Tuple structs are supported in binary and RON but not in JSON.

The attribute rows are where the format differences become concrete. `#[nserde(default)]` on a field works in JSON and RON but not in binary. `#[nserde(rename = "")]` works in all three implemented formats. `#[nserde(proxy = "")]` works only in binary. `#[nserde(serialize_none_as_null)]` works only in JSON, and the same attribute at container level also only in JSON. `#[nserde(transparent)]` is JSON only. `#[nserde(skip)]`, which implies `default`, works in JSON and RON but not in binary. And `#[nserde(crate = "")]` works everywhere, which is what you use when your types live in another crate and you want to avoid the leading path in the generated code.

So a type can be serializable in RON and not in JSON, and an attribute can exist in one format and not another. A generic rule of thumb falls out of that: JSON and RON behave alike, and binary is the odd one out with different attribute support.

The TOML column is `no` for every single row, which is worth pausing on, because Cargo.toml does declare a `toml` feature.

## The TOML feature exists but nothing implements it

This is the sharpest inconsistency in the project, and it is the kind of thing that will cost you an afternoon if you do not check.

The crate features table lists four formats with their feature names: `binary`, `json`, `ron` and `toml`. The TOML format has its own row.

But look at the feature definitions in Cargo.toml:

```toml
[features]
default = ["binary", "json", "ron", "toml", "std"]

binary = ["dep:nanoserde-derive", "nanoserde-derive/binary"]
json = ["dep:nanoserde-derive", "nanoserde-derive/json"]
ron = ["dep:nanoserde-derive", "nanoserde-derive/ron"]
toml = []

std = []
```

The `toml` feature is empty. The three working formats each pull in the derive crate with the matching derive feature, and `toml` pulls in nothing at all. And the support matrix marks every TOML capability as unsupported.

So the TOML feature is a placeholder. Enabling it does nothing, and there is no TOML support to enable. It is on by default, so a default build carries a feature flag that leads nowhere.

The practical advice is to treat the supported formats as three, JSON, binary and RON, and to disable the default features if you want a minimal build:

```toml
nanoserde = { version = "*", default-features = false, features = ["std", "{format feature name}"] }
```

Note that `std` is a separate feature with no dependencies either, so a no_std build is a matter of leaving it out. The other optional dependencies in the manifest are `bytes` and `chrono`, and both are behind their own features, so the zero dependency promise holds for the core formats.

## What the attributes let you express

Reading the attribute rows together gives a fair picture of the expressiveness available.

At field level you have `default` to substitute a default value for a missing field, `rename` to change the serialized key, `proxy` to serialise a field through a different type, and `serialize_none_as_null` to write an absent `Option` as an explicit null rather than omitting it.

At container level you have the same set plus two more. `default = ""` and `default_with = ""` give the container a default value, either a literal or the result of a path you name, and `skip` excludes a field from the output while implying `default` on the way in.

`proxy` deserves a second look because it is the one that changes the type relationship rather than just the representation. It lets a Rust field be stored or serialised as something else, which is how you keep a wire format stable while your internal type changes.

And `serialize_none_as_null` is the attribute that shows the crate is aware of JSON's optional-field semantics. Most serializers omit a `None` field entirely, which is fine in JSON but produces different bytes in a compact binary format where the shape of the output matters. Being able to write an explicit null gives the encoder a stable field count, which is exactly what you want when the output is hashed or compared byte for byte.

The limits are worth stating plainly as well. There is no attribute in the matrix for skipping serialization only or deserialization only, no `flatten`, no `borrow` or `Cow` handling, and no numeric or date-time specialisation beyond what the primitive rows cover. The `bytes` and `chrono` optional dependencies suggest support for those types, but the matrix does not spell out how they behave, so that is a question for the test suite rather than for the README.

## Choosing between this and serde

The alternative is obvious enough to name, and the difference is not subtle.

serde derives its implementation with proc_macro2 and syn and quote, which buys a very large attribute vocabulary, a large ecosystem of companion crates, and integration with the wider Rust ecosystem through `serde` with `derive` and the format crates that implement its traits. It also means a build tree with a few hundred transitive dependencies and a noticeably longer clean build.

nanoserde takes the opposite trade. The dependency tree is two entries, both of them in the same repository. The attribute vocabulary is a couple of dozen entries, with differences between formats. The formats are three rather than dozens, and there is no trait-based extension point for adding your own format, because there is no `Serializer` trait to implement.

That last point is the one to think about most carefully. With serde, a format crate plugs into a shared trait, so adding a format is a separate crate that anyone can write. With nanoserde, the formats are baked into the derive crate, and adding one means modifying the fork.

The decision therefore comes down to what your build environment costs you and what your data shapes need. If you are building a normal server application with ordinary structs and enums, serde costs you nothing that matters and gives you far more room. If you are compiling for an embedded target with a size budget, or you maintain a Rust toolchain in an environment where fetching crates is expensive or impossible, the dependency-free tree is the reason to take nanoserde, and the reduced attribute set is the price.

## Dual licensed, no releases, and a small maintainer base

The Cargo.toml declares `license = "MIT OR Apache-2.0"`, and the repository carries both `LICENSE-MIT` and `LICENSE-APACHE` at the top level. The repository licence field is classified as Apache-2.0. The dual licence is the standard Rust convention and lets you pick whichever suits your organisation, and for a library this small it means there is no restriction on using it inside a closed product.

The metadata also names the authors as makepad, whose tinyserde it forks, and Fedor, who did the fork, which tells you the provenance and who to look at for the dependency removal work.

On releases, the repository has no GitHub releases, so the crates.io version is the only release channel, and the last push was on 2026-10-05 with the manifest at version 0.2.2. The version history is in the crate rather than in tags, so if you need to know what changed between versions the answer is the changelog in the crate's documentation and the commit history rather than a release page.

There is also a Discord link in the README, which is the fastest route to the people who can answer questions about a fork with this particular scope.

The practical summary for an adopter: a small, actively touched crate with a MIT or Apache-2.0 licence, three working formats, a support matrix you should treat as the contract, and a TOML feature that is currently a placeholder.

## Conclusion

Adopt nanoserde when you are compiling for a target where the syn and proc_macro2 dependency tree is unacceptable, such as a small embedded build, and your data shapes fit the simple struct and enum model it supports. Skip it if you need the full serde attribute vocabulary, TOML support, or the wider ecosystem that comes with serde, because the support matrix shows exactly which attributes exist and the TOML feature is declared but unimplemented. Verify first that the fields and attributes your types need appear in the matrix for the format you intend to use, since coverage differs per format, and pin the `nanoserde-derive` version to match `nanoserde` because the dependency is an exact version requirement. The Cargo.toml declares MIT OR Apache-2.0, the last push was on 2026-10-05, and the repository has no GitHub releases.

## FAQ

### What is nanoserde?

nanoserde is a Rust serialization library with zero dependencies. It is a fork of makepad-tinyserde with all the dependencies removed, so there is no syn, proc_macro2 or quote in the build tree, and the cargo tree contains only nanoserde and its own nanoserde-derive crate.

### Which serialization formats does nanoserde support?

The support matrix marks JSON, binary and RON as supported for serialization and deserialization, with structs, enums, HashMap and Vec fields, Option fields and primitives covered. Every TOML row is marked unsupported, even though a `toml` Cargo feature is declared and enabled by default.

### How do I use the nanoserde derive macros?

Import the traits, for example `use nanoserde::{DeJson, SerJson};`, and derive them on your type. The attributes use the `nserde` namespace, so `#[nserde(default)]` fills a missing field with its default and `#[nserde(rename = "type")]` changes the serialized key.

### How do I build nanoserde with only the formats I need?

Turn off the default features and list the ones you want, for example `nanoserde = { version = "*", default-features = false, features = ["std", "json"] }`. The available format features are `binary`, `json`, `ron` and `toml`, and `std` is a separate feature you can leave out for a no_std build.

### What are the limits of the nanoserde support matrix?

Coverage differs by format. Tuple structs are unsupported in JSON, `#[nserde(default)]` on a field and `#[nserde(skip)]` are unsupported in binary, `#[nserde(proxy = "")]` works only in binary, and `#[nserde(serialize_none_as_null)]` and `#[nserde(transparent)]` work only in JSON.

### What licence is nanoserde released under?

The Cargo.toml declares `MIT OR Apache-2.0` with both LICENSE-MIT and LICENSE-APACHE files in the repository. The manifest version is 0.2.2, the last push was on 2026-10-05, and the repository has no GitHub releases.

## Sources

- [Issues](https://github.com/not-fl3/nanoserde/issues)
- [License: Apache-2.0](https://github.com/not-fl3/nanoserde/blob/master/LICENSE)
- [not-fl3/nanoserde on GitHub](https://github.com/not-fl3/nanoserde)
- [README](https://github.com/not-fl3/nanoserde/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/not-fl3-nanoserde
