Skip to content

Formatters & Code

JSON to Rust Struct

Generate idiomatic Rust structs with serde derives.

Runs in your browser
JSON · source
lines: 17chars: 261size: 261 B
Rust struct · result
lines: 21chars: 486size: 486 B
live

Understanding JSON → Rust

serde does most of the work.

Why every Rust JSON codegen tool emits the same skeleton, and the handful of attribute choices that matter for it to be useful.

The serde skeleton.

Every Rust JSON struct starts with #[derive(Serialize, Deserialize, Debug)] and a body that names each field with its inferred type. The serde derive macros do all the heavy lifting — generate the parsing code at compile time, with zero runtime cost and full type safety. The job of the codegen tool is to emit the struct definition and the attribute annotations; serde does the rest.

#[derive(Serialize, Deserialize, Debug)] struct T {}

Option carries nullability.

Rust has no implicit null. A field that can be missing or null in JSON becomes Option<T>None when missing, Some(value) when present. The serde default treats missing fields as a deserialization error unless they're Option or annotated with #[serde(default)]. The codegen tool decides: every fieldOption (loose, forgives missing input) or required (strict, fails on unexpected omission). Strict-by-default produces better error messages; loose-by- default is easier to live with for evolving APIs.

rename, rename_all.

Rust's idiomatic field naming is snake_case. Most JSON APIs also use snake_case, so field names just match. But when they don't — JSON in camelCase, kebab-case, or with fields named after Rust reserved words like type — you need #[serde(rename = "name")] or the struct-level #[serde(rename_all = "camelCase")]. Codegen tools emit these automatically when the JSON name and the Rust name diverge.

A worked example.

From { "userId": 42, "name": "Q", "avatar_url": null, "tags": ["x"] }: two fields are camelCase, one is snake_case, one is null. A useful codegen emits a struct with a top-level #[serde(rename_all = "camelCase")] (handles userId implicitly), then explicit #[serde(rename = "avatar_url")] on the snake-cased outlier, and an Option<String> for the null-able avatar field.

Mixed-case JSON input

userId, name, avatar_url, tags

One rename_all attribute plus one specific rename keeps the struct clean.

rename_all camelCase + rename avatar_url

= user_id, name, avatar_url, tags

String vs &str.

Almost every Rust JSON struct should hold owned String values. Borrowed &str is tempting (no allocation) but requires a lifetime parameter and ties the struct's lifetime to the input buffer — which makes it useless for anything beyond the immediate decode. The win from borrowed strings is real for extreme-throughput cases (network proxies, parsers); for normal application code, String is the right default. Codegen tools default to owned for that reason.

Numbers, the i64-vs-u64-vs-f64 question.

JSON numbers are technically arbitrary-precision; in practice they're floats. Serde maps them to whatever Rust integer or float type you declare, and errors at decode time if the value doesn't fit. A field with sample 42 could be i32, i64, u32, u64, f64 — the inferrer picks based on signed-ness, observed magnitude, and a default precision. i64 is the safest default; bump to f64 if you see any decimals.

Enums for tagged unions.

When a JSON array holds shapes with a discriminator field, Rust's enum is a perfect fit. Annotated with #[serde(tag = "type")], an enum like enum Event { Click { x: i32, y: i32 }, Submit { form: String } } parses cleanly from { "type": "Click", "x": 4, "y": 8 }. The codegen tool that recognises this pattern produces dramatically better Rust than one that merges everything into a struct with optional fields.

Frequently asked questions

Quick answers.

Which crates are required to use the output?

The generated code assumes you are using `serde` with the `derive` feature enabled in your `Cargo.toml`. Most outputs include `#[derive(Serialize, Deserialize)]` attributes by default.

How are field names handled?

Rust prefers `snake_case` while JSON often uses `camelCase`. The tool generates idiomatic Rust names and adds `#[serde(rename = "...")]` attributes where the source key and field name differ.

Are my data structures sent to a server?

No. The logic for parsing the JSON and generating the Rust string is executed locally in your browser. Your API responses and internal schemas remain private.

How does it handle nested objects?

The generator recursively traverses the JSON tree and creates separate named structs for nested objects. This avoids deep nesting of anonymous types and promotes better code reusability.

People also search for

Related tools

More in this room.

See all in Formatters & Code