100% local — your data never leaves your browser

JSON to Rust — serde Structs Ready to Derive

Turn a JSON sample into Rust structs with serde derives. Names become snake_case and the original key rides along, so from_str works first try.

Instant Private Zero cookies

JSON input

Rust output

What this tool does

A JSON sample becomes Rust structs with serde derives.

{ "id": 1, "firstName": "Ada", "address": { "city": "Paris" } }
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Root {
    pub id: i64,
    #[serde(rename = "firstName")]
    pub first_name: String,
    pub address: Address,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Address {
    pub city: String,
}

Rust field names are snake_case, so firstName becomes first_name — and #[serde(rename)] keeps the key, because a struct that no longer reads its own JSON would be worse than an unidiomatic name. The rename appears only where the two differ.

If you only ever read the payload, the option that drops Serialize gives you #[derive(Debug, Deserialize)] and one less trait to satisfy.

Keywords, raw or suffixed

{ "type": "user", "self": "/users/1" }
    pub r#type: String,
    #[serde(rename = "self")]
    pub self_: String,

r#type is a raw identifier: serde already serialises it as type, so no attribute is needed. self is one of the few words Rust does not allow even as a raw identifier — self, Self, crate, super — so it takes an underscore and a rename instead.

What comes from serde_json

null, unknown values, empty arrays and mixed arrays all end up as serde_json::Value:

    pub nickname: Option<serde_json::Value>,
    pub tags: Vec<serde_json::Value>,

so serde_json belongs in your Cargo.toml alongside serde with its derive feature. The path is written in full rather than imported, which keeps the output valid whatever your module layout.

What one sample cannot say

Only null produces an Option. Everything else is required, because the document had a value — and “sometimes missing” is not something one document can show. Wrapping a field in Option<T> is one edit; #[serde(default)] is the other, when absence should mean the default rather than an error.

Two keys that snake_case to the same name — fooBar and foo_bar — are suffixed rather than merged, and each keeps its own rename.

Private by design

Everything runs locally in your browser with JavaScript. Your data is never uploaded, which makes the tool safe for sensitive content, and it keeps working offline.

Frequently asked questions

What do I need in Cargo.toml?
`serde` with the `derive` feature, and `serde_json` as soon as a field holds an unknown value: `Option<serde_json::Value>` and `Vec<serde_json::Value>` come from that crate. The output uses the full path rather than importing it, so nothing breaks if you alias it differently.
How is a key named `type` or `self` handled?
`type` becomes the raw identifier `r#type`, which serde serialises as `type` — no attribute needed. `self` cannot be a raw identifier at all, so it becomes `self_` with `#[serde(rename = "self")]` to keep the key. Both cases are in the output, spelled the way the compiler accepts.
Why is my twenty-digit identifier an `i64`?
Because that is Rust\u2019s natural integer for JSON, and it holds values up to about 9.2 × 10¹⁸. A longer one does not fit: serde_json reports an error at deserialization rather than truncating. If the field is a snowflake ID, `u64` buys you one more bit and `String` is what most APIs actually expect.

Related converters