New blogMaking an AI Agent You Can Trust With Money

★ Rust (serde) · from JSON

JSON to Rust (serde) Converter

Serde structs with idiomatic snake_case fields, #[serde(rename)] where the JSON key differs, Option for anything that can be missing or null, and raw identifiers for keys like type.

  • Runs in your browser
  • No signup

Strictness

Your JSON

Rust (serde)

3 types · 16 fields
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub id: i64,
    pub email: String, // email
    #[serde(rename = "display-name")]
    pub display_name: String,
    #[serde(rename = "createdAt")]
    pub created_at: String, // date-time
    pub plan: serde_json::Value,
    pub tags: Vec<String>,
    pub team: Team,
    pub invoices: Vec<Invoice>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invoice {
    pub number: String,
    pub amount: f64,
    pub paid: bool,
    #[serde(rename = "paidAt")]
    pub paid_at: Option<String>, // date-time
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub note: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Team {
    pub id: String, // uuid
    pub name: String,
    pub website: String, // url
}

Typing APIs by hand?

I set up end-to-end type safety (DB → API → UI) so this never happens again.

Fix my types
01

How the Rust (serde) mapping works

Each JSON value is read, merged with every other sample of the same key, and then mapped to the closest Rust (serde) type:

JSON to Rust (serde) mapping
In your JSONRust (serde)
stringString
integer / decimali64 / f64
true / falsebool
null alongside a valueOption<T>
key missing in some samplesOption<T> + #[serde(default)]
camelCase or invalid keysnake_case + #[serde(rename = ...)]
keyword key like typer#type
arrayVec<T>
mixed values or always nullserde_json::Value
02

Worked example

A user record with a nested team, a list of invoices (one missing a note, one with a null payment date) and an awkward display-name key:

{
  "id": 1042,
  "email": "ada@example.com",
  "display-name": "Ada",
  "createdAt": "2026-11-07T09:30:00Z",
  "plan": null,
  "tags": ["beta", "founder"],
  "team": {
    "id": "7f3c9a2e-1b4d-4c8e-9f2a-6d5e4c3b2a19",
    "name": "Analytical Engines",
    "website": "https://example.com"
  },
  "invoices": [
    { "number": "INV-001", "amount": 49.5, "paid": true, "paidAt": "2026-10-01T00:00:00Z" },
    { "number": "INV-002", "amount": 49, "paid": false, "paidAt": null, "note": "Card declined" }
  ]
}

Becomes this Rust (serde), generated by the same engine that runs the converter above (3 types, 16 fields):

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub id: i64,
    pub email: String, // email
    #[serde(rename = "display-name")]
    pub display_name: String,
    #[serde(rename = "createdAt")]
    pub created_at: String, // date-time
    pub plan: serde_json::Value,
    pub tags: Vec<String>,
    pub team: Team,
    pub invoices: Vec<Invoice>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invoice {
    pub number: String,
    pub amount: f64,
    pub paid: bool,
    #[serde(rename = "paidAt")]
    pub paid_at: Option<String>, // date-time
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub note: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Team {
    pub id: String, // uuid
    pub name: String,
    pub website: String, // url
}
03

Rust (serde) gotchas

Missing vs null

Option<T> accepts null, but a missing key also needs #[serde(default)]. The generated attributes include both where the sample shows it.

Timestamps stay strings

Parsing dates needs the chrono or time crate. Fields are String with a comment; swap in chrono::DateTime<Utc> with serde support if you use it.

serde_json::Value is an escape hatch

Mixed-type fields compile, but you lose type safety. Consider an #[serde(untagged)] enum once you know the shapes.

04

Questions developers actually ask

Which crates do I need?
serde with the derive feature, plus serde_json to parse. serde_json is also needed if any field uses serde_json::Value.
Why r#type instead of type_?
Raw identifiers let you use the natural name, and serde strips the r# when (de)serializing, so no rename is needed.
Can I deny unknown fields?
Add #[serde(deny_unknown_fields)] above a struct to make extra keys an error.
Is my JSON sent to a server?
No. Parsing and type generation run in your browser, so you can paste real API responses with tokens or customer data. Nothing leaves the tab.
How does it know a field is optional?
Paste an array of objects, or a response that contains one. Every element is read and merged: keys missing from some elements become optional, and keys that are sometimes null become nullable.
05

Other languages

06

More from the workbench

Fine print

Tools: on the house.
Engineer: by the hour.

A codebase that doesn't need this tool is the upgrade. I set up end-to-end type safety so your API, database and UI agree by construction.

I'm Himanshu, an AI and full-stack engineer. I build MVPs, AI agents and the growth plumbing around them for early-stage startups.