To convert a Rust struct to a JSON string, derive Serialize on the type and call serde_json::to_string or serde_json::to_string_pretty on a value — the returned String is the JSON payload. The reverse direction — generating the struct from a sample of JSON so it can later be serialized back to text — is the piece most developers get stuck on, especially when keys come from an external API in camelCase, mixed types appear in the same field, or numbers exceed i64. The JSON to Rust Struct Converter addresses that bootstrap step by turning one strict JSON sample into reviewable Rust source with Serialize, Deserialize, and Debug derives, then leaving the actual serialization call to your project. Source generation runs locally, the page emits text only, and the destination Cargo.toml is expected to pin compatible serde and serde_json versions. From there the round-trip — value → JSON → value — becomes the test that decides whether the inferred types actually fit your production data.

What serializing a Rust struct to JSON actually does
Serialization is the process of walking a Rust value and writing each field as JSON text. Serde implements that walk through two halves: a data structure that knows how to serialize itself (the Serialize trait) and a data format that knows how to consume those calls (the serde_json crate). For the common struct-to-string case the pipeline is small: add #[derive(Serialize)] to the type, construct a value, and call serde_json::to_string(&value). The library handles field naming, container writing, and number formatting, returning a String. When you need pretty output for logging or debugging, serde_json::to_string_pretty adds two-space indentation without changing the data model.
The reverse direction — turning that JSON text back into a struct — uses #[derive(Deserialize)] and serde_json::from_str::<MyStruct>(&text). Both directions share the same generated impls on the same type, which is why a struct that has been hand-written can also be hand-tested by serializing it and parsing the result back. This is the same round-trip pattern you will use to verify any type produced from a JSON sample.
Why a JSON sample sits in front of the struct
Most production structs do not start as Rust code. They start as the JSON that some upstream service emits, and the Rust type is reverse-engineered from that sample. The challenge is that one sample is not an API contract: a field that is present today may be missing tomorrow, an integer that fits in i64 today may exceed it next quarter, and a string that looks like a number may really be a UUID. Hand-writing types against a single sample silently bakes those assumptions in. The JSON to Rust Struct Converter makes the assumptions explicit by emitting Option fields, serde_json::Value fallbacks, and serde rename attributes — every one of which is a placeholder the developer is meant to review.
The inferred source is not the final contract. It is a starting point that captures the evidence in the sample, so the only decision left is which fields to widen (u64, i128, Decimal, strings) and which to treat as enums, borrowed slices, or tagged unions. Once that review is done, serializing the struct to a JSON string is the same code path as before — the derive macros and to_string call do not care how the type was authored.
Generate reviewable types, then serialize them to a JSON string
- Paste a representative strict JSON sample into the page. An object, array, or primitive all work; for an object the root struct is named from the input you provide.
- Enter an ASCII root type name (for example, Config or Invoice). The generator builds nested type names from the object path, so no other naming input is required.
- Generate the source and read every decision the tool made: numeric widths, Option wrappers, serde_json::Value fallbacks for mixed evidence, serde renames for camelCase or hyphenated keys, and trailing underscores on Rust keywords.
- Copy the emitted source into a Rust project whose Cargo.toml already pins compatible serde with the derive feature and serde_json. The page does not add dependencies for you.
- Run cargo fmt on the copied file, then cargo build. Compile errors usually point to a missing import (use serde::{Serialize, Deserialize};) or to a crate version mismatch — both belong to the project, not the generator.
- Round-trip representative JSON with serde_json::from_str::<YourType>(&fixture) followed by serde_json::to_string(&value) and confirm both succeed. This is the only test that proves the generated type fits your data.
How the converter maps JSON evidence to Rust types
The mapping rules below are the ones the generator applies. They are deliberately conservative: any place the sample offers conflicting evidence, the tool downgrades to a type that signals uncertainty instead of inventing precision that one JSON document cannot prove.
| JSON evidence in the sample | Emitted Rust type |
|---|---|
| String value | String |
| Boolean value | bool |
| Integer within JavaScript safe range | i64 |
| Integer outside safe range or with decimal point | f64 |
| Object | Public struct with #[derive(Serialize, Deserialize, Debug)] |
| Array with a merged element type | Vec<ElementType> |
| Empty array | Vec<serde_json::Value> |
| null alone | Option<serde_json::Value> |
| null together with a non-null value of type T | Option<T> |
| Mixed incompatible values in the same field | serde_json::Value |
Two consequences matter for the eventual struct-to-JSON-string call. First, fields typed as Option<T> serialize as either the value or JSON null depending on whether the option is Some or None — serde handles that branch automatically. Second, fields that came back as serde_json::Value will serialize whatever JSON value you store in them, which preserves the original evidence but means the on-the-wire shape depends entirely on what you put in.
Serde renames, snake_case fields, and identifier collisions
JSON keys are not always valid Rust identifiers, and Rust field names are not always what you want on the wire. The generator resolves both halves of that mismatch. Camel case boundaries, spaces, punctuation, and hyphens become underscores; leading digits are prefixed; Rust keywords receive a trailing underscore; and normalized identifiers that collide with each other receive numeric suffixes. Whenever the Rust field name ends up different from the original JSON key, the generator emits a #[serde(rename = "originalKey")] attribute so deserialization still finds the right wire field and serialization writes the original key back out.
This is the difference between a generated type that quietly breaks and one that round-trips. Without the rename attribute, a JSON key like createdAt would deserialize into a Rust field called created_at only if you manually aliased it; with the attribute, serde reads createdAt from the wire and writes createdAt back when you call serde_json::to_string. The same attribute is what makes the eventual struct-to-JSON-string step produce output that matches the original API rather than the Rust-ified version.
Round-trip the generated struct against real fixtures
Inference from one sample cannot establish an API contract. Treat the generated source as a hypothesis and test it against fixtures that include success and failure cases. A field that is Option today may be required in tomorrow's response; a small integer may require u64, i128, Decimal, or a string when values grow; a string may represent a UUID, date, URL, enum variant, borrowed slice, or secret; and mixed objects may deserve a tagged enum. Run cargo test with at least one fixture per shape you expect, plus one that should fail to deserialize, and confirm the output of to_string is byte-identical (or at least semantically identical) to the input. The serde documentation on the derive feature covers the trait surface available for these refinements: the Serde using derive page and the broader Serde overview.
Identifier rules on the Rust side are documented at doc.rust-lang.org/reference/identifiers.html; reading them explains why the generator prefixes leading digits and appends an underscore to keywords — those are language-level constraints, not tool quirks.
Strict JSON, limits, and what the page will not do
The generator accepts strict JSON only. Comments, trailing commas, NaN, Infinity, BigInt syntax, undefined, and JavaScript object literals are rejected before inference runs. JSON numbers are parsed by JavaScript first, so unsafe integer spellings cannot be represented exactly and fall back away from i64 inference; precision-sensitive identifiers and decimals should be kept 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. The page does not compile Rust, install crates, execute generated code, or upload the sample, and it does not add crate paths, module visibility policy, lifetimes, generics, custom serializers, deny_unknown_fields, defaults, flattening, tagging, or validation.
| Limit | Value |
|---|---|
| Input characters | 500,000 |
| Values traversed | 50,000 |
| Nesting depth | 40 |
| Output characters | 1,000,000 |
Because the tool only emits source text, the destination project is responsible for the dependencies. Add serde with the derive feature and serde_json to Cargo.toml, paste the generated file into src/, run cargo fmt and cargo build, then write the round-trip test described above. That sequence — generate, format, compile, round-trip — is the full workflow from a JSON sample to a struct that you can confidently serialize to a JSON string.