Serde Default Values: Why Functions Instead of Constants?
If you’ve worked with Serde deserialization in Rust, you’ve probably seen code like this:
#[derive(Debug, serde::Deserialize)]
pub struct SearchArgs {
pub query: String,
#[serde(default = "default_limit")]
pub limit: u64,
}
fn default_limit() -> u64 { 10 }
You might be wondering: Why use a function? Why not just a constant? Well I did anway!
This tutorial explains the reasoning behind this pattern and when to use it.
The Problem: Missing Fields
When deserializing JSON into Rust structs, you often want optional fields with sensible defaults. Without defaults, this JSON would fail:
{
"query": "search term"
}
Because limit is missing, Serde can’t deserialize it into:
pub struct SearchArgs {
pub query: String,
pub limit: u64, // ❌ Missing in JSON!
}
Solution 1: The Simple Default
You can use #[serde(default)] to use the type’s Default trait:
#[derive(Debug, serde::Deserialize)]
pub struct SearchArgs {
pub query: String,
#[serde(default)] // Uses u64::default() = 0
pub limit: u64,
}
Problem: This gives you limit = 0, which probably isn’t what you want for a search limit!
Solution 2: Custom Default Functions
Instead, provide a function that returns your preferred default:
#[derive(Debug, serde::Deserialize)]
pub struct SearchArgs {
pub query: String,
#[serde(default = "default_limit")]
pub limit: u64,
}
fn default_limit() -> u64 { 10 } // Much better!
Now missing limit fields get a sensible value of 10 instead of 0.
Why Not Constants?
Reason 1: Serde’s API Design
The #[serde(default = "...")] attribute only accepts function paths, not constants:
// ❌ This doesn't compile
const DEFAULT_LIMIT: u64 = 10;
#[serde(default = DEFAULT_LIMIT)] // Error!
pub limit: u64,
// ✅ This works
fn default_limit() -> u64 { 10 }
#[serde(default = "default_limit")]
pub limit: u64,
Serde expects a string containing the path to a function, not a value.
Reason 2: Type Flexibility
Functions work with any return type, including complex structs:
fn default_config() -> AppConfig {
AppConfig {
timeout: 30,
retries: 3,
endpoint: "https://api.example.com".to_string(),
}
}
#[serde(default = "default_config")]
pub config: AppConfig,
Constants would be more limiting and verbose for complex types.
Real-World Example
Here’s a practical example from an MCP server using Qdrant:
use serde::Deserialize;
use schemars::JsonSchema;
#[derive(Debug, Deserialize, JsonSchema)]
pub struct SearchTextArgs {
/// The text query to search for
pub query: String,
/// Maximum number of results (default: 10)
#[serde(default = "default_limit")]
pub limit: u64,
/// Include payload data in results (default: true)
#[serde(default = "default_with_payload")]
pub with_payload: bool,
}
fn default_limit() -> u64 { 10 }
fn default_with_payload() -> bool { true }
This allows clients to send minimal JSON:
{
"query": "find documents"
}
Which deserializes to:
SearchTextArgs {
query: "find documents",
limit: 10, // from default_limit()
with_payload: true, // from default_with_payload()
}
Optional: The Hybrid Approach
If you want to define constants for reuse elsewhere, you can combine both:
// Define constants for use in multiple places
pub const DEFAULT_LIMIT: u64 = 10;
pub const DEFAULT_WITH_PAYLOAD: bool = true;
// Wrapper functions for Serde
fn default_limit() -> u64 { DEFAULT_LIMIT }
fn default_with_payload() -> bool { DEFAULT_WITH_PAYLOAD }
#[derive(Debug, Deserialize)]
pub struct SearchArgs {
#[serde(default = "default_limit")]
pub limit: u64,
#[serde(default = "default_with_payload")]
pub with_payload: bool,
}
// Now you can use the constants elsewhere too
impl SearchArgs {
pub fn new(query: String) -> Self {
Self {
query,
limit: DEFAULT_LIMIT,
with_payload: DEFAULT_WITH_PAYLOAD,
}
}
}
However, this is usually overkill. The inline functions are concise enough:
fn default_limit() -> u64 { 10 }
Best Practices
✅ Do This
// Clear, concise, inline defaults
fn default_limit() -> u64 { 10 }
fn default_timeout() -> u64 { 30 }
#[serde(default = "default_limit")]
pub limit: u64,
⚠️ Consider This
// If you need the value in multiple places
pub const DEFAULT_LIMIT: u64 = 10;
fn default_limit() -> u64 { DEFAULT_LIMIT }
❌ Avoid This
// Using type defaults when you want specific values
#[serde(default)] // Gives you 0, not 10!
pub limit: u64,
Summary
- Serde requires functions for
#[serde(default = "...")], not constants - Functions let you provide sensible defaults instead of
0orfalse - Keep it simple: inline functions are usually best
- Only extract to constants if you need the value elsewhere in your code
This pattern makes your APIs more ergonomic by allowing clients to omit optional fields while still getting reasonable default values.
