JSON to Rust: serde-ready structs
In Rust, serde and serde_json turn JSON into strongly typed structs, but you have to declare those structs first. This JSON to Rust generator writes them from a sample. Each struct derives Debug, Clone, Serialize and Deserialize, uses pub fields in snake_case, and adds #[serde(rename = "...")] wherever a JSON key cannot be used as a Rust identifier.
Example
{
"id": 42,
"title": "Write docs",
"done": false,
"priority": 2.5,
"tags": ["docs"],
"owner": { "name": "Ada", "email": null },
"due": "2026-10-01T09:00:00Z",
"subtasks": [
{ "title": "Outline", "estimate": 2 },
{ "title": "Draft" }
]
}
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: i64,
pub title: String,
pub done: bool,
pub priority: f64,
pub tags: Vec<String>,
pub owner: Owner,
pub due: String,
pub subtasks: Vec<Subtask>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Owner {
pub name: String,
pub email: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Subtask {
pub title: String,
pub estimate: Option<i64>,
}
The optional estimate is Option<i64>; serde treats a missing key as None for Option fields. email, which was only null, is Option<serde_json::Value> so any JSON value is accepted. The output above is shown with blank lines between fields removed.
Using the structs
// Cargo.toml
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
let task: Task = serde_json::from_str(&json)?;
println!("{} has {} subtasks", task.title, task.subtasks.len());
let back = serde_json::to_string_pretty(&task)?;
The generated file starts with a commented example; the extern crate lines in it date from Rust 2015 and are not needed in the 2018 or 2021 editions.
Reading serde errors
When the JSON does not match the structs, serde_json returns an error with the location, such as missing field `title` at line 1 column 42 or invalid type: string "3", expected i64. The first means a non-Option field was absent: make it Option<T> or add #[serde(default)]. The second means the API sends a different type than your sample showed; change the field type or use serde_json::Value while you investigate.
Naming and renames
- camelCase keys become snake_case fields with a
renameattribute; you can replace many renames with one#[serde(rename_all = "camelCase")]on the struct. - Keys that are Rust keywords, such as
type, get a prefixed field name (root_type) plus a rename. - Keys starting with a digit, such as
2fa, become names likethe_2_fa.
Type mapping and adjustments
| JSON | Rust |
|---|---|
| integer / decimal | i64 / f64 |
| string (including dates) | String |
| array / object | Vec<T> / a struct |
| optional or nullable | Option<T> |
| null only / mixed | serde_json::Value or an enum |
- Dates stay
String. For real date types, enable theserdefeature ofchronoortimeand change the field toDateTime<Utc>orOffsetDateTime. - Smaller integers: use
u32ori32if you know the range; serde reports an error if a value does not fit. - Serializing None: add
#[serde(skip_serializing_if = "Option::is_none")]to omit null fields in output. - Strictness: unknown keys are ignored by default; add
#[serde(deny_unknown_fields)]to reject them. - Tests: add
PartialEqto the derive list so you can compare decoded values withassert_eq!in unit tests.
Structs for XML input: XML to Rust. Validate your sample with the JSON Validator, and generate a language-neutral contract with JSON to JSON Schema. Everything runs in your browser.