Step 3 of 6
Shaping the JSON: rename, default, skip
The JSON you exchange is often designed by someone else: a JavaScript frontend uses camelCase, an API has a field called type (a Rust keyword), a config file leaves out settings that have sensible defaults. serde attributes let your Rust structs keep Rust names and still match the JSON exactly.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ServerConfig {
host_name: String,
#[serde(default = "default_port")]
port: u16,
#[serde(default)]
max_connections: u32,
#[serde(skip_serializing_if = "Option::is_none")]
admin_email: Option<String>,
}
fn default_port() -> u16 {
8080
}
fn main() {
let cfg: ServerConfig = serde_json::from_str(r#"{"hostName": "arena.local"}"#).unwrap();
println!("{} {} {} {:?}", cfg.host_name, cfg.port, cfg.max_connections, cfg.admin_email);
println!("{}", serde_json::to_string(&cfg).unwrap());
}
arena.local 8080 0 None
{"hostName":"arena.local","port":8080,"maxConnections":0}
Piece by piece:
#[serde(rename_all = "camelCase")]on the struct renames every field in both directions:host_nameishostNamein JSON. Other choices:"snake_case","kebab-case","SCREAMING_SNAKE_CASE","PascalCase","lowercase","UPPERCASE".#[serde(rename = "type")]on one field renames just that field. It's how you handle JSON keys that are Rust keywords: name the fieldkindand rename it.Option<T>fields are special: a missing key (ornull) becomesNone. No attribute needed.#[serde(default)]fills a missing field withDefault::default()(0,"", emptyVec,false).#[serde(default = "default_port")]calls your function instead; the string is a path to a function with no arguments.#[serde(skip_serializing_if = "Option::is_none")]leaves the key out of the output instead of writingnull. It takes any functionfn(&T) -> bool;"Vec::is_empty"is another favorite.#[serde(skip)]ignores a field completely (it must implementDefault, since it's filled with the default when reading).
Strict configs: deny_unknown_fields
Ignoring unknown fields is great for APIs, bad for config files: a typo like "prot": 9000 would silently be ignored and you'd run on the default port. #[serde(deny_unknown_fields)] turns it into an error:
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Limits {
users: u32,
rooms: u32,
}
fn main() {
match serde_json::from_str::<Limits>(r#"{"users": 50, "roms": 4}"#) {
Ok(l) => println!("{} users, {} rooms", l.users, l.rooms),
Err(e) => println!("config error: {e}"),
}
}
config error: unknown field `roms`, expected `users` or `rooms` at line 1 column 20
Common mistakes
#[serde(default)]on a type withoutDefault. The derive expands to a call toDefault::default(), so you getthe trait bound ... Default is not satisfied. Derive or implementDefault, or usedefault = "some_fn".- Writing the function instead of its path:
default = "default_port()"is an error; it'sdefault = "default_port". - Mixing up the two renames.
rename_allgoes on the struct (or enum) and applies a casing rule;renamegoes on one field and gives it an exact name.
Your task
A payment provider sends webhooks like {"eventId": "ev_1", "type": "charge", "amountCents": 1299, "currency": "EUR", "customerEmail": "[email protected]"}. Complete Webhook so that:
- keys are camelCase (
event_idiseventId), and the fieldkindis the JSON keytype, - a missing
currencymeans"USD"(writefn default_currency() -> String), customer_emailis optional, and when it'sNonethe key is left out of the output entirely.
Challenge 2
Game settings files only list what the player changed. Put #[serde(default)] on the struct itself (not on a field): then every missing field comes from the struct's Default impl. Write that impl Default for Settings with volume 80, difficulty "normal" and subtitles true, and add the attribute.
Challenge 3
Write fn load_limits(json: &str) -> Result<Limits, String> for a strict config. The JSON keys are kebab-case (max-users, max-rooms), unknown keys must be rejected (so a typo is caught), and on error return the serde message as a String with config: in front, for example config: unknown field ....