JSON to Rust Struct: Generate Serde Types From a Sample
Generate Rust structs from JSON free. Get serde derives or Debug/Clone, snake_case fields with rename attributes, Vec and Option types, ready to paste.
To turn JSON into Rust, paste a representative sample into the JSON to Rust struct generator, name the root type, and choose whether you want serde derives. The tool infers every field, creates a struct for each nested object, maps JSON types to Rust types, and generates Option<T> for missing or null fields. The output is ready to paste into a .rs file.
Hand-writing these structs is mechanical work with a lot of small failure points: a missed Option, a field that needs #[serde(rename = "...")], an array of objects that should become a Vec. Generating from a real payload removes those mistakes and gives you a starting point you can refine.
What the generator produces
For the default serde mode, each struct gets:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub name: String,
pub age: i64,
#[serde(rename = "emailAddress")]
pub email_address: Option<String>,
}
The file starts with the imports and a comment describing the Cargo dependencies. Nested objects become separate named structs, arrays become Vec<T>, and fields that are null or absent in some samples become Option<T>.
With plain mode, the derive line drops to #[derive(Debug, Clone)] and serde imports and rename attributes are omitted. Use it when you are generating data-shape types for something other than serde.
How to generate Rust structs from JSON
- Open the JSON to Rust struct generator.
- Paste a JSON sample. A real API response works better than a minimal example.
- Set the root struct name. The default is
Root. - Choose the derives: serde (
Debug, Clone, Serialize, Deserialize) or plain (Debug, Clone). - Press Generate.
- Copy the code, add the listed dependencies to
Cargo.toml, and adjust names or types as needed.
The result also lists the struct count, which is a quick check that nested objects were detected as separate types rather than flattened.
Type mapping
| JSON value | Rust type |
|---|---|
| String | String |
| Integer | i64 |
| Decimal number | f64 |
| Boolean | bool |
| null | serde_json::Value (or Option<T> when merged with a real type) |
| Object | A generated struct |
| Array | Vec<T> |
| Empty or mixed array | Vec<serde_json::Value> |
Two details are worth knowing. Integers become i64, not the smallest unsigned type, because JSON has no integer width and a negative value in production would break a u32. Strings stay String; the generator does not guess that a date is chrono::DateTime or that a UUID is a uuid::Uuid. Those conversions are decisions for you, and guessing them from a sample is how you get a type that compiles but rejects real data.
Missing, null and optional fields
The generator merges all samples of the same object shape. That matters most for arrays: if one user object has an email key and another does not, email becomes optional. If a key is present but null in one sample, it becomes nullable. Both cases produce Option<T>.
This is a deliberately conservative choice. In Rust, a field that might be absent has to be an Option unless you add #[serde(default)] and a default value. Generating Option<T> compiles and deserialises correctly; generating a bare T does not.
For a field that is optional but should default to an empty string or zero, change the type after generating and add #[serde(default)]. The generator gives you a correct baseline, not a finished domain model.
Name conversion and serde attributes
Rust fields are snake_case. JSON keys often are not. The generator converts emailAddress to email_address and adds:
#[serde(rename = "emailAddress")]
when the names differ, so serialisation still uses the original key. Rust keywords are escaped as raw identifiers where possible, and a few reserved words that cannot be raw get a trailing underscore.
Nested object names are built from the path: a user object with an address object produces User and UserAddress, with numeric suffixes if two paths would otherwise collide. Arrays use a singularised field name, so users: [...] tends to produce a User type.
Root arrays and scalars
If the document is a top-level object, the root struct is straightforward. If it is an array, the generator emits a newtype:
pub struct Root(pub Vec<Item>);
A top-level scalar becomes a newtype around its type. That is valid Rust and easy to change if you would rather use a type alias.
Common problems and fixes
A field is Option<T> when it is always present. Some sample in the merged set was missing it. Use a more uniform sample, or change the type to T after generating.
A field is serde_json::Value when I expected a type. The value was null in every sample, or the array mixed incompatible types. Give the generator a sample with a real value, or replace Value with the type you know the API returns.
Dates and IDs are plain String. The generator does not infer semantic types. Add chrono, time or uuid and convert the fields yourself.
The output has more structs than I want. Nested objects are named separately by design. Inline them manually if a struct is only used once and the indirection is not worth it.
A JSON key became an unexpected field name. Acronyms and punctuation are normalised to valid Rust identifiers, with a serde rename preserving the original key. Rename the field if you prefer a different Rust name.
The JSON is rejected. The input must be strict JSON. Comments, trailing commas and single quotes are not accepted. Run it through the JSON formatter to find the syntax error.
The output stops at a deep level. Inference is capped at a depth of 24 and 64 structs, and anything beyond becomes serde_json::Value. That protects against pathological payloads; for a deeply nested document, generate the top levels and hand-write the rest.
Tips for better generated structs
- Use a real response, not a hand-written mock. Real payloads contain the nulls, optional fields and mixed arrays that shape the types.
- Include several array elements. The generator merges them, so two or three objects with different shapes produce a more accurate
Optionset than one. - Decide on serde early. Mixing serde and plain output halfway through a project creates inconsistent derives.
- Keep the generator output close to the wire format. Add domain methods and stricter types in a separate layer rather than editing the generated structs into something unrecognisable.
- Check integer widths.
i64is safe, but an API that guarantees unsigned values may deserveu64after review. - Generate Go or TypeScript too if you cross boundaries. The JSON to Go struct generator and JSON to Zod schema builder use the same inference approach for their ecosystems.
Dependencies
In serde mode, the generated header tells you what to add:
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_json is only needed when the output contains serde_json::Value or when you are parsing JSON directly. If your types are only used with another format, you can drop it and replace Value with a concrete type.
Frequently asked questions
Is the JSON to Rust generator free?
Yes. There is no account and no charge.
Does my JSON get uploaded?
No. The conversion runs in your browser, and the JSON is not sent to storage.
Does it generate serde derives?
By default, yes: Debug, Clone, Serialize, Deserialize, with the matching Cargo dependency noted in the output. You can switch to plain Debug, Clone if you do not use serde.
How are optional fields handled?
A key that is missing from some samples or null in some samples becomes Option<T>. The generator merges array elements of the same shape before deciding.
Does it detect dates, UUIDs or enums?
No. It maps JSON types only. Strings stay String, and you add chrono, uuid or an enum yourself.
What happens with a top-level array?
It generates a newtype struct around a Vec, such as pub struct Root(pub Vec<Item>);.
Bottom line
Generating Rust types from a real payload is faster and less error-prone than writing them by hand, as long as you review the result. Feed the tool a representative sample, keep serde enabled, and treat Option and serde_json::Value as honest signals about the data rather than defects. Start with the JSON to Rust struct generator, then tighten the types once the shape is clear.
Related posts
View allIBAN Validator: Check an IBAN Before You Send Money
One mistyped character can bounce a payment. Check the country, length and mod-97 checksum of an IBAN before you send it.
Base64 Encode and Decode: A Practical Guide
Encode text to Base64 or decode it free in your browser. UTF-8 safe, line breaks tolerated, size and character counts shown, with errors on invalid input.
Convert CSV to JSON: Keep Types and Headers Right
Convert CSV to JSON free with auto delimiter detection, typed numbers and booleans, duplicate-header handling and 2/4-space or tab output. Runs in browser.
Comments (0)
Be the first to share feedback on this post.