JSON to Rust

Generate serde-ready Rust structs from a JSON sample — snake_case fields with rename attributes where the spelling had to change, Option<T> where keys come and go or turn null, and every slot that had to become serde_json::Value reported with its reason. Free, no sign-up — runs locally in your browser.

Your work is auto-saved in this browser only, so a refresh won't lose it. Nothing is uploaded.

Why generate Rust structs from a sample?

serde makes JSON in Rust nearly free of boilerplate — nearly. Somebody still has to write the structs, and for a real API response that means a dozen nested types with field names the API chose in camelCase and your codebase needs in snake_case, plus a decision about every field that only sometimes shows up. The sample already contains all of that evidence. This converter extracts it mechanically: objects become structs, arrays become Vec, absence becomes Option, and the keys that had to be renamed get their #[serde(rename)] attributes written for you.

How to use this tool

  1. Paste the JSON. A real API response is ideal; include several array elements if you have them, because absence across elements is how Option fields are discovered.
  2. Name the root struct. Root is fine for a quick look; for code you will keep, use the name it will wear in your project.
  3. Press Generate Rust. The root struct comes first, the structs it references after it.
  4. Read the notes if any appear. They list every slot that became serde_json::Value, every keyword escape and every name collision — nothing was decided silently.
  5. Copy or download the result, then refine it: swap in chrono or uuid types, add #[serde(default)] where your API omits keys, and let the compiler take it from there.

The rules it follows

Numbers. All-whole numbers become i64; any fraction widens the slot to f64 — the same widening rule as this site's other generators, and for the same reason: a JSON 1.0 is the number 1 to every parser, so a field your API always sends as 2.0 reads as an integer here. Names. Structs are PascalCase after the key that holds them; fields are snake_case, with a rename attribute whenever that changes the spelling. Absence and null. Missing from some elements → Option<T>; sometimes null → Option<T>; null everywhere or genuinely mixed types → serde_json::Value, always noted. Merging. Identical shapes — same keys, same types — share one struct instead of being emitted twice.

Worked example

Given this sample (the second user has no nickname, and one key is camelCase):

{
  "teamName": "Platform",
  "users": [
    { "id": 1, "firstName": "Ada", "nickname": "ada" },
    { "id": 2, "firstName": "Grace" }
  ]
}

the converter emits:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    #[serde(rename = "teamName")]
    pub team_name: String,
    pub users: Vec<User>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: i64,
    #[serde(rename = "firstName")]
    pub first_name: String,
    pub nickname: Option<String>,
}

The renames are the API's camelCase talking; the Option is the second user talking. Both are the sample's facts, written down where Rust needs them.

Frequently asked questions

What do I need in my Cargo.toml to use the output?

serde with the derive feature, and serde_json — the structs use #[derive(Serialize, Deserialize)], and any slot typed serde_json::Value needs that crate. Add serde = { version = "1", features = ["derive"] } and serde_json = "1" and the generated code compiles as-is.

When does a field become Option<T>?

In exactly two situations, both visible in the sample: the key is missing from some elements of an array (the sample proves it comes and goes), or the key is present but sometimes null. A key that is null everywhere it appears cannot be an Option of anything — there is no evidence what it would be an Option of — so it becomes serde_json::Value and the notes say so.

Why i64 and not i32 or u64?

Because the sample can only prove 'whole number'. i64 is the width JSON tooling conventionally lands on (it is what serde_json itself uses for signed integers), and it will hold any integer a JSON document can carry short of the extremes. If a field is really a u32 port or an i32 counter, narrow it after generating — that is a modelling fact the sample cannot know.

Why did a field get a #[serde(rename)] attribute?

Rust fields are snake_case; JSON keys often are not. When snake_casing a key changes its spelling — firstName becomes first_name, 'last name' becomes last_name — the rename attribute pins the wire name so serde reads the key you actually have. The same applies when a key is a Rust keyword (type, match): the field is written r#type and the rename carries the original key.

Two of my keys turned into almost the same field name. What happened?

Keys like "first-name" and "first_name" both snake_case to first_name. The second becomes first_name_2 with a rename attribute preserving its key, and the notes flag the collision — it usually means the API itself has an ambiguity worth knowing about.

Does an Option<T> field have to be present when deserializing?

By serde's default rules, yes — Option means the value may be null, not that the key may be absent. If your API omits keys entirely, add #[serde(default)] to those fields; the generator does not add it for you because presence behaviour is a contract decision, and silently defaulting would hide it. The FAQs on the JSON to Go page explain the same trade-off in Go's terms.

Related tools