Loading page…
Rust walkthroughs
Loading page…
#[serde(deny_unknown_fields)] for forward compatibility in APIs?#[serde(deny_unknown_fields)] configures a struct to reject any JSON object containing fields that don't have corresponding struct fields, rather than silently ignoring them. This creates a trade-off for API evolution: it prevents silent data loss when clients send new fields that older API versions don't recognize, but it also breaks backward compatibility when the API adds new fields that older clients didn't expect. Without this attribute, unknown fields are silently dropped during deserialization, which can hide bugs but enables forward compatibility. The choice depends on your API versioning strategy: strict validation catches client errors early but requires careful version management, while permissive parsing enables smoother API evolution but risks silent data loss.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct User {
name: String,
email: String,
}
fn main() {
let json = r#"{
"name": "Alice",
"email": "alice@example.com",
"role": "admin",
"department": "engineering"
}"#;
// Unknown fields are silently ignored
let user: User = serde_json::from_str(json).unwrap();
println!("{:?}", user);
// User { name: "Alice", email: "alice@example.com" }
// role and department are discarded
}By default, Serde silently discards any fields not defined in the struct.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct User {
name: String,
email: String,
}
fn main() {
let json = r#"{
"name": "Alice",
"email": "alice@example.com",
"role": "admin"
}"#;
// This will fail because "role" is unknown
let result: Result<User, _> = serde_json::from_str(json);
match result {
Ok(user) => println!("{:?}", user),
Err(e) => println!("Error: {}", e),
// Error: unknown field `role`, expected `name` or `email`
}
}deny_unknown_fields rejects any unrecognized fields with a clear error message.
use serde::{Deserialize, Serialize};
// Version 1.0 of API
#[derive(Deserialize, Serialize, Debug)]
#[serde(deny_unknown_fields)]
struct ConfigV1 {
timeout: u32,
retries: u32,
}
// Version 2.0 adds new field
#[derive(Deserialize, Serialize, Debug)]
#[serde(deny_unknown_fields)]
struct ConfigV2 {
timeout: u32,
retries: u32,
cache_ttl: u32, // New field in v2
}
fn main() {
// Server sends v2 response to v1 client
let v2_response = r#"{
"timeout": 30,
"retries": 3,
"cache_ttl": 300
}"#;
// v1 client with deny_unknown_fields fails!
let result: Result<ConfigV1, _> = serde_json::from_str(v2_response);
assert!(result.is_err());
// Error: unknown field `cache_ttl`
}With deny_unknown_fields, adding new fields breaks older clients.
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)]
struct ConfigV1 {
timeout: u32,
retries: u32,
}
fn main() {
// Server sends v2 response to v1 client
let v2_response = r#"{
"timeout": 30,
"retries": 3,
"cache_ttl": 300
}"#;
// v1 client silently ignores unknown field
let config: ConfigV1 = serde_json::from_str(v2_response).unwrap();
println!("{:?}", config);
// ConfigV1 { timeout: 30, retries: 3 }
// cache_ttl is silently discarded
}Without the attribute, new fields are ignored, enabling forward compatibility.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct Config {
timeout: u32,
retries: u32,
}
fn main() {
// Old client sends v1 request to v2 server
let v1_request = r#"{
"timeout": 30,
"retries": 3
}"#;
// This works - no unknown fields
let config: Config = serde_json::from_str(v1_request).unwrap();
println!("{:?}", config);
}Old clients sending fewer fields always works, regardless of the attribute.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Settings {
debug: bool,
verbose: bool,
}
fn main() {
// Client typo: "debugg" instead of "debug"
let json_with_typo = r#"{
"debugg": true,
"verbose": true
}"#;
// Without deny_unknown_fields: silently accepts with defaults
let settings: Settings = serde_json::from_str(json_with_typo).unwrap();
println!("{:?}", settings);
// Settings { debug: false, verbose: true }
// debug is false (default) because "debugg" was ignored!
}Without deny_unknown_fields, typos result in silent data loss.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct Settings {
debug: bool,
verbose: bool,
}
fn main() {
let json_with_typo = r#"{
"debugg": true,
"verbose": true
}"#;
let result: Result<Settings, _> = serde_json::from_str(json_with_typo);
match result {
Ok(settings) => println!("{:?}", settings),
Err(e) => {
// Catches the typo immediately
println!("Configuration error: {}", e);
// unknown field `debugg`, expected `debug` or `verbose`
}
}
}deny_unknown_fields catches typos and configuration errors at parse time.
use serde::Deserialize;
// Strategy 1: Permissive (forward compatible)
#[derive(Deserialize, Debug)]
struct ApiV1Permissive {
name: String,
version: String,
// Unknown fields ignored - new fields work
}
// Strategy 2: Strict (catches errors, breaks on new fields)
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct ApiV1Strict {
name: String,
version: String,
// Unknown fields rejected - catches typos
}
// Strategy 3: Explicit unknown field handling
#[derive(Deserialize, Debug)]
struct ApiV1Explicit {
name: String,
version: String,
#[serde(flatten)]
extra: serde_json::Value, // Capture unknown fields
}
fn main() {
let json = r#"{"name": "my-api", "version": "1.0", "new_field": "value"}"#;
// Permissive: works
let p: ApiV1Permissive = serde_json::from_str(json).unwrap();
println!("Permissive: {:?}", p);
// Strict: fails
let s: Result<ApiV1Strict, _> = serde_json::from_str(json);
println!("Strict: {:?}", s);
// Explicit: captures unknown fields
let e: ApiV1Explicit = serde_json::from_str(json).unwrap();
println!("Explicit: {:?}", e);
// extra contains {"new_field": "value"}
}Three strategies with different trade-offs for API evolution.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Config {
name: String,
#[serde(flatten)]
other: serde_json::Map<String, serde_json::Value>,
}
fn main() {
let json = r#"{
"name": "my-service",
"timeout": 30,
"retries": 3,
"debug": true
}"#;
let config: Config = serde_json::from_str(json).unwrap();
println!("Name: {}", config.name);
println!("Other fields: {:?}", config.other);
// {"timeout": Number(30), "retries": Number(3), "debug": Bool(true)}
// Can process unknown fields
if let Some(timeout) = config.other.get("timeout") {
println!("Timeout: {:?}", timeout);
}
}Use #[serde(flatten)] to capture unknown fields instead of rejecting or ignoring.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct ServerConfig {
#[serde(default)]
port: u16,
#[serde(default = "default_host")]
host: String,
}
fn default_host() -> String {
"localhost".to_string()
}
fn main() {
// Both fields missing but no unknown fields
let json = r#"{}"#;
let config: ServerConfig = serde_json::from_str(json).unwrap();
println!("{:?}", config);
// ServerConfig { port: 0, host: "localhost" }
// Unknown field still rejected
let json_with_unknown = r#"{"hostname": "example.com"}"#;
let result: Result<ServerConfig, _> = serde_json::from_str(json_with_unknown);
assert!(result.is_err());
}Default values work independently of deny_unknown_fields.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct DatabaseConfig {
host: String,
port: u16,
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct AppConfig {
database: DatabaseConfig,
name: String,
}
fn main() {
let json = r#"{
"name": "my-app",
"database": {
"host": "localhost",
"port": 5432,
"ssl": true
}
}"#;
// Error: unknown field `ssl` in nested struct
let result: Result<AppConfig, _> = serde_json::from_str(json);
assert!(result.is_err());
}deny_unknown_fields applies recursively to nested structs that also have the attribute.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct StrictConfig {
name: String,
// Unknown fields rejected at this level
}
#[derive(Deserialize, Debug)]
struct PermissiveMetadata {
created_at: String,
// Unknown fields ignored at this level
}
#[derive(Deserialize, Debug)]
struct MixedConfig {
config: StrictConfig,
metadata: PermissiveMetadata,
}
fn main() {
let json = r#"{
"config": {
"name": "test",
"extra": "rejected"
},
"metadata": {
"created_at": "2024-01-01",
"extra": "ignored"
}
}"#;
// Config section is strict, metadata is permissive
let result: Result<MixedConfig, _> = serde_json::from_str(json);
assert!(result.is_err()); // "extra" in config is rejected
}Each struct controls its own unknown field handling independently.
use serde::Deserialize;
// API client for external service
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct ApiResponse {
id: String,
status: String,
}
// Problem: API adds new field, client breaks
fn handle_response(json: &str) -> Result<ApiResponse, serde_json::Error> {
serde_json::from_str(json)
}
fn main() {
// API adds "timestamp" field
let new_response = r#"{
"id": "123",
"status": "ok",
"timestamp": "2024-01-15"
}"#;
// Client with deny_unknown_fields fails
match handle_response(new_response) {
Ok(response) => println!("{:?}", response),
Err(e) => println!("API change detected: {}", e),
}
}For external APIs, avoid deny_unknown_fields to handle API evolution gracefully.
use serde::Deserialize;
// Server receiving client requests
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct CreateUserRequest {
username: String,
email: String,
}
fn handle_create_user(json: &str) -> Result<String, String> {
let request: CreateUserRequest = serde_json::from_str(json)
.map_err(|e| format!("Invalid request: {}", e))?;
// Process request
Ok(format!("Created user: {}", request.username))
}
fn main() {
// Client sends unexpected field
let invalid_request = r#"{
"username": "alice",
"email": "alice@example.com",
"role": "admin"
}"#;
match handle_create_user(invalid_request) {
Ok(msg) => println!("{}", msg),
Err(e) => println!("Error: {}", e),
// Error: Invalid request: unknown field `role`
}
}For server validation, deny_unknown_fields catches unexpected client behavior.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let json = r#"{"x": 1.0, "y": 2.0, "z": 3.0}"#;
match serde_json::from_str::<Point>(json) {
Ok(point) => println!("{:?}", point),
Err(e) => {
// Clear error message indicating the unknown field
println!("Error: {}", e);
// unknown field `z`, expected `x` or `y`
}
}
// Multiple unknown fields
let json = r#"{"x": 1.0, "y": 2.0, "a": 1, "b": 2}"#;
match serde_json::from_str::<Point>(json) {
Ok(point) => println!("{:?}", point),
Err(e) => {
println!("Error: {}", e);
// unknown field `a`, expected `x` or `y`
// Note: only first unknown field is reported
}
}
}Error messages clearly indicate the unknown field and expected fields.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct Config {
required: String,
optional: Option<u32>,
}
fn main() {
// Missing optional field is fine
let json = r#"{"required": "test"}"#;
let config: Config = serde_json::from_str(json).unwrap();
println!("{:?}", config);
// Config { required: "test", optional: None }
// Unknown field is rejected
let json = r#"{"required": "test", "unknown": 123}"#;
let result: Result<Config, _> = serde_json::from_str(json);
assert!(result.is_err());
// Present optional field works
let json = r#"{"required": "test", "optional": 42}"#;
let config: Config = serde_json::from_str(json).unwrap();
println!("{:?}", config);
}Optional fields are different from unknown fields; deny_unknown_fields doesn't affect them.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct User {
#[serde(rename = "userName")]
username: String,
#[serde(rename = "emailAddress")]
email: String,
}
fn main() {
// Correct: use renamed field names in JSON
let json = r#"{"userName": "alice", "emailAddress": "alice@example.com"}"#;
let user: User = serde_json::from_str(json).unwrap();
println!("{:?}", user);
// Error: using struct field names
let json = r#"{"username": "alice", "email": "alice@example.com"}"#;
let result: Result<User, _> = serde_json::from_str(json);
assert!(result.is_err());
// unknown field `username`, expected `userName` or `emailAddress`
// Error: unknown field
let json = r#"{"userName": "alice", "emailAddress": "a@b.com", "role": "admin"}"#;
let result: Result<User, _> = serde_json::from_str(json);
assert!(result.is_err());
}deny_unknown_fields uses the serialized (renamed) field names in error messages.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct Config {
#[serde(alias = "timeout_ms")]
timeout: u64,
}
fn main() {
// Primary name works
let json = r#"{"timeout": 1000}"#;
let config: Config = serde_json::from_str(json).unwrap();
println!("{:?}", config);
// Alias works
let json = r#"{"timeout_ms": 1000}"#;
let config: Config = serde_json::from_str(json).unwrap();
println!("{:?}", config);
// Both provided: uses primary, alias ignored
let json = r#"{"timeout": 1000, "timeout_ms": 2000}"#;
let result: Result<Config, _> = serde_json::from_str(json);
assert!(result.is_err()); // timeout_ms is unknown when timeout present
}Aliases are recognized as valid fields, not unknown.
| Aspect | With deny_unknown_fields | Without |
|--------|---------------------------|---------|
| Typos | Caught immediately | Silently ignored |
| New fields | Breaks old clients | Ignored by old clients |
| API evolution | Requires version management | More flexible |
| Debugging | Clearer error messages | Silent failures |
| Forward compat | Breaks on new fields | Handles new fields |
| Backward compat | Works (fewer fields) | Works |
Use deny_unknown_fields when:
Avoid deny_unknown_fields when:
Alternatives:
#[serde(flatten)] with serde_json::Value to capture unknown fieldsKey insight: deny_unknown_fields is a trade-off between strict validation and flexibility. It prevents silent data loss from typos but breaks forward compatibility. For APIs you control and can version carefully, the strictness is valuable. For external APIs or APIs that need to evolve without breaking clients, the default permissive behavior is safer. Choose based on whether you need strict contracts or flexible evolution.