Skip to content
Terug naar blog
Tutorials

JSON naar Rust-struct: serde-structs genereren (gids 2026)

Zet JSON correct om naar Rust-structs: numerieke types (i64/u64/f64), Option voor null, serde rename voor camelCase, plus valkuilen om te vermijden. Gratis.

11 min leestijd

JSON naar Rust-struct: serde-structs genereren (gids 2026)

Plak je payload in de JSON naar Rust struct omzetter en kopieer de serde-struct die eruit rolt. Dat is de hele workflow van JSON naar Rust-struct: niets installeren, niets uploaden, alles gebeurt in je browser. Heb je nu meteen een struct nodig, dan ben je binnen enkele seconden klaar.

Een struct genereren en de juiste struct genereren zijn echter twee verschillende problemen. Een omzetter kan alleen maar gokken. En bij Rust staat er meer op het spel. In TypeScript zakt een verkeerde gok terug naar een losse any die je kunt negeren. Bij serde loopt een verkeerde gok vast bij het deserialiseren: een float die in een i64 belandt, een onverwachte null in een niet-optioneel veld, en serde_json::from_str geeft je een Err in plaats van je data. Daarom is het in Rust belangrijker dan in de meeste talen om de struct meteen goed te krijgen.

Hieronder lees je hoe de inferentie precies werkt, welke regel voor numerieke types de meeste omzetters verkeerd doen, wanneer je naar #[serde(rename)] grijpt in plaats van #[serde(rename_all)], en hoe je omgaat met datums, dynamische sleutels en Rust-keywords zodat de output altijd compileert.

Zo zet je JSON om naar Rust

JSON omzetten naar Rust gaat in drie stappen:

  1. Plak je JSON. Zet een object, array of ruwe API-respons in het invoerveld. De omzetting draait direct en volledig aan de clientkant.
  2. Stel de output af. Hernoem de root-struct, schakel de serde-derives aan of uit, voeg Debug en Clone toe, of laat de pub-zichtbaarheid weg om bij de stijl van je crate te passen.
  3. Kopieer of download. Pak de gegenereerde Rust met één klik en plak het rechtstreeks in je project.

Dat is het hele transactionele pad. Is je invoer geminificeerd of twijfel je of hij geldig is, haal hem dan eerst door een JSON formatter zodat de omzetter met schone, correct opgebouwde JSON werkt. De rest van deze gids legt uit hoe je de output leest, zodat je de gevallen kunt corrigeren die een tool niet zelf kan afleiden.

Hoe JSON-types op Rust worden afgebeeld

Elke JSON-waarde heeft een Rust-tegenhanger, maar de afbeelding is niet helemaal één-op-één:

JSON-waardeRust-type
"text"String
42i64 (grote waarden u64, daarboven f64)
3.14, 2e3f64
true / falsebool
nullOption<T> (of Option<serde_json::Value>)
[1, 2, 3]Vec<T>
{ ... }een benoemde struct

Bij objecten zit het echte werk. Neem een typische REST-payload:

{
  "id": 101,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "active": true,
  "roles": ["admin", "user"]
}

De omzetter produceert een serde-klare struct:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub id: i64,
    pub name: String,
    pub email: String,
    pub active: bool,
    pub roles: Vec<String>,
}

Elke scalar wordt afgebeeld op een primitief type, en roles wordt Vec<String>. Het enige JSON-type dat niet netjes past is number: JSON heeft één numeriek type, terwijl Rust je laat kiezen tussen i64, u64 en f64. Die keuze krijgt hieronder een hele sectie, want daar gaan de meeste generatoren de fout in.

Zo leidt de omzetter structs af

Vier regels dekken bijna alles wat je erin stopt: één struct per objectvorm, samenvoegen sleutel voor sleutel bij arrays, precieze numerieke types en idiomatische veldnamen.

Structurele inferentie: één benoemde struct per object

Elke afzonderlijke objectvorm wordt een eigen benoemde struct. Geneste objecten worden niet inline gezet; ze worden opgetild naar losse, gerefereerde definities:

{ "repo": "serde", "owner": { "login": "dtolnay", "id": 100 } }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub repo: String,
    pub owner: Owner,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Owner {
    pub login: String,
    pub id: i64,
}

De geneste owner wordt een eigen Owner-struct, waar het veld naar verwijst. Identieke vormen worden ontdubbeld, zodat twee velden met dezelfde structuur één struct delen in plaats van kopieën te maken.

Arrays samenvoegen en Option-velden

Geef je een array van objecten door, dan voegt de omzetter ze sleutel voor sleutel samen. Een sleutel die in sommige elementen wel en in andere niet voorkomt, wordt een Option:

{ "users": [{ "id": 1, "nick": "x" }, { "id": 2 }] }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub users: Vec<User>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: i64,
    pub nick: Option<String>,
}

id komt in elk element voor, dus blijft verplicht. nick ontbreekt bij de tweede gebruiker en wordt daarom Option<String>. Let op wat er niet staat: geen #[serde(default)]. serde behandelt Option al als optioneel en deserialiseert een ontbrekende sleutel naar None, dus dat attribuut zou overbodig zijn.

Correcte numerieke types: i64, u64, f64

Dit is de regel die concurrerende tools overslaan, en juist die loopt vast op echte payloads. De generator hanteert drie niveaus. Een integer wordt i64. Overschrijdt een waarde i64::MAX, dan promoveert hij naar u64. Boven u64::MAX valt hij terug op f64. Elk token met een decimaalteken of een exponent, zoals 1.0 of 2e3, wordt f64, ongeacht de grootte.

{ "user_id": 12000000000000000000, "balance": 19.99, "retries": 3 }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub user_id: u64,
    pub balance: f64,
    pub retries: i64,
}

Hier is user_id groter dan i64::MAX, dus belandt hij in u64 en blijft de heen-en-weer-conversie kloppen. balance heeft een decimaalteken en is dus f64, geen integer. serde dwingt dat type namelijk af bij het deserialiseren. Geef het een struct die een snowflake- of Twitter-achtige ID als i32 typeert en de waarde loopt over; typeer een float-veld als i64 en serde geeft een fout in plaats van stilletjes af te kappen. De drie-niveauregel zorgt ervoor dat een gegenereerde rust struct from json blijft deserialiseren in plaats van vast te lopen met een panic op de eerste grote ID.

snake_case-velden en #[serde(rename)]

JSON-sleutels zijn vaak camelCase; idiomatische Rust-velden zijn snake_case. De omzetter hernoemt het veld en voegt een #[serde(rename)] toe die het terugkoppelt aan de exacte JSON-sleutel:

{ "login": "octocat", "publicRepos": 15, "followerCount": 9001, "createdAt": "2011-01-25" }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub login: String,
    #[serde(rename = "publicRepos")]
    pub public_repos: i64,
    #[serde(rename = "followerCount")]
    pub follower_count: i64,
    #[serde(rename = "createdAt")]
    pub created_at: String,
}

Een sleutel die al snake_case is, zoals login, krijgt geen rename. De rest wordt netjes teruggekoppeld aan het wire-formaat, terwijl je Rust-code natuurlijk leest.

#[serde(rename)] vs #[serde(rename_all)]

De tool geeft een #[serde(rename)] per veld omdat dat altijd werkt, ook als één payload camelCase, snake_case en onregelmatige sleutels in hetzelfde object door elkaar gebruikt. De tool hoeft nooit te redeneren over een gedeelde conventie die er misschien niet is.

Delen alle velden in een struct wel één conventie, dan kun je die attributen samenvouwen tot één rename op containerniveau. Verwijder de regels per veld en zet één #[serde(rename_all = "camelCase")] op de struct:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
    pub login: String,
    pub public_repos: i64,
    pub follower_count: i64,
    pub created_at: String,
}

Beide versies deserialiseren dezelfde JSON. Gebruik rename per veld voor gemengde of onregelmatige sleutels; grijp naar rename_all wanneer een struct uniform is en je minder ruis wilt. serde levert voor hetzelfde attribuut ook andere conventies, zoals "snake_case", "kebab-case" en "SCREAMING_SNAKE_CASE".

Rust-keywords en sleutels die geen identifier zijn

JSON-sleutels zijn gewoon strings, dus niets houdt een API tegen om een sleutel met de naam type of match te sturen, allebei gereserveerde woorden in Rust. De generator schoont deze op tot een geldige identifier plus een rename:

{ "type": "user", "match": true, "first-name": "Ada" }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    #[serde(rename = "type")]
    pub type_: String,
    #[serde(rename = "match")]
    pub match_: bool,
    #[serde(rename = "first-name")]
    pub first_name: String,
}

De vorm met een liggend streepje achteraan (type_) is bewust gekozen boven een raw identifier als r#type. Raw identifiers kunnen niet elk keyword op elke plek uitdrukken; met name self, crate en super worden als raw identifier geweigerd, dus een aanpak van opschonen plus rename is de enige die altijd compileert. Sleutels die geen identifier zijn, zoals first-name of 2fa met een cijfer vooraan, krijgen dezelfde behandeling: een geldige Rust-naam en een rename terug naar de letterlijke JSON-sleutel.

Deserialiseren met serde_json

Zodra je de struct hebt, voeg je de dependencies toe aan Cargo.toml:

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Daarna heb je maar één regel nodig om het in te lezen. Dit voorbeeld is compleet en compileert zoals het is:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub id: i64,
    pub name: String,
    pub email: String,
    pub active: bool,
    pub roles: Vec<String>,
}

fn main() -> Result<(), serde_json::Error> {
    let data = r#"{"id":101,"name":"Ada Lovelace","email":"a@ex.com","active":true,"roles":["admin"]}"#;
    let root: Root = serde_json::from_str(data)?;
    println!("{} has {} role(s)", root.name, root.roles.len());
    Ok(())
}

De Deserialize-derive doet al het werk. Gebruik serde_json::from_str voor een string, from_slice voor een byte-slice, of from_reader voor een bestand of HTTP-body, en serde_json::to_string om een waarde terug te serialiseren naar JSON. Dit is de beloning voor het goed kiezen van de types: de serde derive die je genereerde is een runtime-validator, dus een serde_json deserialize-aanroep die Ok teruggeeft, is een garantie dat de data bij je struct paste — niet slechts de overtuiging van een compiler dat het zo was.

Datums, dynamische sleutels en onbekende velden

Sommige vormen passen niet op een gewoon struct-veld, en de generator geeft die een verstandige standaardwaarde die je vervolgens met de hand aanscherpt.

Datums. JSON heeft geen datumtype, dus een ISO- of RFC 3339-timestamp komt binnen als een gewone String. Voor echt werken met datums zet je het veld om naar een chrono-type en schakel je chrono’s serde-feature in (chrono = { version = "0.4", features = ["serde"] }). serde leest RFC 3339 dan automatisch in:

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
    pub name: String,
    pub created_at: DateTime<Utc>,
}

Dynamische sleutels. Als sleutels tijdens runtime variëren, zoals een map van ID’s naar waarden, is een vaste struct de verkeerde vorm. Gebruik een HashMap met String als sleutel:

use std::collections::HashMap;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub scores: HashMap<String, i64>,
}

Onbekende extra velden. Wil je een getypeerde struct houden maar toch sleutels opvangen die je niet hebt gemodelleerd, voeg dan een #[serde(flatten)]-veld toe dat op een HashMap steunt. Voor een waarde die echt dynamisch is, is serde_json::Value de vangnetoplossing. Als je eigenlijk structuur wilt valideren in plaats van types te genereren, behandelt de gids over JSON Schema-validatie hoe je een contract van begin tot eind afdwingt.

json-to-rust vs. quicktype vs. typeshare vs. handmatig

Er is geen enkele beste manier om Rust-types uit JSON te produceren. Het hangt af van waar de JSON leeft en in welke richting je omzet.

AanpakGeschikt voorOpmerking
Online omzetter (deze tool, transform.tools)Eenmalige conversies, gevoelige payloads, niets installerenLeidt af uit een voorbeeld, volledig aan de clientkant
quicktypeOutput in meerdere talen, codegen in een pipelineOok op voorbeelden gebaseerd; CLI of web
typeshareBestaande Rust-types delen met TS, Swift, KotlinOmgekeerde richting: Rust naar andere talen
schemafy / typifyGenereren uit een JSON SchemaInvoer is een schema, geen voorbeeld
Structs met de hand schrijvenKleine payloads, serde lerenVolledige controle, bewerkelijk en foutgevoelig op schaal

Het onderscheid dat je je eigen moet maken: een online omzetter en quicktype leiden beide types af uit een voorbeeld van JSON, terwijl typeshare de andere kant op werkt en geannoteerde Rust-types omzet naar TypeScript of Swift. schemafy en typify nemen een JSON Schema als invoer, geen voorbeelddata. Is je codebase TypeScript in plaats van Rust, dan geldt dezelfde op voorbeelden gebaseerde aanpak met de JSON naar TypeScript Omzetter, en de gids over JSON naar TypeScript-interfaces behandelt die kant grondig.

Veelvoorkomende valkuilen bij het genereren van Rust uit JSON

Gegenereerde structs zijn een startpunt. Let op het volgende voordat je de output op live data vertrouwt.

  • Eén voorbeeld onthult zelden elke vorm. Optionele velden kun je alleen afleiden uit meerdere array-elementen. Plak een representatieve array, zodat de Option-inferentie klopt en geen gok is op basis van één gelukkig object.
  • Lege of gemengde arrays vallen terug op serde_json::Value. Dat is het eerlijke antwoord als er niets af te leiden valt. Geef een rijker voorbeeld om een concreet elementtype te krijgen.
  • Versmal een grote ID niet tot i32. Snowflake- en soortgelijke ID’s overschrijden 2^53 en lopen over een 32-bits veld. Houd de gegenereerde i64 of u64 aan.
  • Een veld dat altijd null is, wordt Option<serde_json::Value>, geen gewone optional. De tool heeft geen type om af te leiden, dus behandel het als een tijdelijke waarde en geef het een echt type zodra je er een weet.
  • serde_json::Value heeft de serde_json-dependency nodig. Bevat de output Value en ontbreekt de crate in je Cargo.toml, dan compileert de code niet.
  • Een datum die als String blijft staan, kan geen datumberekeningen doen. Wil je timestamps vergelijken of formatteren, stap dan over op een chrono-type in plaats van strings met de hand in te lezen.

Veelgestelde vragen

Waarom weigert serde mijn JSON terwijl de struct er goed uitziet?

Bijna altijd een type-mismatch. De twee gebruikelijke oorzaken zijn een floating-pointwaarde die in een i64-veld belandt, en een sleutel die soms ontbreekt maar niet als Option is getypeerd. serde dwingt het type af bij het deserialiseren, dus zet het veld goed op f64 of Option<T>, of genereer de struct opnieuw uit een representatief voorbeeld.

Welk numeriek Rust-type moet een JSON-integer worden?

Standaard i64. Promoveer naar u64 als een waarde i64::MAX overschrijdt, en val terug op f64 voorbij u64::MAX. Elk getal met een decimaalteken of exponent wordt f64, ongeacht de grootte, omdat serde een float weigert die in een integer-veld wordt gedeserialiseerd.

Hoe maak ik een veld optioneel als JSON het weglaat?

Typeer het als Option<T>. serde behandelt Option automatisch als optioneel en deserialiseert een ontbrekende sleutel naar None, dus je hebt #[serde(default)] niet nodig. De omzetter markeert een veld als Option wanneer sommige bemonsterde items de sleutel missen.

Gebruik ik #[serde(rename)] of #[serde(rename_all)]?

Gebruik #[serde(rename)] per veld wanneer een payload naamgevingsstijlen door elkaar gebruikt; dat werkt altijd. Volgt elk veld in een struct één conventie, verwijder dan de attributen per veld en zet in plaats daarvan één #[serde(rename_all = "camelCase")] op de struct. Beide deserialiseren identiek.

Hoe ga ik om met JSON-sleutels die Rust-keywords zijn, zoals type?

De generator geeft type_ met een #[serde(rename = "type")] die terugkoppelt aan de oorspronkelijke sleutel. Dat is robuuster dan een raw identifier als r#type, omdat raw identifiers self, crate en super niet op elke plek kunnen dekken.

Waarom worden JSON-datums als String getypeerd in plaats van als een datumtype?

JSON heeft geen datumtype, dus een timestamp is onderweg gewoon een string, en String is de eerlijke standaard. Voor echt werken met datums verander je het veld in chrono’s DateTime<Utc> of NaiveDate en schakel je chrono’s serde-feature in; serde leest RFC 3339 dan voor je in.

Hoe deserialiseer ik JSON naar de gegenereerde struct?

Voeg serde_json toe aan Cargo.toml en schrijf dan let root: Root = serde_json::from_str(json)?;. De Deserialize-derive doet de rest. Gebruik from_slice voor een byte-slice en from_reader voor een bestand of HTTP-body.

Is mijn JSON privé bij het gebruik van een online JSON naar Rust struct omzetter?

Ja. De omzetting draait 100% in je browser met JavaScript. Je JSON, inclusief tokens, ID’s en klantgegevens, verlaat de pagina nooit en wordt nooit naar een server gestuurd. Als je zover bent, probeer de JSON naar Rust struct omzetter op je eigen payload.

Tags: rust json serde type-safety developer-tools

Gerelateerde artikelen

Alle artikelen bekijken