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.
🎯 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 vanprijs_centenin JSONprijsCenten. Kies één stijl voor je hele API.skip_serializing_iflaatomschrijvingweg als hijNoneis, in plaats van"omschrijving": null.defaultvult 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 prijsCententerwijl jeprijs_centenstuurt:rename_allis actief; stuur de camelCase-naam.Serializevergeten op een genest type: de compiler noemt het type dat de trait mist.f64voor geld: zie les 1. Gebruik centen.
Oefenen
- Maak
Product,NieuwProductenWijzigProductzoals hierboven, metcamelCase. - Maak
StatusenBetalingmettag = "soort"en schrijf een unit test die de JSON-vorm controleert. - Voeg
Cententoe en gebruik het inProduct. Wat verandert er aan de JSON? - Test
deny_unknown_fieldsopNieuwProductmet een extra veldid.
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 mettransparent; datums in RFC 3339. - Volgende les: gedeelde state met
Arc.
📝 Quiz: Serde: JSON lezen en schrijven
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.