RustArena

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:

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

Your task

A payment provider sends webhooks like {"eventId": "ev_1", "type": "charge", "amountCents": 1299, "currency": "EUR", "customerEmail": "[email protected]"}. Complete Webhook so that:

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 ....

Previous: From JSON back into structs Next: Enums in JSON