Skip to content
Select theme

How to handle a missing field in JSON

Short answer: When a key can be absent, make the field an Option<T> — serde fills it with None whether the key is missing or null — or give the field a fallback with #[serde(default)]. Pick Option when the code has to notice the field is gone; pick default when there is a value to fall back to and nothing downstream should have to branch.

This continues the json-demo project from the earlier pages in this chapter — cargo new json-demo, with the same two crates:

Cargo.toml
[dependencies]
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.154"

A required field that the API sometimes omits fails with missing field when the payload arrives — that error is on the deserialisation page. A key that is spelled differently is a rename problem instead, and rename and alias are on the previous page. This page is about the two remaining cases: the key is not there at all, or it is there and null.

src/main.rs
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct User {
id: u64,
full_name: Option<String>,
}
fn main() {
let with = r#"{"id": 7, "full_name": "Grace Hopper"}"#;
let without = r#"{"id": 7}"#;
let null = r#"{"id": 7, "full_name": null}"#;
for text in [with, without, null] {
let User { id, full_name } = serde_json::from_str(text).unwrap();
println!("id {id}, full_name {full_name:?}");
}
}
id 7, full_name Some("Grace Hopper")
id 7, full_name None
id 7, full_name None

No attribute is needed — serde treats Option as optional by itself. An absent key and an explicit null both arrive as None, and the two are indistinguishable afterwards. That is the trade: the type forces every reader of the field to decide what None means, with a match or an unwrap_or, instead of quietly inventing a value.

#[serde(default)]: a fallback the code never sees

Section titled “#[serde(default)]: a fallback the code never sees”
src/main.rs
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct User {
id: u64,
#[serde(default)]
full_name: String,
}
fn main() {
let without = r#"{"id": 7}"#;
let with = r#"{"id": 7, "full_name": "Grace Hopper"}"#;
for text in [without, with] {
let User { id, full_name } = serde_json::from_str(text).unwrap();
println!("id {id}, full_name {full_name:?}");
}
}
id 7, full_name ""
id 7, full_name "Grace Hopper"

#[serde(default)] fills an absent field with Default::default() for its type: "" for a String, 0 for numbers, false for a bool. The field stays a plain String, so the rest of the program never learns the key was missing.

src/main.rs
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct User {
id: u64,
#[serde(default)]
full_name: String,
}
fn main() {
let text = r#"{"id": 7, "full_name": null}"#;
match serde_json::from_str::<User>(text) {
Ok(user) => println!("id {}, full_name {:?}", user.id, user.full_name),
Err(e) => println!("{e}"),
}
}
invalid type: null, expected a string at line 1 column 27

The default fires only when the key is absent. A null is a present value of the wrong type, and the error says so. So: the API omits the key and default is enough; the API sends null and the field has to be an Option.

src/main.rs
use serde::Deserialize;
fn default_port() -> u16 {
8080
}
#[derive(Debug, Deserialize)]
struct Config {
host: String,
#[serde(default = "default_port")]
port: u16,
}
fn main() {
let minimal = r#"{"host": "localhost"}"#;
let full = r#"{"host": "localhost", "port": 9000}"#;
for text in [minimal, full] {
let Config { host, port } = serde_json::from_str(text).unwrap();
println!("{host}:{port}");
}
}
localhost:8080
localhost:9000

#[serde(default = "default_port")] takes the path to a function — no parentheses — and uses its return value when the key is missing. The attribute also goes on the struct itself, where it covers every field at once: serde starts from Self::default() and overwrites the keys the JSON contains, which is why that form needs #[derive(Default)] on the struct.

  • Option when “missing” is information — the field’s absence changes what the program does.
  • default when every caller wants the same fallback and the field should read like any other.
  • #[serde(default)] on an Option field changes nothing: an absent key already produces None.
  • Going the other way, a None serialises as null unless the field carries skip_serializing_if — that is on the serialisation page.