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:
e.line()ande.column(): the position in the input.e.is_syntax(): not valid JSON at all (a stray character, a missing comma).e.is_eof(): the input ended too early (truncated file, empty input).e.is_data(): valid JSON, wrong shape for your type (missing field, wrong type, unknown variant).
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:
- Parse into a struct, then check it in a function that returns
Result(simple and explicit; the next exercise does this). - Make the type validate itself:
#[serde(try_from = "String")]on a newtype tells serde to deserialize aStringfirst and then call yourTryFrom<String>impl. Invalid values never exist as your type at all.
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:
- not valid JSON (syntax error or input ending early):
syntax error at line L, column C - valid JSON, wrong shape:
bad config:followed by serde's message - port
0:invalid: port must not be 0 - workers outside
1..=64:invalid: workers must be 1 to 64 - otherwise:
starting <name> on port <port> with <workers> workers
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).