serde types from a JSON sample
In Rust, typed deserialization with serde is the norm: you declare structs, derive Deserialize, and serde_json::from_str does the rest. The tedious part is writing the structs for a nested API response with exactly the right field names and option types. This generator infers them from a sample payload and writes idiomatic code with the attributes serde needs.
Mapping rules
- Every object becomes a
pub structwithpubfields and#[derive(Debug, Clone, Serialize, Deserialize)]. - Whole numbers become
i64and other numbersf64; strings, booleans and arrays becomeString,boolandVec<T>. - Field names are snake_case. When the JSON uses another convention consistently, a single container attribute such as
#[serde(rename_all = "camelCase")]or"kebab-case"maps them; individual keys that do not fit get#[serde(rename = "...")]. - Rust keywords are avoided: a
typekey becomes a field likeroot_typewith a rename attribute. - Objects inside an array are merged. A field that is missing or
nullin part of the sample becomesOption<T>, which serde treats asNonewhen the key is absent. - A field that is only ever
nullbecomesOption<serde_json::Value>. - Arrays that mix types become an
#[serde(untagged)]enum with one variant per type, which serde tries in order. - A JSON array at the root becomes
pub type Root = Vec<RootElement>;.
A comment at the top lists the crates required — serde with the derive feature, and serde_json — and shows let data: Root = serde_json::from_str(&json)?;.
Root type name
Root type name (default Root) is the only option. It names the top-level struct or alias and is converted to PascalCase, so github repo becomes GithubRepo.
Adjusting the result
The derives include Debug and Clone for convenience; trim them if you care about compile times. Inference cannot know more than the sample shows: timestamps stay String (use chrono or time with their serde features if needed), small integer ranges are not narrowed to u8 or i32, and values above 2^53 trigger a warning — switch those to u64 or i128. If an API may add fields, serde ignores unknown keys by default, so the structs keep working. Everything is computed client-side; use the Rust formatter after editing.
With reqwest, the structs plug straight into response.json::<Root>().await?; with axum they work as Json<Root> extractors. If you only need part of a large response, delete the fields you do not use — serde skips keys that have no matching field, so a trimmed struct still deserializes.
Examples
GitHub-style repository object
Produces Repository and Owner structs; license becomes Option<serde_json::Value> and the type key is renamed to avoid the keyword.
{
"id": 1296269,
"full_name": "octocat/Hello-World",
"private": false,
"stargazers_count": 80,
"license": null,
"owner": { "login": "octocat", "id": 1, "type": "User" },
"topics": ["octocat", "api"]
}// Requires serde (with the "derive" feature) and serde_json:
// let data: Repository = serde_json::from_str(&json)?;
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Repository {
pub id: i64,
pub full_name: String,
pub private: bool,
pub stargazers_count: i64,
pub license: Option<serde_json::Value>,
pub owner: Owner,
pub topics: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Owner {
pub login: String,
pub id: i64,
#[serde(rename = "type")]
pub owner_type: String,
}
camelCase API with optional fields
A rename_all attribute maps the camelCase keys, couponCode becomes Option<String>, and the root is a Vec type alias.
[
{ "orderId": "ord_1", "totalAmount": 129.9, "isPaid": true },
{ "orderId": "ord_2", "totalAmount": 79.5, "isPaid": false, "couponCode": "SPRING10" }
]// Requires serde (with the "derive" feature) and serde_json:
// let data: Root = serde_json::from_str(&json)?;
use serde::{Serialize, Deserialize};
pub type Root = Vec<RootElement>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RootElement {
pub order_id: String,
pub total_amount: f64,
pub is_paid: bool,
pub coupon_code: Option<String>,
}
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
Unexpected content after the end of the JSON valueExplained | Several JSON values were pasted, for example lines from a JSON Lines file. | Convert the lines to an array with JSON Lines to JSON first. |
Missing comma after line 1Explained | A comma is missing between two properties in the sample. | Add the comma at the reported position. |
Some integers are larger than 2^53; they are typed as floating-point numbers. Use a 64-bit or big-integer type if you need exact values.Explained | A warning: the sample has integers too large to infer exactly, such as snowflake IDs. | Change the field to u64, i128 or String. |
Frequently asked questions
Which crates do I need?
serde with the derive feature, and serde_json for parsing. Add them with cargo add serde --features derive and cargo add serde_json.
How are optional fields represented?
As Option<T>. serde fills them with None when the key is missing or the value is null.
What is #[serde(untagged)] for?
It lets an enum match a JSON value by shape rather than by a tag, which is how a mixed array such as [1, “a”] is decoded.
Is my JSON uploaded?
No, generation is done locally in your browser.