Why generate Rust structs from a sample?
serde makes JSON in Rust nearly free of boilerplate — nearly.
Somebody still has to write the structs, and for a real API
response that means a dozen nested types with field names the
API chose in camelCase and your codebase needs in snake_case,
plus a decision about every field that only sometimes shows
up. The sample already contains all of that evidence. This
converter extracts it mechanically: objects become structs,
arrays become Vec, absence becomes
Option, and the keys that had to be renamed get
their #[serde(rename)] attributes written for you.
How to use this tool
- Paste the JSON. A real API response is ideal; include several array elements if you have them, because absence across elements is how
Optionfields are discovered. - Name the root struct.
Rootis fine for a quick look; for code you will keep, use the name it will wear in your project. - Press Generate Rust. The root struct comes first, the structs it references after it.
- Read the notes if any appear. They list every slot that became
serde_json::Value, every keyword escape and every name collision — nothing was decided silently. - Copy or download the result, then refine it: swap in
chronooruuidtypes, add#[serde(default)]where your API omits keys, and let the compiler take it from there.
The rules it follows
Numbers. All-whole numbers become
i64; any fraction widens the slot to
f64 — the same widening rule as this site's other
generators, and for the same reason: a JSON 1.0 is
the number 1 to every parser, so a field your API always sends
as 2.0 reads as an integer here. Names.
Structs are PascalCase after the key that holds them; fields
are snake_case, with a rename attribute whenever that changes
the spelling. Absence and null. Missing from
some elements → Option<T>; sometimes null →
Option<T>; null everywhere or genuinely
mixed types → serde_json::Value, always noted.
Merging. Identical shapes — same keys, same
types — share one struct instead of being emitted twice.
Worked example
Given this sample (the second user has no nickname, and one key is camelCase):
{
"teamName": "Platform",
"users": [
{ "id": 1, "firstName": "Ada", "nickname": "ada" },
{ "id": 2, "firstName": "Grace" }
]
} the converter emits:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
#[serde(rename = "teamName")]
pub team_name: String,
pub users: Vec<User>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: i64,
#[serde(rename = "firstName")]
pub first_name: String,
pub nickname: Option<String>,
}
The renames are the API's camelCase talking; the
Option is the second user talking. Both are the
sample's facts, written down where Rust needs them.