JSON to Rust Serde
Generate Rust serde structs from sample JSON with correct Option<T> optionality and exact 64-bit integers — entirely on this device.
This sets field visibility; the struct itself is always emitted pub.
i32 stays as you ask it — a sample value outside i32 range is flagged, not widened for you.
The generator's usage preamble above the structs — off by default.
The generated serde structs appear here as you type.
Press ? for shortcuts
Do it offline:$ npx quicktype sample.json --lang rust --top-level Root --no-maps --no-enums --no-date-times --no-uuids --no-integer-strings --no-boolean-strings --no-leading-comments -o types.rs
How to use it
- Paste a payload or drop a .json file; the Rust pane redraws as you type.
- Add a sample per response variant — anything one variant omits is wrapped for you.
- Set the Rust side: type name, visibility, derives, integers, spacing, and where the key rename lives.
- Copy the source, download a .rs file, or share a permalink carrying every sample and setting.
Paste one payload or several and read back serde-ready Rust: one struct per object shape, the derive line on top, a #[serde(rename)] holding any key an identifier cannot spell. A key absent from any sample arrives as Option<T>, so missing and present stay apart. Numbers are weighed as raw tokens, so a nineteen-digit identifier keeps i64. Samples stay in this tab.
Examples
{"order_id": 4021, "customer_email": "sam@example.com", "shipped_at": null, "line_items": []}use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub order_id: i64,
pub customer_email: String,
pub shipped_at: Option<serde_json::Value>,
pub line_items: Vec<Option<serde_json::Value>>,
}
Snake-case keys already spell legal identifiers, so no rename appears. Two fields carry no evidence — shipped_at was null everywhere, line_items never held an element — so both degrade to serde_json::Value.
{"device_id": "kit-9", "firmware": "1.4.2"}
{"device_id": "kit-3"}use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub device_id: String,
pub firmware: Option<String>,
}
The second reading omits firmware, so it becomes Option<String> while device_id, present in both, stays String. Tick "skip_serializing_if on Option fields" (the on-screen label) and the field gains #[serde(skip_serializing_if = "Option::is_none")] above it, keeping None out of the re-serialized JSON rather than writing null.
{"snowflake_id": 9223372036854775807}use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub snowflake_id: i64,
}
That literal is exactly i64::MAX. Inference through JavaScript doubles would call it 9223372036854776000 and hand you f64; the scanner reads the digits before any float conversion, retypes the field i64 — serde_json parses it losslessly — and raises the warning quoted further down.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Option<T> is evidence, not decoration
A field is wrapped only when some sample lacked the key, so one payload yields no Option at all. Paste the error variant and the empty variant too, and optionality settles where it belongs.
No evidence means serde_json::Value
An always-null field becomes Option<serde_json::Value>, an always-empty array Vec<Option<serde_json::Value>>: no sample names an element type. Where such a value survives, the code needs serde_json in Cargo.toml beside serde.
i64 or f64 decides whether digits survive
Past 2^53 a double stops counting by ones, so digit runs inside i64's range are retyped i64 and flagged. A literal past i64::MAX has no integer home and stays f64 — carry it as a string if the digits matter. A token written 1e2 widens the other way: serde_json refuses an exponent for an integer field.
Forcing i32 is honoured, then warned
Pin the width to 32 bits and a sampled 3000000000 keeps i32 as asked, with a warning naming the literal that will fail to deserialize. The setting is yours; the consequence is stated.
Keys Rust cannot spell keep a byte-exact rename
type collides with a keyword and lands as root_type, 2fa cannot open an identifier and becomes the_2_fa, münchen loses its umlaut to mnchen — each with #[serde(rename)] holding the original bytes. Twenty renames are listed one by one; past that, a single line counts the rest.
Where the rename lives is your call
camelCase payloads get one rename_all = "camelCase" on the container by default. The per-field setting drops it and writes an explicit rename above each field it covered — same wire keys, easier to grep.
Not compiler-verified here
This page pins bytes, not builds: examples are replayed through the generator in continuous integration against frozen vectors, and neither rustc nor cargo runs there. Treat the source as a faithful draft to build inside your own crate.
Common errors
Sample 2: Unexpected "'" at line 1, column 2 — probable cause: single-quoted strings are JSON5, not JSON; use double quotes.- Merging starts only once every tab parses. The prefix names the failing tab, and shows only with several samples open; the caret frame marks the byte.
Code generation failed: Syntax error in input JSON Root: Maximum call stack size exceeded- Inference recurses, so absurd nesting exhausts the stack. Everything after the fixed prefix is the generator's own reason, relayed word for word; flatten a few levels and rerun.
Input is larger than the 10 MB limit for this tool.- Beyond the combined ceiling the run is refused whole rather than trimmed behind your back. Shape comes from variety, not bulk: keep a few representative records and point the command below at the full export.
integer 9223372036854775807 is beyond ±2^53, so JavaScript-based inference reads it as a rounded double. It is typed i64 here, which serde_json decodes exactly — check the value against your source.- An advisory, not a failure — the struct is generated and the field is already i64. It exists because whatever produced the sample may itself have rounded.
integer 3000000000 does not fit i32, and the fixed 32-bit width was requested, so the field keeps that type and this value fails to deserialize. Switch the integer option to i64 to decode it.- Raised only under the forced 32-bit setting, and only for a literal that really exceeds it. Widen the option or narrow the data; nothing here pretends the value fits.
The code uses serde_json::Value, so serde_json is a real build dependency, not just serde. Add serde_json to Cargo.toml next to serde with its derive feature.- The one dependency claim this page makes, fired only when an untyped value survives. Richer samples remove both the chip and the Value.
FAQ
Why did a key I always send still come back as Option<String>?
Because one open sample does not carry it. Optionality is read from the tabs, never guessed from the name, so a truncated payload flips a field. Close that tab, or accept the wrap.
Is serde enough in Cargo.toml, or do I need serde_json too?
serde with its derive feature covers everything except serde_json::Value. If that type shows up — from a null-only field or an empty collection — serde_json is a build dependency too, and a chip says so beneath the source.
Which integer width should I keep for a 64-bit identifier?
i64 whenever the digits fit, which the generated code already picks. Where they do not, a string field is the safer contract: f64 drops low digits, and anything wider needs a crate this page does not assume.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Generate now — the only start above 2 MB.
- ⌘/Ctrl + S
- Save the Rust source as a .rs file, not the page.
- ⌘/Ctrl + C
- With nothing selected, copies the output pane.
- ⌘/Ctrl + Shift + C
- Copy the whole output even mid-selection.
- t
- Flip the theme, unless a text field holds focus.
- ? or ⌘/Ctrl + /
- Open the shortcut overlay.
- Esc
- Dismiss the topmost dialog.