The JSON to Rust Struct Converter infers Serde-ready Rust structs from a single strict JSON sample and emits Serialize, Deserialize, and Debug derives for every public type it generates, all without uploading the sample or installing crates. The tool takes one representative JSON document, parses it as strict JSON in the browser, and recursively walks each value to choose a Rust primitive, container, nested struct, or Option wrapper. Strings map to owned String, Booleans to bool, safe JSON integers to i64, and every other JSON number to f64. Arrays become Vec<T>, null becomes Option, and nested objects become named public structs with serde derives already attached. Because inference runs against one sample, the source it produces is reviewable rather than authoritative — every numeric width, optional field, and identifier rename needs a human eye before it ships as a contract. The rest of this article explains what the converter emits, the type-mapping rules it follows, and how to integrate the resulting source into a Serde-enabled Cargo project.

Rust Source the Converter Emits
The page emits Rust source text only. It never compiles the code, runs the code, or installs any crate into your project. Every public struct carries the standard derive trio — Serialize, Deserialize, and Debug — and uses serde attribute syntax so the generated imports assume a Serde-enabled destination. Nested objects are not flattened, not tagged, and not anonymous; each one becomes its own named public struct, and the path from the root type down to that nested object supplies a deterministic, collision-safe name.
The emitted source does not include any use statement, mod declaration, lifetime parameter, generic parameter, or custom serializer. There is no deny_unknown_fields, no default, no flattening, no tagging, and no validation hook. The output is the minimum surface needed for Serde to round-trip a payload that matches the sample you pasted, leaving every policy decision to the developer who reviews and adopts the code.
JSON-to-Rust Type Mapping the Generator Uses
The mapping below reflects the rules the generator applies to one sample. Where the evidence is ambiguous, the generator falls back to serde_json::Value rather than guessing an enum or coercing a type.
| JSON observation | Rust type the generator emits |
|---|---|
| String value | String |
| Boolean value | bool |
| Safe JSON integer | i64 |
| Any other JSON number | f64 |
| Integer and decimal mixed | f64 |
| Non-empty array | Vec<T> for merged element type T |
| Empty array | Vec<serde_json::Value> |
| null with no other observed type | Option<serde_json::Value> |
| null combined with type T | Option<T> |
| JSON object | Public struct with Serialize, Deserialize, Debug |
| Nested JSON object | Named nested public struct, type name derived from path |
| Object array field present in every object | Required (non-Option) field |
| Object array field missing from any object | Option<T> field |
| Incompatible mixed values | serde_json::Value |
Generate Rust Structs from JSON in Three Steps
The end-to-end workflow is intentionally short. You paste strict JSON, generate the source, then take it into a Cargo project that already wires up Serde.
- Paste strict JSON and enter an ASCII root type name. The sample can be a JSON object, an array, or a primitive; the generator needs the strict subset, so comments, trailing commas, NaN, Infinity, and JavaScript-style object literals are rejected. Pick a root name that is a valid Rust identifier — ASCII letters, digits, and underscores, not a reserved keyword — because that name becomes the public struct at the top of the emitted source.
- Generate and review the output. Scan the source for the five details that change shape: numeric widths (safe integer versus f64), Option wrappers, mixed-type fields that fell back to serde_json::Value, nested struct names, and #[serde(rename = "...")] attributes where the Rust field diverges from the JSON key. If any of those are wrong for your real data, edit the source by hand before pasting it into your project.
- Copy into a Serde-enabled Rust project. Add the result to a Cargo.toml that pins a serde version with derive support (and serde_json if you intend to parse JSON at the destination), run cargo fmt, run cargo build, and round-trip representative fixtures through serde_json::from_str and serde_json::to_string to confirm the sample and the struct agree.
Identifier Normalization and Serde Rename Attributes
JSON keys are not Rust identifiers. The generator translates each key into a collision-safe ASCII snake_case field and, whenever the wire key and the Rust field diverge, attaches a #[serde(rename = "...")] attribute so the on-the-wire name is preserved exactly. The rules are deterministic:
| JSON key shape | Rust field that gets emitted |
|---|---|
| Already snake_case | Same name; no rename attribute |
| camelCase, spaces, punctuation, or hyphens | Underscores at each boundary; rename preserves the original wire key |
| Leading digit (for example 2fa_enabled) | Underscore prefix; rename preserves the wire key |
| Rust keyword (for example type, ref) | Trailing underscore; rename restores the original key |
| Two keys that normalize to the same identifier | Numeric suffix on the second; rename restores the original key |
Nested struct names follow the same discipline: the path from the root type down to each nested object supplies a deterministic, collision-safe name. You can rename freely in your project, but the first version the generator emits already round-trips with the original payload.
Inputs the Generator Rejects and Hard Limits
Because the generator is a strict-JSON parser, several common JSON-shaped inputs fail up front. Comments, trailing commas, NaN, Infinity, BigInt-style suffixes, undefined, and JavaScript object literals are not accepted — verify your input with a JSON validity check before pasting if your sample came from a hand-edited file. JSON numbers are also parsed by the JavaScript engine before inference reaches the type chooser, so integer values that are too large to represent exactly as an i64 fall back to f64 rather than i64; if precision matters — for example, BigInt identifiers, currency minor units, or decimal scientific measurements — keep those fields as strings in the source contract.
The work is bounded at 500,000 input characters, 50,000 values, 40 nesting levels, and 1,000,000 output characters. Anything outside those limits returns an error rather than a partial output. Eight external fixtures lock the corner cases — String, i64, f64, bool, Vec, Option, null, and Rust-keyword rename syntax — and additional tests cover nested structs, normalized collisions, and invalid input rejection. There is no upload step: the sample never leaves the browser.
Integrating the Generated Source Into a Serde Project
The generated struct is only useful inside a project that wires up Serde. The minimum Cargo.toml entries a Serde-enabled project needs are a serde dependency with the derive feature enabled, and a serde_json dependency for JSON parsing at the destination. The generator adds neither, so pin compatible versions yourself — a typical pairing is serde version 1 with the derive feature, and serde_json version 1, both resolvable from crates.io.
Once the dependencies resolve, paste the generated source into a module, run cargo fmt, run cargo build, and confirm the type compiles. Then exercise the real wire format: deserialize a fixture with serde_json::from_str::<YourRoot>(&payload), serialize the result back with serde_json::to_string(&value), and diff the round-trip against the original sample. The round-trip is the moment of truth — if a field that the generator marked Option is actually required in production, or if a numeric value fell back to f64 when the contract expects u64, i128, or a decimal type, you will see it here. The official Serde derive guide and the Rust identifiers reference are the right places to confirm the syntax you are adopting.
Why the Output Is Reviewable, Not Authoritative
Inference from one sample cannot establish an API contract. A field present in the sample may be optional elsewhere; a small integer may need u64, i128, a decimal crate, or a string elsewhere; a string may represent a UUID, an ISO 8601 date, a URL, an enum variant, a borrowed slice, or a secret that should never reach logs; and mixed objects may deserve a tagged enum rather than a flat struct. Before adopting the generated source, review authoritative API documentation and run several success and failure fixtures — including malformed payloads, missing fields, and extra fields — through the same round-trip described above. The generator's job is to remove the typing chore; the contract stays yours.
If you want to start from a representative sample and produce the corresponding Serde-ready struct, the JSON to Rust Struct Converter runs the inference locally and hands you a copy-ready module. Format and compile the copied source, then round-trip representative JSON with serde_json before adopting it.
If you're weighing options, Minify JSON in VS Code Without Breaking Numbers covers this in detail.