When the JSON is incomplete
You are building a configuration loader for a web server. The JSON file arrives with {"host": "localhost"}. Your struct defines host, port, and timeout. The deserializer crashes because port and timeout are missing. You didn't want to force the user to specify every single field; you just wanted sensible fallbacks. If port is absent, it should be 8080. If timeout is absent, it should be 30 seconds.
Serde is strict by design. It expects a one-to-one mapping between JSON keys and struct fields. If a key is missing, Serde rejects the input. This strictness prevents silent data loss, but it makes partial updates and optional config fields painful without help.
That's where #[serde(default)] comes in. It tells Serde to stop complaining when a field is missing and instead fill in a value. The attribute is the bridge between incomplete data and a complete struct.
The "fill in the blank" stamp
Think of deserialization like a clerk checking a form. The clerk goes down the list of fields. If a box is filled, the clerk copies the value. If a box is empty, the clerk checks for a stamp.
#[serde(default)] is that stamp. When the clerk sees an empty box with the stamp, they write the standard default value and move on. If the box is empty and there is no stamp, the clerk rejects the form.
The "standard default value" comes from the Default trait in Rust. Every type that implements Default has a canonical zero-value. For u32, it is 0. For String, it is "". For Vec, it is []. Serde uses Default::default() to generate the fallback.
This means #[serde(default)] only works out of the box for types that implement Default. If you try to use it on a custom type without Default, the compiler stops you. Serde doesn't guess; it relies on the trait system.
Minimal example: relying on Default
Here is the simplest case. You have a config struct with a timeout field. You mark it with #[serde(default)]. Since u32 implements Default, Serde can fill in 0 if the JSON omits the key.
use serde::Deserialize;
#[derive(Deserialize)]
struct Config {
// If JSON lacks "timeout", Serde calls u32::default() which returns 0.
#[serde(default)]
timeout: u32,
}
fn main() {
let json = r#"{}"#;
let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.timeout, 0);
}
The code compiles and runs. The timeout is 0. This is technically correct, but 0 is rarely a useful default for a timeout. A zero timeout might mean "no wait" or "immediate failure", which breaks your logic.
Zero is rarely a good default for a port or a timeout. Don't let Default trick you into accepting garbage values.
Custom defaults for real values
When Default::default() isn't what you want, you provide a custom function. The syntax changes from #[serde(default)] to #[serde(default = "function_name")]. Serde calls that function whenever the field is missing.
The function must return the exact type of the field. It takes no arguments. It can be a free function, a method, or a constant path.
use serde::Deserialize;
// Convention: default functions are private and return the field type.
// Naming as `default_<field>` is the community standard for clarity.
fn default_timeout() -> u32 {
30
}
#[derive(Deserialize)]
struct Config {
// Calls default_timeout() when the field is missing.
#[serde(default = "default_timeout")]
timeout: u32,
}
fn main() {
let json = r#"{}"#;
let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.timeout, 30);
}
The function runs only when the key is missing. If the JSON contains {"timeout": 10}, Serde parses 10 and ignores the function entirely. The default is a fallback, not a validator.
The function runs only when the key is missing. If the key is present, Serde parses the value and ignores the function.
How Serde generates the code
Serde is a macro-based library. When you derive Deserialize, the macro generates code that looks roughly like this:
// Simplified view of generated code
impl<'de> Deserialize<'de> for Config {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct ConfigVisitor;
impl<'de> Visitor<'de> for ConfigVisitor {
type Value = Config;
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut timeout = None;
while let Some(key) = map.next_key()? {
match key {
Field::Timeout => {
if timeout.is_some() {
return Err(Error::duplicate_field("timeout"));
}
// Parse the value if present
timeout = Some(map.next_value()?);
}
}
}
// If missing, call the default
let timeout = timeout.unwrap_or_else(|| <u32 as Default>::default());
Ok(Config { timeout })
}
}
// ...
}
}
With #[serde(default = "default_timeout")], the generated code changes the fallback line to timeout.unwrap_or_else(default_timeout).
This reveals a crucial detail: the default logic is baked into the generated code at compile time. The function must be in scope and must satisfy the type signature. If you use #[serde(default)] on a type that doesn't implement Default, the generated code tries to call Default::default() and the compiler rejects it with E0277 (the trait bound MyType: Default is not satisfied).
Serde won't guess your intent. If Default isn't right, provide the function.
Struct-level defaults
You can apply #[serde(default)] to the entire struct, not just individual fields. This is useful when most fields have sensible defaults and you want to avoid repeating the attribute.
use serde::Deserialize;
#[derive(Default, Deserialize)]
#[serde(default)]
struct Config {
host: String,
port: u16,
timeout: u32,
}
fn main() {
// Only host is provided. port and timeout get their Default values.
let json = r#"{"host": "localhost"}"#;
let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 0);
assert_eq!(config.timeout, 0);
}
The struct-level default requires the struct to implement Default. The derive macro #[derive(Default)] generates the implementation, which calls Default::default() for each field.
You can mix struct-level and field-level defaults. Field-level attributes override the struct-level behavior. If the struct has #[serde(default)] but a field has #[serde(default = "custom")], the field uses the custom function.
Use struct-level defaults to avoid repeating the attribute on every field.
Pitfalls and compiler errors
Missing Default implementation
The most common error is using #[serde(default)] on a type that doesn't implement Default.
#[derive(Deserialize)]
struct Config {
#[serde(default)]
mode: Mode,
}
enum Mode {
Debug,
Release,
}
This fails to compile. Mode does not implement Default. The compiler emits E0277 (the trait bound Mode: Default is not satisfied). You must either implement Default for Mode or provide a custom function.
impl Default for Mode {
fn default() -> Self {
Self::Debug
}
}
Confusing default with skip_serializing_if
Beginners often confuse #[serde(default)] with #[serde(skip_serializing_if = "...")]. They solve opposite problems.
#[serde(default)] controls deserialization (reading JSON into Rust). It fills in missing fields.
#[serde(skip_serializing_if = "...")] controls serialization (writing Rust to JSON). It omits fields when they match a condition.
If you want a field to be optional in both directions, you need both attributes.
#[derive(Serialize, Deserialize)]
struct Config {
// Fill with 8080 if missing during deserialization.
#[serde(default = "default_port")]
// Omit during serialization if the value is 8080.
#[serde(skip_serializing_if = "is_default_port")]
port: u16,
}
fn default_port() -> u16 { 8080 }
fn is_default_port(port: &u16) -> bool { *port == 8080 }
Option fields are already defaultable
Option<T> implements Default as None. Adding #[serde(default)] to an Option field is redundant. Serde will produce None for missing keys regardless of the attribute.
#[derive(Deserialize)]
struct Config {
// This default is redundant. Missing key produces None anyway.
#[serde(default)]
tag: Option<String>,
}
The attribute doesn't hurt, but it adds noise. Remove it unless you are using it for documentation purposes.
However, if you want a default of Some(value) instead of None, you need a custom function.
fn default_tag() -> Option<String> {
Some("default".to_string())
}
#[derive(Deserialize)]
struct Config {
#[serde(default = "default_tag")]
tag: Option<String>,
}
Now a missing key produces Some("default"). This is a subtle but powerful pattern for required fields that have a fallback.
Don't rely on Default for primitives if zero breaks your logic. Write the function.
Convention asides
Naming default functions: The community convention is default_<field_name>. This makes the function self-documenting. default_timeout is clearer than timeout_default or get_timeout. Stick to the prefix form.
Function scope: The function must be in scope at the point where the struct is defined. You can define it in the same module, or import it. You cannot reference a function from a different module without importing it. The string in #[serde(default = "...")] is a path, so #[serde(default = "crate::defaults::timeout")] works if the path is valid.
cargo fmt: Serde attributes are formatted by cargo fmt. You might see #[serde(default)] or #[serde( default )] in wild code. Run cargo fmt and stop arguing about whitespace. The formatter handles it.
Decision: choosing the right default strategy
Use #[serde(default)] when the field type implements Default and the default value is acceptable for missing data.
Use #[serde(default = "function")] when you need a custom value that differs from Default::default().
Reach for Option<T> without default when the absence of a field should result in None rather than a concrete value.
Pick #[serde(default = "function")] returning Option<T> when you want a default of Some(value) instead of None.
Apply #[serde(default)] at the struct level when most fields have sensible defaults and you want to reduce attribute repetition.
Avoid #[serde(default)] on Option<T> fields unless you have a specific reason to document the fallback; the behavior is redundant.
Treat #[serde(default)] as a contract: "If this is missing, fill it with the standard value." If the standard value is wrong, the contract is broken. Provide the function.