When the JSON has surprises
You deserialize a JSON payload from an API. Your struct defines id and name. The JSON contains id, name, and favorite_color. You expect a crash. You get a result. The favorite_color vanishes. Serde parsed the data you asked for and silently discarded the rest.
This is the default behavior. Serde ignores unknown fields unless you tell it otherwise. The design choice prioritizes forward compatibility. APIs evolve. Config files grow. If every new field broke your deserialization, you would rewrite your code every time the data source added metadata. Serde protects you by focusing only on the fields you declared.
The default behavior: Ignore and move on
Serde treats unknown fields as noise. When the deserializer encounters a key that doesn't match any field in your struct, it skips that key and continues. No error occurs. No warning appears. The data is simply not loaded.
use serde::Deserialize;
/// A config struct that only cares about the host.
/// Extra fields in the input are ignored automatically.
#[derive(Deserialize, Debug)]
struct ServerConfig {
host: String,
port: u16,
}
fn main() {
// The JSON has 'host', 'port', and 'debug'.
// 'debug' is unknown to ServerConfig.
let json = r#"{"host": "localhost", "port": 8080, "debug": true}"#;
// Deserialization succeeds. 'debug' is dropped.
let config: ServerConfig = serde_json::from_str(json).unwrap();
println!("{:?}", config);
// Output: ServerConfig { host: "localhost", port: 8080 }
}
Under the hood, the #[derive(Deserialize)] macro generates code that iterates over the input keys. For each key, it checks if the key matches a field name. If there is a match, it deserializes the value into that field. If there is no match, it checks for the deny_unknown_fields attribute. If the attribute is absent, the generated code calls a skip method on the deserializer and moves to the next key. The unknown data never touches your struct.
Trust the silence. Serde ignores unknown fields by default so your code survives data evolution.
Missing fields versus unknown fields
Beginners often confuse missing fields with unknown fields. They are different problems that require different solutions.
A missing field is a key that your struct expects but the input does not provide. An unknown field is a key that the input provides but your struct does not expect.
#[serde(default)] handles missing fields. It tells Serde what value to use when a key is absent. It has no effect on unknown fields. If you add #[serde(default)] to a struct, Serde will still ignore unknown fields. It will just fill in blanks for fields you declared but didn't receive.
use serde::Deserialize;
/// This struct expects 'name' and 'age'.
/// 'name' has a default. 'age' does not.
#[derive(Deserialize, Debug)]
struct User {
#[serde(default)]
name: String,
age: u8,
}
fn main() {
// Case 1: 'name' is missing. 'extra' is unknown.
// 'default' fills 'name'. 'extra' is ignored.
let json1 = r#"{"age": 30, "extra": "ignored"}"#;
let user1: User = serde_json::from_str(json1).unwrap();
println!("{:?}", user1);
// Output: User { name: "", age: 30 }
// Case 2: 'age' is missing. Deserialization fails.
// 'default' does not apply to 'age'.
let json2 = r#"{"name": "Alice"}"#;
let result: Result<User, _> = serde_json::from_str(json2);
println!("{:?}", result);
// Output: Err(Error("missing field `age`", line:1 column:18))
}
The default attribute provides a fallback for absence. It does not catch surprises. If you need to reject unknown fields, default won't help. You need deny_unknown_fields.
Confuse missing with unknown and you'll chase ghosts. default fills gaps. It doesn't catch surprises.
Rejecting surprises with deny_unknown_fields
Sometimes you want strictness. If the input contains data you didn't ask for, you want to know. This is common when you control both sides of the data exchange, or when unexpected fields indicate a version mismatch or a bug.
Add #[serde(deny_unknown_fields)] to your struct. Serde will return an error if it encounters any key that doesn't match a field.
use serde::Deserialize;
/// A strict struct for internal data exchange.
/// Any field not listed here causes a deserialization error.
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct DatabaseRecord {
id: u64,
value: String,
}
fn main() {
let json = r#"{"id": 1, "value": "hello", "timestamp": 12345}"#;
// Deserialization fails because 'timestamp' is unknown.
let result: Result<DatabaseRecord, _> = serde_json::from_str(json);
match result {
Ok(record) => println!("{:?}", record),
Err(e) => println!("Error: {}", e),
}
// Output: Error: unknown field `timestamp`, expected one of `id`, `value`
}
The error message is explicit. It lists the unknown field and the fields Serde expected. This makes debugging straightforward. You see exactly which field caused the rejection.
The community convention is to reserve deny_unknown_fields for internal formats or strict contracts. In library code, avoid deny_unknown_fields on public types. Users of your library might add fields to their data that you haven't implemented yet. Rejecting those fields breaks their code. Keep public deserialization lenient. Use deny_unknown_fields in binaries where you control the data source.
Deny unknown fields to enforce contracts. Silence hides bugs.
Capturing the leftovers with flatten
Ignoring unknown fields works when you don't care about the extra data. Rejecting them works when extra data is an error. What if you want to keep the extra data?
Use #[serde(flatten)] with a HashMap. This tells Serde to deserialize all unknown fields into the map. Known fields still go into their struct members. Unknown fields end up in the map.
use serde::Deserialize;
use std::collections::HashMap;
/// A config struct that captures extra settings.
/// Known fields are typed. Unknown fields go into 'extra'.
#[derive(Deserialize, Debug)]
struct FlexibleConfig {
host: String,
#[serde(flatten)]
extra: HashMap<String, serde_json::Value>,
}
fn main() {
let json = r#"{"host": "localhost", "timeout": 30, "retries": 3}"#;
let config: FlexibleConfig = serde_json::from_str(json).unwrap();
println!("Host: {}", config.host);
println!("Extra: {:?}", config.extra);
// Output:
// Host: localhost
// Extra: {"timeout": Number(30), "retries": Number(3)}
}
The flatten attribute changes the deserialization logic. Serde first tries to match keys to named fields. Keys that don't match any named field are collected into the flattened container. The container must implement Deserialize for a map-like structure. HashMap<String, serde_json::Value> is the standard choice because it accepts any JSON value.
You can combine flatten with deny_unknown_fields. If you do, deny_unknown_fields applies to keys that don't match any named field and don't end up in the flattened map. This is rare, but useful if you have multiple flattened maps and want to ensure every key lands somewhere.
Flatten when you need the leftovers. Don't discard data you might need later.
Pitfalls and error messages
Silent data loss is the biggest risk with unknown fields. If your struct ignores a field that contains critical information, your program proceeds with incomplete data. You might not notice until runtime behavior goes wrong.
For example, an API adds a deprecated field to signal that a resource is being removed. Your struct ignores the field. Your code continues to use the resource. The API shuts it down. Your app breaks. If you had used deny_unknown_fields, you would have seen the error immediately and updated your struct.
Another pitfall is assuming #[serde(default)] handles unknown fields. It doesn't. If you add default to a struct and still get unknown field errors with deny_unknown_fields, check your field names. The error isn't about missing values. It's about keys that don't exist in the struct.
When deny_unknown_fields triggers, the error is a runtime deserialization error, not a compiler error. The code compiles fine. The failure happens when you call from_str or from_slice. The error type is serde_json::Error (or the error type of your format). It implements std::error::Error. You can inspect the error message to find the offending field.
If you see unknown field 'foo', expected one of 'bar', 'baz', the input has a key foo that your struct doesn't define. Either add foo to the struct, add #[serde(flatten)] to capture it, or remove it from the input.
Check your field names against the input. A typo in a field name makes the real field unknown.
Decision: How to handle unknown fields
Use Serde's default behavior when you are parsing config files, user data, or third-party APIs where extra metadata might appear and you don't care about it. Forward compatibility is free.
Use #[serde(deny_unknown_fields)] when you are building a strict contract, like a database migration tool or an internal API where unexpected data indicates a bug or a version mismatch. Catch mistakes early.
Use #[serde(flatten)] when you need to preserve unknown fields for later processing or logging. Capture the data instead of dropping it.
Use #[serde(default)] when a field might be missing from the input and you want to provide a fallback value. This handles absence, not unknown keys.
Reach for deny_unknown_fields in binaries. Keep public library types lenient.