JSON to Rust Structs

Paste JSON to generate Rust structs with serde derive macros. 100% client-side.

Last reviewed: April 2026

New to this tool? Click here for instructions

JSON Input
Rust Output
Paste JSON above to generate Rust structs.

Paste any JSON payload and instantly generate idiomatic Rust structs with serde derive macros — Option<T> for nullable fields, Vec<T> for arrays, snake_case field renaming, and nested struct generation, all client-side in your browser.

What This Tool Does

This converter takes a JSON object and produces ready-to-compile Rust struct definitions decorated with #[derive(Serialize, Deserialize, Debug, Clone)]. Every JSON primitive maps to its closest Rust equivalent: "string" becomes String, integer numerics become i64 (or u64 when non-negative), floats become f64, booleans become bool, null becomes Option<T>, JSON arrays become Vec<T>, and nested objects become their own named structs linked from the parent struct field.

Field names follow Rust idiom — camelCase JSON keys are converted to snake_case Rust identifiers, with a #[serde(rename = "camelCase")] attribute added automatically so the wire format still matches the original JSON. Reserved Rust keywords like type, match, or ref are escaped to r#type, r#match, r#ref with the same serde rename safeguard. Three output modes are available: pure Serde derive, Serde derive plus Option wrapping on every field (defensive parsing for evolving APIs), and a Public Fields mode that adds pub visibility to every struct member.

The entire conversion runs in JavaScript inside your browser. No JSON sample, struct name, or generated code ever leaves the page — useful when the JSON contains internal API contracts, customer data, or anything else you would not want to paste into a remote service.

How to Use It

The interface is built for a single fast loop: paste, read, copy. The example below walks through every control on the page.

Paste Your JSON Sample

Drop a representative JSON object into the left input pane. The parser accepts any valid JSON — a single object, an array of objects (the first element drives type inference for the array), or even a primitive value (which generates a type alias). Whitespace is irrelevant; minified or pretty-printed both work. The textarea supports Tab for indentation rather than focus navigation, so you can fix formatting in place without leaving the field.

Pick a Generation Mode

The three option chips above the input control which derive style is emitted. Serde is the default — it produces minimal idiomatic structs suitable for stable, fully-populated API responses. Serde + Option wraps every field in Option<T>, a defensive posture useful when the upstream API is evolving or when you cannot guarantee every field is present on every response. Public Fields emits pub on every struct member, which is necessary when the structs need to be constructed by code outside the defining module.

Read the Generated Rust Code

The right pane updates live as you type (with a 150 ms debounce) and prints the generated Rust struct list with use serde::{Serialize, Deserialize}; at the top. Nested JSON objects produce sibling struct definitions named after their parent field in PascalCase — a JSON field profile: {...} becomes a Profile struct referenced from the parent. Arrays of objects produce structs suffixed with Item: tags: [{...}] generates a TagsItem struct.

Copy or Download Output

The Copy button puts the generated code on your clipboard for pasting straight into src/models.rs or wherever your DTO module lives. Download .rs saves the same text to a file named output.rs. The status bar below the panes reports how many structs and fields were generated, or — if your JSON failed to parse — exactly which token tripped the JSON parser, so syntax errors are easy to pinpoint.

Worked Example: User Profile with Nested Object

To see the converter handle the full range of common JSON shapes — integers, strings, arrays of primitives, nested objects, and explicit nulls — work through this concrete example. Paste the following JSON into the input pane:

{
  "id": 42,
  "email": "a@b.com",
  "tags": ["admin"],
  "profile": {
    "name": "X",
    "age": 30,
    "verified": null
  }
}

With the default Serde mode selected, the converter produces:

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct User {
    pub id: u64,
    pub email: String,
    pub tags: Vec<String>,
    pub profile: Profile,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Profile {
    pub name: String,
    pub age: u8,
    pub verified: Option<bool>,
}

Five inference decisions are visible in that output. id: 42 is a small non-negative integer so it maps to u64 rather than i64 — non-negative numerics get unsigned types in the Rust output, which is more honest about the value's domain. email is a string so it becomes String (not &str, for reasons covered in the FAQ below). tags is a JSON array of strings, so it becomes Vec<String>; if it had been empty the converter would have fallen back to Vec<serde_json::Value> because the element type is unknown. profile is an object, so it generates a sibling Profile struct referenced from the parent. And verified: null in the JSON sample becomes Option<bool> — the converter inspects nullable positions even when only null appears in the sample, defaulting to Option<bool> when context suggests boolean-shaped data and Option<serde_json::Value> when no shape can be inferred.

Type-mapping reference (inline SVG): the figure below shows the exact mapping rules the converter applies for every JSON construct it recognizes.

JSON to Rust Type Mapping Reference Visual reference table: each JSON value shape on the left, the corresponding Rust type the converter emits on the right, with annotations for edge cases and serde attribute notes. JSON to Rust Type Mapping Inference rules applied by the converter for each JSON value shape JSON Sample Rust Type Emitted Notes & Serde Attributes "hello" String Owned; not &str (deserialization needs ownership) 42 u64 Unsigned for non-negative ints; i64 if negative seen -15 i64 Sign forces signed integer type 3.14 f64 Any decimal becomes 64-bit float true / false bool Direct mapping; trivially Copy null Option<T> T inferred from context, falls back to Value ["a","b"] Vec<String> Homogeneous arrays infer element type [1,"x",true] Vec<serde_json::Value> Heterogeneous: fallback to Value or enum {"name":"X"} Profile (sibling struct) Nested object generates its own struct "camelKey" camel_key: String #[serde(rename = "camelKey")] auto-added "type" (key) r#type: String Reserved keyword escape; rename attribute added
Reference: every JSON construct the converter recognizes, the Rust type it emits, and the serde attributes it adds automatically. Empty arrays, nested objects, and reserved keywords all have explicit fallback rules.

Switching to Serde + Option mode on the same JSON wraps every field — even ones that were present and non-null in the sample — so the struct becomes pub id: Option<u64>, pub email: Option<String>, pub tags: Option<Vec<String>>, pub profile: Option<Profile>. That extra defensiveness costs you nothing at the type level (Option's None variant occupies the same slot a present value would) but forces every consumer to acknowledge the possibility of absence at compile time.

Common Use Cases

JSON-to-Rust generation pays off most when you are crossing a boundary between Rust code and an external system that already speaks JSON. The following workflows are the ones the converter sees most heavily.

API Client Scaffolding

When you need to consume a third-party REST or GraphQL API from Rust — Stripe, GitHub, OpenAI, an internal microservice — paste a representative response from the API documentation or a curl probe directly into the converter. The generated struct becomes the response type for your reqwest::Client call. Add a corresponding request struct, derive Serialize on it, and the client is mostly written. For OpenAPI-described APIs the OpenAPI to Rust generator handles the same job at full-API scope, but for one-off endpoints the paste-a-sample workflow here is faster.

ETL Pipelines with serde_json

Batch ETL jobs that ingest JSON from S3, Kafka, or a vendor SFTP drop benefit from typed deserialization rather than walking serde_json::Value trees. Run a representative record through the converter, drop the struct into your job binary, and switch from let v: Value = serde_json::from_str(line)? to let rec: InvoiceLine = serde_json::from_str(line)?. The compiler then catches every field-access typo at build time rather than during the 3am pager call.

Config-File Schemas for clap / structopt

CLI tools written with clap or its earlier sibling structopt often accept a JSON or TOML config file. Defining the config schema as a serde-derived struct gives you free validation: missing required fields surface as deserialization errors before your CLI starts work, and the struct itself doubles as documentation for users writing their own config. The converter is a fast way to scaffold the config struct from a sample config.example.json committed to the repo.

Deserializing OpenTelemetry / Trace Data

Observability pipelines emit large volumes of structured JSON — OpenTelemetry spans, Sentry events, structured logs. A Rust-based collector or transformer that deserializes these payloads into typed structs runs faster and uses less memory than one that operates on Value trees, because it skips the dynamic dispatch overhead and benefits from compiler-driven layout optimization. Paste a sample span into the converter, refine the generated struct to use chrono::DateTime<Utc> for timestamp fields, and you have your collector's input type.

Axum and Actix-Web Request Handlers

Web framework handlers in Axum and Actix-Web accept request bodies through extractors that deserialize JSON into a struct — Json<T> in Axum, web::Json<T> in Actix. Paste a sample request body into the converter to generate the T. Add validation rules with the validator crate, derive Validate on the same struct, and the handler signature reads as a contract: deserialize, validate, then run business logic on a fully-typed value.

Edge Cases and Limitations

JSON's looseness and Rust's strictness collide in a handful of recurring patterns. Knowing them in advance saves a debugging round trip.

Heterogeneous arrays do not survive type inference. Rust's Vec<T> requires a single concrete T; an array like [1, "two", true, {"obj":1}] has no single Rust type. The converter detects this case and falls back to Vec<serde_json::Value> so the code still compiles. The idiomatic refactor is to define an enum with #[serde(untagged)] variants for each shape the array can contain, but enum inference from raw samples is not reliable, so the converter defers that decision to you.

Nullable fields need Option, not a sentinel value. A field that may be missing from the JSON entirely (key not present in the object) or that may carry a JSON null must be wrapped in Option<T>. Without Option, a missing key triggers a serde deserialization error and a null value triggers a type-mismatch error. The Serde + Option output mode wraps every field defensively; the default Serde mode only wraps fields that the converter saw as null in the sample.

Empty arrays force fallback typing. "tags": [] in your sample gives the converter no element to inspect, so it emits Vec<serde_json::Value> rather than guessing. Provide at least one representative element in the sample to get a concrete type. If you cannot, leave the fallback and refactor the type after generation when you have a real element to copy from.

snake_case rename attributes are not optional. Rust's standard library and major crate ecosystem all use snake_case for field and function names. The converter renames camelCase or PascalCase JSON keys to snake_case Rust identifiers and adds #[serde(rename = "originalKey")] so wire compatibility is preserved. If you delete the rename attribute, serialization will emit snake_case on the wire — breaking any consumer that expects the original casing. Either keep the rename or apply #[serde(rename_all = "camelCase")] at the struct level to handle every field uniformly.

Large integer values exceed i64. Integers larger than 9,223,372,036,854,775,807 (the i64::MAX) cannot deserialize into i64 without panicking. Common offenders are JavaScript-sourced timestamps in microseconds, Twitter snowflake IDs sent as raw numbers (rather than strings), and certain blockchain values. Three options: switch the field to u64 if you can guarantee non-negative, switch to the serde_with crate's DisplayFromStr helper to deserialize a numeric string into a large integer type, or refactor the upstream JSON producer to emit large integers as strings.

Date and timestamp parsing requires chrono. JSON has no native date type — dates arrive as ISO-8601 strings, Unix epoch numbers, or vendor-specific formats. The converter emits String for any string-shaped field by default; swap to chrono::DateTime<Utc> after generation when you confirm the format. For RFC 3339 ISO-8601, the chrono serde feature handles parsing automatically. For Unix epoch integers, use the serde_with::TimestampSeconds helper. For custom formats, write a tiny custom Deserialize impl using chrono::NaiveDateTime::parse_from_str.

Lifetimes for borrowed strings are rare in DTOs. Beginning Rust developers sometimes try to use &str in their generated structs for "performance." This only works when the deserialized buffer outlives the struct (the #[serde(borrow)] case), and adds a lifetime parameter to the struct that propagates through every consumer. For ordinary REST and ETL code, String is correct, ergonomic, and almost always fast enough — the allocation cost is dwarfed by network and disk I/O on the same code path.

Behind the Scenes: How Serde Generates Code

The Serde Framework (Tryzelaar, 2015)

Serde — the name is a portmanteau of "serialize" and "deserialize" — was created in 2015 by Erick Tryzelaar with significant contributions from David Tolnay, who remains its primary maintainer. Before Serde, Rust JSON handling went through rustc_serialize (a slow, runtime-reflective approach inherited from early Rust) or hand-written deserialization code. Serde's design pulled the ideas from Haskell's Aeson and Scala's Play JSON forward: define data structures once with derive macros, get serialization and deserialization for every format the ecosystem supports — JSON, YAML, TOML, MessagePack, CBOR, BSON, RON, Postcard — without writing per-format code.

Zero-Copy Where Possible

Serde is designed to deserialize without unnecessary allocations whenever the data model permits. A &str field with #[serde(borrow)] deserializes by pointing directly into the original input buffer rather than copying bytes into a fresh allocation. A Cow<str> field does the same when the input does not contain escape sequences, and falls back to an owned allocation only when escapes force a copy. For struct deserialization from JSON, Serde reads the input as a streaming token sequence — values are pulled into struct fields one at a time without ever materializing the full Value tree in memory. This is what makes Serde-based ETL pipelines fast: they pay the parse cost once, in a streaming fashion, with no intermediate dynamic representation.

#[derive] Macro Expansion

When you write #[derive(Serialize, Deserialize)], Rust's procedural-macro system invokes Serde's derive crate at compile time. The macro receives your struct's syntax tree and synthesizes two trait implementations: one for Serialize (a method that walks the struct's fields and emits them through a Serializer), and one for Deserialize (a Visitor-pattern implementation that consumes input tokens and reconstructs the struct). The generated code is monomorphic — there is no runtime reflection, no virtual dispatch — which is why Serde matches or beats hand-written deserialization in benchmarks. You can inspect the generated code with cargo expand if you are curious; the output is large but human-readable.

Alternative Codecs in the Serde Ecosystem

Once a struct derives Serde traits, it can be encoded in any format with a Serde codec. JSON via serde_json is the default, but the same struct works with Postcard for compact embedded systems messaging (no dependencies on std, designed for microcontrollers), MessagePack via rmp-serde for binary efficiency, CBOR via ciborium for IoT use, TOML via toml-rs for config files, and YAML via serde_yaml. The same struct definition serves as the schema across all of them — generate from JSON here and re-use the struct against MessagePack in production for wire savings.

Comparison: This Tool vs Alternatives

Several other tools and crates exist for generating Rust code from JSON or schema descriptions. Each has a different scope and a different trade-off profile.

quicktype.io is the most prominent web-based JSON-to-code generator, supporting Rust as one of dozens of target languages. Its strengths are multi-language coverage and a rich set of formatting options. Its trade-offs versus the in-browser tool here: quicktype runs in a remote service unless you self-host it, the Rust output sometimes uses Option more aggressively than necessary, and the tool is geared toward general code generation rather than the specific Serde-idiomatic patterns that production Rust APIs use. For one-off snippets and quick scaffolding, the ThisDevTool converter is more direct.

schemafy_lib is a Rust crate that generates structs from JSON Schema documents (not raw JSON samples). If your upstream data source publishes a JSON Schema — many enterprise APIs and OpenAPI-described services do — schemafy lets you regenerate types from schema during your build.rs script, keeping the Rust types in sync with the published schema automatically. The downside is that JSON Schema is a separate document type that not every API provides, and writing one by hand from a raw JSON sample is more work than the paste-into-this-tool path.

typify is a newer Rust crate from the Oxide Computer team that generates Rust types from JSON Schema with significantly better handling of advanced schema features (oneOf, anyOf, allOf compositions, recursive references) than schemafy provides. Typify is the right answer when your project already has a comprehensive JSON Schema and you want the type generation tightly coupled to the schema version. It is overkill when you are working from a raw JSON sample and just need a struct.

The ThisDevTool converter sits at the lightest weight point on this spectrum: paste a sample, copy the result. No build script, no schema file, no remote service. For the common case of "I have a JSON payload, I want a struct," it gets you to a compiling Rust file in well under a minute, and the result is idiomatic enough to ship after minor edits.

Frequently Asked Questions

Generated DTO structs should use String, not &str. Serde deserializes JSON text into owned String values by default because the parsed buffer is consumed during deserialization; a borrowed &str would dangle once the input buffer is freed. Borrowed string fields require an explicit lifetime parameter on the struct plus #[serde(borrow)], and they only work when you can guarantee the input buffer outlives the struct. For most API client and ETL code, owned String is the correct, ergonomic default.
Rust's type system rejects heterogeneous arrays at compile time because Vec<T> must contain elements of a single concrete type T. When your JSON contains mixed-type arrays — for example [1, "two", true] — you have three options: define a tagged enum with #[serde(untagged)] variants for each shape, fall back to Vec<serde_json::Value> for fully dynamic handling, or refactor the JSON source to use a discriminated object format. The converter defaults to Vec<serde_json::Value> when it detects element-type mismatches.
The #[serde(default)] attribute tells serde to populate a missing JSON field with the type's Default::default() value instead of returning a deserialization error. Applied to a field of type String it produces an empty string; applied to Vec<T> it produces an empty vector; applied to Option<T> it produces None. This is the standard way to evolve an API schema without breaking existing client code — clients on older versions silently get default values for fields the server has not yet started sending.
Option<T> encodes the absence-of-value case directly into Rust's type system: a field that may be missing or JSON null becomes Option<T>, and the compiler forces every consumer to handle the None branch via match, if let, or unwrap_or. Without Option, a missing or null JSON field would either trigger a deserialization panic or — worse — silently fall through to a default that the caller never noticed. Option turns runtime ambiguity into compile-time clarity, which is the whole point of Rust's null story.
JSON has no native date type, so dates arrive as strings. To deserialize them into a typed DateTime, add the chrono crate (with the serde feature) and annotate the field as chrono::DateTime<Utc>. For RFC 3339 ISO-8601 strings serde will parse automatically; for custom formats use the serde_with crate's TimestampSeconds or DisplayFromStr helpers, or write a custom serde::Deserialize impl. The converter outputs String for date-shaped fields by default — swap to DateTime<Utc> after generation when you confirm the input format.
Yes — for DTOs that move through async pipelines, get cached, or get split across thread boundaries, deriving Clone is almost always worth the small cost. Clone is shallow and explicit in Rust (you must call .clone()), so derived structs do not silently pay for copies the way they would in C# or Java. Skip Clone only for very large nested structs where you have profiled and confirmed the allocation cost matters, in which case wrapping the struct in Arc<T> at the call site is usually the better fix.
Rust reserves identifiers like type, match, ref, mod, fn, use, and impl. When JSON uses these as keys, the converter emits the field name with a trailing or escaped form (such as type_ or r#type) and adds a #[serde(rename = "type")] attribute so the JSON key still maps correctly during serialization. The r#keyword raw-identifier syntax is the more idiomatic modern choice; the trailing-underscore form is older but still common in serde-generated code.
Yes, when your JSON contains a discriminator field (commonly named type, kind, or variant), the idiomatic Rust representation is an enum with #[serde(tag = "type")] for internally-tagged variants or #[serde(untagged)] for variants distinguished only by shape. The converter does not auto-detect this pattern from raw JSON samples — generation defaults to a flat struct with an Option<String> tag field. Refactor manually after generation when you recognize the union pattern in your schema.

Quick reference

JSON to Rust Structs Quick Reference
JSON Type Rust Type Attributes Notes
{ "name": "Alice", "age": 30 } struct Person { name: String, age: u8 } [serde(rename_all = "snake_case")] Maps object to struct with field renaming
[{"id": 1}, {"id": 2}] Vec<struct Item { id: i32 }> [serde(deny_unknown_fields)] Handles array of nested objects
{ "is_active": true } bool [serde(default)] Preserves boolean values
{ "timestamp": "2023-10-01T12:00:00Z" } chrono::DateTime<Utc> [serde(with = "serde_datetime")] Requires custom deserializer
{ "role": "admin" } enum Role { Admin, User } [serde(rename_all = "lowercase")] Maps string to enum variant
{ "data": { "value": 42 } } struct Nested { data: struct Inner { value: i32 } } [serde(flatten)] Handles deeply nested objects