Skip to content

JSON to Rust

Generate Rust structs with serde derives from a JSON sample.

Input

What this tool does

Generate Rust structs with #[derive(Serialize, Deserialize)] from a JSON sample. Nullable fields become Option<T>, JSON keys that aren’t valid snake_case identifiers get a #[serde(rename = "…")] attribute, and nested objects become their own structs. Powered by quicktype, runs entirely in your browser.

How to use it

Paste JSON (or load the example) and read the Rust structs on the right. Each nested object becomes a separate struct with #[derive(Serialize, Deserialize)]; add serde = { version = "1", features = ["derive"] } to your Cargo.toml and you’re done.

Input: {"id":42,"name":"devsmiths","createdAt":"2024-03-11T08:24:00Z","stars":1280,"public":true,"contributors":[{"login":"ada","commits":51,"admin":true},{"login":"linus","commits":33,"admin":false}],"homepage":null}

Output (Rust):

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Root {
    pub id: i64,
    pub name: String,
    #[serde(rename = "createdAt")]
    pub created_at: String,
    pub stars: i64,
    pub public: bool,
    pub contributors: Vec<Contributor>,
    pub homepage: Option<serde_json::Value>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Contributor {
    pub login: String,
    pub commits: i64,
    pub admin: bool,
}

Limits and edge cases

  • Default derives are Debug, Serialize, Deserialize. Add Clone, PartialEq, or Hash by hand for the structs that need them — the generator is conservative because deriving everything blows up compile times.
  • JSON keys that aren’t valid snake_case Rust identifiers get a #[serde(rename = "…")] attribute preserving the original. For an entire struct, prefer #[serde(rename_all = "camelCase")] on the struct — the generator emits per-field renames; consolidate by hand if every key in the struct shares a casing convention.
  • Whole numbers are i64 by default. If you have positive IDs that should never be negative, switch to u64 by hand — serde tolerates either, but Rust’s type system gives you the invariant for free.
  • Nullable fields become Option<T>. Use #[serde(default)] if you also want missing keys to deserialise as None (the generator emits this automatically when all optional is on).
  • Fields where the JSON sample is null become Option<serde_json::Value> rather than a specific type — the generator has no information about what the real type should be. Tighten by hand once you know.
  • Date strings stay as String. Pull in the chrono or time crate and replace the field types if you need typed dates; #[serde(with = "chrono::serde::ts_seconds")] is the usual incantation.

Frequently asked questions

Why only Debug + Serialize + Deserialize — where's Clone?
Conservative defaults. Deriving Clone, PartialEq, Hash, Eq on every type bloats compile times (each derive runs proc-macros) and most JSON DTOs don't need all of them. Add `#[derive(Clone, PartialEq)]` to specific structs by hand. For the common case where every type needs Clone, do a one-line sed: `s/Debug, Serialize, Deserialize/Clone, Debug, PartialEq, Serialize, Deserialize/`.
Why does the generator emit per-field serde renames instead of #[serde(rename_all = "camelCase")]?
Because not every struct in a real payload has every field in the same case (mixing camelCase with snake_case is common in legacy JSON). Per-field renames are always safe; rename_all is only safe when every key matches the convention. Consolidate by hand once you confirm a struct's keys are uniform — `rename_all` on the struct is shorter and faster to compile than dozens of per-field attributes.
How do I make missing keys deserialize as None instead of erroring?
Two ways. Field-level: add `#[serde(default)]` next to the field — serde uses `Option::default()` (which is `None`) for missing keys. Struct-level: `#[serde(default)]` on the struct uses `Default::default()` for every field, which requires implementing or deriving `Default`. The 'all optional' toggle in the generator emits the field-level form on every Option field.
Why is my null field typed as Option<serde_json::Value>?
Because the sample showed only null for that field and the generator has nothing to base a real type on. `serde_json::Value` is an escape hatch — it deserializes anything. Tighten by hand: if you know the field is sometimes a string, change to `Option<String>`. If it's a polymorphic union, use `#[serde(untagged)]` on an enum.
Can I get chrono::DateTime for ISO 8601 date strings?
Not automatically. Change `created_at: String` to `created_at: DateTime<Utc>` and add `#[serde(with = "chrono::serde::ts_seconds")]` or `chrono::serde::ts_rfc3339` depending on format. For the `time` crate (the stdlib-friendlier alternative), it's `OffsetDateTime` with the `serde-well-known` feature.
How big is the wire-format mismatch between i64 / u64 / f64?
JSON has one number type — JavaScript double-precision float. Values up to 2^53 are exact integers; beyond that you lose precision before the data even reaches serde. For 64-bit unsigned IDs (Discord snowflakes, Twitter IDs, etc.), the producer should serialize them as strings; you then use `#[serde(deserialize_with = ...)]` with a string-to-u64 helper. The generator can't detect this from a sample.

Content reviewed by