Skip to content
ZeroServer.tools

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.

Optional fields
or drop a file here

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 Option fields with skip_serializing_if.
  • Checking, from the notes, which numbers an f64 would change, and which serde_json reads otherwise without float_roundtrip.

Frequently Asked Questions

How do JSON types map to Rust?
Strings are String, booleans bool, objects a struct and arrays a Vec. A whole number is an i64, or a u64, i128 or u128 when the integers need one, and a fraction, an exponent or -0 makes an f64, as in 10.0 and 1e3. A number beyond an f64, such as 1e400, is a Box<RawValue>, which keeps it as written.
What does the code need to build?
Cargo.toml needs serde = { version = "1", features = ["derive"] } and serde_json = "1", and a Box<RawValue> needs serde_json's raw_value feature too, whose line a note gives. The file starts with use serde::{Deserialize, Serialize};, and each struct derives Debug, Clone, Serialize and Deserialize.
How are keys named?
Each field is its key in snake_case. A struct takes the rename_all rule, such as camelCase, 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, as r#type, but self, super and crate take 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 has skip_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 an Option with skip_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 f64 next 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 an f64.
  • Typing a key that some objects lack, or hold as null, as a plain String or i64: serde_json refuses an object without the key, with "missing field", and the null, with "invalid type: null". Here such a key is an Option.
  • Reading large IDs through serde_json::Value: it reads an integer beyond 64 bits as an f64, so 123456789012345678901234 becomes 123456789012345685803008. A typed field, an i128 here, keeps it.
  • Naming a field after a keyword: type, match and self do not build. Here type is r#type, which serde reads as type, and self is self_, 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.

Related Converters tools

Private & free — this tool runs entirely in your browser.

IndieKit — Ship your Next.js startup in days.affiliate