RustArena

Step 6 of 6

Bad input, validation and other formats

At work, the JSON you parse comes from users, other teams and the internet. Three habits make that safe.

1. Report errors helpfully

A serde_json::Error knows where it happened and what kind of problem it was:

use serde::Deserialize;

#[derive(Deserialize)]
struct Job {
    name: String,
    retries: u8,
}

fn check(text: &str) {
    match serde_json::from_str::<Job>(text) {
        Ok(job) => println!("ok: {} x{}", job.name, job.retries),
        Err(e) if e.is_syntax() || e.is_eof() => {
            println!("not JSON (line {}, column {})", e.line(), e.column())
        }
        Err(e) => println!("wrong shape: {e}"),
    }
}

fn main() {
    check(r#"{"name": "backup", "retries": 3}"#);
    check(r#"{"name": "backup" "retries": 3}"#);
    check(r#"{"name": "backup", "retries": 999}"#);
    check("");
}
ok: backup x3
not JSON (line 1, column 19)
wrong shape: invalid value: integer `999`, expected u8 at line 1 column 33
not JSON (line 1, column 0)

2. Validate after parsing

serde checks the shape; your business rules (a port isn't 0, an email has an @, a date range doesn't run backwards) are your job. Two common patterns:

use serde::Deserialize;

#[derive(Deserialize)]
#[serde(try_from = "String")]
struct Email(String);

impl TryFrom<String> for Email {
    type Error = String;
    fn try_from(s: String) -> Result<Self, Self::Error> {
        if s.contains('@') { Ok(Email(s)) } else { Err(format!("invalid email: {s}")) }
    }
}

#[derive(Deserialize)]
struct Signup {
    email: Email,
}

fn main() {
    for text in [r#"{"email": "[email protected]"}"#, r#"{"email": "ana"}"#] {
        match serde_json::from_str::<Signup>(text) {
            Ok(s) => println!("welcome {}", s.email.0),
            Err(e) => println!("rejected: {e}"),
        }
    }
}
welcome [email protected]
rejected: invalid email: ana at line 1 column 16

3. Don't unwrap outside input

Inside a function, propagate with ? (next module: with context attached). In main, print a clear message and exit with a failure code (the CLI module). Never let a malformed request crash a server.

Other formats: same derives

Because the derives are format-independent, switching formats is a one-line change. With the toml crate (not available on this site, so shown only):

use serde::Deserialize;

#[derive(Deserialize)]
struct Config {
    name: String,
    port: u16,
}

fn load(text: &str) -> Result<Config, toml::de::Error> {
    toml::from_str(text) // text is: name = "arena"\nport = 8080
}

The same Config reads JSON with serde_json::from_str. Other format crates you'll meet: YAML (several crates exist; the original serde_yaml is no longer maintained, so check what your team uses), csv for spreadsheets, and binary formats like MessagePack (rmp-serde), bincode and postcard for compact messages between services and devices.

Your task

Write a config loader. Read all of standard input and parse it into Config { name: String, port: u16, workers: u32 }. Print exactly one line:

Challenge 2

Make the type validate itself. Complete Username so that serde reads a String and then calls TryFrom<String>: a username must be 3 to 16 characters long (count chars()) and contain only ASCII letters, digits and _. The error messages are username too short, username too long and username has invalid characters. Account must then reject bad usernames while parsing.

Challenge 3

Orders arrive as JSON Lines on standard input, one {"id": 7, "cents": 1250} per line. Parse each non-empty line into the starter's Order (only cents is required; a missing id is fine). For every line that fails, print line N: <serde message> (N counts from 1, including empty lines). At the end print total: X.YZ (the sum of the valid orders' cents, in dollars with two decimals) and valid: V of T (T counts non-empty lines).

Previous: Dynamic JSON with Value and json!