JSON to Rust Struct
Generate Rust structs that serde_json reads a JSON sample into and writes back. Every value is typed, and a key that some objects lack is an Option.
About JSON to Rust
The objects in the JSON become Rust structs that serde_json reads the JSON into and writes back, through serde's Serialize and Deserialize derives. Fields are snake_case, and a struct takes the rename_all rule that leaves the fewest keys to rename one by one, as #[serde(rename_all = "camelCase")] reads avatarUrl into avatar_url, with a rename for each key the rule does not give.
Every value at a place counts, not just the first. A whole number is an i64, or a u64, an i128 or a u128 when the integers need one, since serde_json reads each exactly. A number written with a fraction or an exponent, as 10.0 and 1e3 are, makes an f64, since serde_json refuses those for an integer type, and so does -0. A number beyond an f64, such as 1e400, makes a Box<serde_json::value::RawValue>, which keeps it as written.
The objects at one place, such as the items of a list, make one struct, with the keys in the order first seen. A key that is null in some objects, or that some objects lack, is an Option, which serde_json reads as None for either. A key that some objects lack also has skip_serializing_if = "Option::is_none", so it is left out again when the JSON is written.
A key that holds two kinds of value, such as a string and a number, is a serde_json::Value, and one that holds only null an Option<serde_json::Value>; objects that are always empty are a serde_json::Map<String, serde_json::Value>. A serde_json::Value refuses a number beyond an f64 too, so such a key with one is a Box<RawValue>, which needs serde_json's raw_value feature: a note gives the line for Cargo.toml. A note says why a field has each of those types, as one does for each number an f64 or a serde_json::Value changes, and each null that skip_serializing_if leaves out.
Without its float_roundtrip feature, which Cargo.toml must turn on, serde_json reads some numbers as an f64 next to the nearest one, such as 1.602176634e-19, and refuses a few that the nearest holds. A note names each field with such a number, and the line that turns the feature on.
A keyword is a raw identifier, as in r#type, which serde reads as type, but self, super and crate, which cannot be one, take an underscore and a rename. A name already taken gets a number, as in user_id_2. Structs are UpperCamelCase, the struct of a list's items is singular, as Order is for orders, and none is named as a type the code uses, such as String.
serde_json refuses a key that appears twice in an object a struct reads, and half of a surrogate pair, which a Rust string cannot hold. A note names each such place in the JSON, and a key with half of a surrogate pair has no field.
When the JSON is an array of objects, the root struct describes them, and a note gives the type that reads the whole array. Choose Every field to make every field an Option, for an API that can leave any key out. The code is a whole Rust file, laid out as rustfmt lays it out, which builds with no warning and which clippy passes. It needs serde = { version = "1", features = ["derive"] } and serde_json = "1" in Cargo.toml. Everything runs in your browser.
For Go, see JSON to Go Struct. For C#, try JSON to C# Class, and for TypeScript, JSON to TypeScript.
Built and maintained by Meet Shah · Last updated
What this tool is used for
- Generating serde structs from an API response, for a Rust service, a CLI or a test.
- Getting the field names and renames right for keys in camelCase, or with characters a Rust name cannot hold, such as spaces, dots and dashes.
- Writing the types for an endpoint with no OpenAPI schema, from a response copied out of the browser or a log, for a Rust client.
- Seeing which keys only some objects of a list carry, as the
Optionfields withskip_serializing_if. - Checking, from the notes, which numbers an
f64would change, and which serde_json reads otherwise without float_roundtrip.
Frequently Asked Questions
- How do JSON types map to Rust?
- Strings are
String, booleansbool, objects a struct and arrays aVec. A whole number is ani64, or au64,i128oru128when the integers need one, and a fraction, an exponent or -0 makes anf64, as in 10.0 and 1e3. A number beyond anf64, such as 1e400, is aBox<RawValue>, which keeps it as written. - What does the code need to build?
- Cargo.toml needs
serde = { version = "1", features = ["derive"] }andserde_json = "1", and aBox<RawValue>needs serde_json'sraw_valuefeature too, whose line a note gives. The file starts withuse serde::{Deserialize, Serialize};, and each struct derivesDebug,Clone,SerializeandDeserialize. - How are keys named?
- Each field is its key in snake_case. A struct takes the
rename_allrule, such ascamelCase, that leaves the fewest keys to rename, and a field whose key the rule does not give has#[serde(rename = "…")]. A keyword is a raw identifier, asr#type, butself,superandcratetake an underscore and a rename. - How are null and missing keys handled?
- A key that is null in some objects, or that some objects lack, is an
Option, which serde reads as None for either, without#[serde(default)]. A key some objects lack also hasskip_serializing_if = "Option::is_none", which leaves None out when the JSON is written, a null the JSON has for it too, as a note says. - What happens to unknown fields?
- serde_json skips a key that no field reads. Add
#[serde(deny_unknown_fields)]to a struct to refuse such a key instead, which is worth doing for a configuration file, where a mistyped key would otherwise be skipped. - Should every field be optional?
- A sample shows only some of the keys an API can send as null or leave out: here a key that is null in some objects, or that some objects lack, is an
Option. The API's contract says which others are. Every field makes every field anOptionwithskip_serializing_if, so a response can leave any key out. - Why do some notes name float_roundtrip?
- Without that feature, serde_json reads some numbers as the
f64next to the nearest one, such as 1.602176634e-19 as 1.6021766340000001e-19, and refuses a few that the nearest holds, such as 1.7976931348623158e308. A note names each field with such a number, and gives the line of Cargo.toml that turns the feature on.
Common errors and gotchas
- Generating from a sample where a decimal field happens to hold a whole number, such as a price of 12: the field is an
i64, and serde_json then refuses 12.5 for it. Use a sample with a fraction, or write 12.0, which gives anf64. - Typing a key that some objects lack, or hold as null, as a plain
Stringori64: serde_json refuses an object without the key, with "missing field", and the null, with "invalid type: null". Here such a key is anOption. - Reading large IDs through
serde_json::Value: it reads an integer beyond 64 bits as anf64, so 123456789012345678901234 becomes 123456789012345685803008. A typed field, ani128here, keeps it. - Naming a field after a keyword:
type,matchandselfdo not build. Heretypeisr#type, which serde reads as type, andselfisself_, with a rename. - Expecting a key that appears twice in an object to be read as JavaScript reads it: serde_json refuses it for a struct, with "duplicate field", where JSON.parse keeps the last value. A note names each place.
- Trusting the type a key has in one response: a number in one response can be a string in the next, which serde_json refuses for an
i64. Paste several responses as an array, and every value counts.