Beetje Coderen

Les 5 · Module 1 · Async en Axum-basis

Serde: JSON lezen en schrijven

Beheers Serde voor je API-contract: rename-regels, optionele en standaardvelden, enums als tagged JSON, eigen datatypen en het scheiden van request- en responsetypen.

⏱ 75 minuten leestijd + oefenen

Illustratie: een document en dozen met pijlen heen en terug, data die van vorm verandert

🎯 Na deze les kun je

  • Serialize en Deserialize afleiden en met attributen sturen
  • optionele velden en standaardwaarden correct modelleren
  • enums als JSON weergeven met tag en content
  • aparte typen voor invoer en uitvoer gebruiken

Je API-contract is de JSON die clients sturen en terugkrijgen. Met Serde leg je dat contract vast in Rust-typen. In deze les maak je de typen van kassa-api netjes: duidelijke veldnamen, geen onverwachte nulls, en enums die clients begrijpen.

De basis

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Product {
    pub id: u32,
    pub naam: String,
    pub prijs_centen: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub omschrijving: Option<String>,
    #[serde(default = "waar")]
    pub actief: bool,
}
fn waar() -> bool { true }
  • rename_all = "camelCase" maakt van prijs_centen in JSON prijsCenten. Kies één stijl voor je hele API.
  • skip_serializing_if laat omschrijving weg als hij None is, in plaats van "omschrijving": null.
  • default vult een ontbrekend veld bij het inlezen.

Invoer en uitvoer scheiden

Een client mag geen id meesturen en geen actief aanzetten bij het aanmaken. Gebruik daarom aparte typen:

#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct NieuwProduct { pub naam: String, pub prijs_centen: u32, pub omschrijving: Option<String> }

#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WijzigProduct { pub naam: Option<String>, pub prijs_centen: Option<u32>, pub actief: Option<bool> }

Voor PATCH betekent None "niet wijzigen". Wil je onderscheid tussen "veld weggelaten" en "expliciet null", gebruik dan Option<Option<T>> met #[serde(default, with = "::serde_with::rust::double_option")] (crate serde_with).

Enums

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Status { Open, Betaald, Geannuleerd }

JSON: "open", "betaald". Enums met data:

#[derive(Serialize, Deserialize)]
#[serde(tag = "soort", rename_all = "snake_case")]
pub enum Betaling {
    Contant { ontvangen_centen: u32 },
    Pin { terminal: String },
    Bon { code: String },
}
{ "soort": "pin", "terminal": "T1" }

Voorspel eerst: wat geeft serde_json::to_string(&Betaling::Contant { ontvangen_centen: 500 }) zonder het attribuut tag?

Uitkomst

{"Contant":{"ontvangen_centen":500}}: de standaard is externally tagged, met de variantnaam als sleutel. Dat is lastig voor clients in JavaScript of TypeScript. Met #[serde(tag = "soort")] wordt het {"soort":"contant","ontvangen_centen":500}: één object met een discriminator-veld, wat bijna elke client en OpenAPI-tool goed begrijpt.

Eigen typen: geld

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Centen(pub u32);

impl std::ops::Add for Centen { type Output = Centen; fn add(self, o: Centen) -> Centen { Centen(self.0 + o.0) } }

transparent laat het type in JSON als gewoon getal verschijnen, terwijl je in Rust niet per ongeluk een aantal en een prijs kunt optellen.

Datums

// cargo add time --features serde-well-known,macros
#[derive(Serialize)]
pub struct Bestelling { pub id: u32, #[serde(with = "time::serde::rfc3339")] pub geplaatst_op: time::OffsetDateTime }

Gebruik altijd een vast formaat (RFC 3339, bijv. 2026-03-01T12:00:00Z).

Typische fouten

  • missing field prijsCenten terwijl je prijs_centen stuurt: rename_all is actief; stuur de camelCase-naam.
  • Serialize vergeten op een genest type: de compiler noemt het type dat de trait mist.
  • f64 voor geld: zie les 1. Gebruik centen.

Oefenen

  1. Maak Product, NieuwProduct en WijzigProduct zoals hierboven, met camelCase.
  2. Maak Status en Betaling met tag = "soort" en schrijf een unit test die de JSON-vorm controleert.
  3. Voeg Centen toe en gebruik het in Product. Wat verandert er aan de JSON?
  4. Test deny_unknown_fields op NieuwProduct met een extra veld id.
Modelantwoord oefening 1

Zie De basis en Invoer en uitvoer scheiden. Controleer: {"naam":"Thee","prijsCenten":250} wordt ingelezen; de response bevat id en actief.

Modelantwoord oefening 2
#[test]
fn pin_als_json() {
    let j = serde_json::to_value(Betaling::Pin { terminal: "T1".into() }).unwrap();
    assert_eq!(j, serde_json::json!({ "soort": "pin", "terminal": "T1" }));
    let s: Status = serde_json::from_str("\"betaald\"").unwrap();
    assert_eq!(s, Status::Betaald);
}
Modelantwoord oefening 3

Niets: door #[serde(transparent)] blijft het "prijsCenten": 250. In Rust is Centen(250) + 3u32 nu een compileerfout: precies de bedoeling.

Modelantwoord oefening 4

unknown field id, expected one of naam, prijsCenten, omschrijving en status 422. Een client kan zo geen id kiezen.

Lokaal oefenen

Geen live lab. Draai cargo test voor de JSON-tests; die hebben geen server nodig.

Samenvatting

  • Serde-attributen bepalen je JSON-contract: rename_all, default, skip_serializing_if, deny_unknown_fields.
  • Aparte typen voor aanmaken, wijzigen en teruggeven.
  • Enums als tag-JSON; nieuwtypen met transparent; datums in RFC 3339.
  • Volgende les: gedeelde state met Arc.

📝 Quiz: Serde: JSON lezen en schrijven

5 vragen · 70% nodig om te slagen

1. Wat doet #[serde(rename_all = "camelCase")]?
2. Waarom aparte typen voor NieuwProduct en Product?
3. Wat doet #[serde(tag = "soort")] op een enum?
4. Wat doet #[serde(transparent)] op Centen(u32)?
5. Wat doet skip_serializing_if = "Option::is_none"?

Dit was de laatste gratis les

Je hebt les 1–5 van Rust-webservices met Axum gratis gevolgd. Wil je verder? Met één aankoop (€ 49,00, eenmalig) krijg je de overige 15 lessen, de live labs en het certificaat. Geen abonnement.

Cursus kopen en verdergaan