Loading page…
Rust walkthroughs
Loading page…
serde::de::IgnoredAny enable forward-compatible deserialization for unknown fields?serde::de::IgnoredAny is a special deserializer type that consumes any valid Serde data—objects, arrays, strings, numbers, booleans, or null—without storing the value, enabling forward-compatible deserialization where unknown fields from newer API versions are silently skipped instead of causing errors. When a struct uses #[serde(deny_unknown_fields)], any unrecognized field in the input causes deserialization to fail, which breaks forward compatibility because adding fields to an API would break older clients. By using IgnoredAny in combination with flatten or explicit field handling, deserializers can accept and discard unknown fields gracefully, allowing older code to process data from newer versions that have additional fields without failing.
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"
}"#;
let user: User = serde_json::from_str(json).unwrap();
println!("{:?}", user);
// Works! Unknown field "role" is ignored by default
}By default, Serde ignores unknown fields for forward compatibility.
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"
}"#;
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 breaks forward compatibility by rejecting unknown fields.
use serde::de::IgnoredAny;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct User {
name: String,
email: String,
#[serde(flatten)]
_extra: std::collections::HashMap<String, IgnoredAny>,
}
fn main() {
let json = r#"{
"name": "Alice",
"email": "alice@example.com",
"role": "admin",
"created_at": "2024-01-15",
"preferences": {
"theme": "dark",
"notifications": true
}
}"#;
let user: User = serde_json::from_str(json).unwrap();
println!("{:?}", user);
// Unknown fields are consumed but not stored
}IgnoredAny consumes unknown fields without storing them.
use serde::de::IgnoredAny;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Config {
version: String,
name: String,
#[serde(flatten)]
_forward_compat: IgnoredAny,
}
fn main() {
// Future API version adds new fields
let json = r#"{
"version": "1.0",
"name": "myapp",
"new_feature_flag": true,
"experimental_setting": "enabled"
}"#;
let config: Config = serde_json::from_str(json).unwrap();
println!("{:?}", config);
// Extra fields are silently consumed
}Using IgnoredAny with flatten captures and discards all unrecognized fields.
use serde::Deserialize;
// Approach 1: Capture unknown fields for inspection
#[derive(Deserialize, Debug)]
struct CaptureUnknown {
name: String,
#[serde(flatten)]
extra: std::collections::HashMap<String, serde_json::Value>,
}
// Approach 2: Ignore unknown fields completely
#[derive(Deserialize, Debug)]
struct IgnoreUnknown {
name: String,
#[serde(flatten)]
_extra: std::collections::HashMap<String, serde::de::IgnoredAny>,
}
fn main() {
let json = r#"{"name": "test", "future": "field", "another": 123}"#;
// Captures unknown fields
let captured: CaptureUnknown = serde_json::from_str(json).unwrap();
println!("Captured: {:?}", captured.extra);
// HashMap {"future": String("field"), "another": Number(123)}
// Ignores unknown fields
let ignored: IgnoreUnknown = serde_json::from_str(json).unwrap();
println!("Ignored: {:?}", ignored._extra);
// HashMap keys exist but values are empty IgnoredAny
}serde_json::Value stores unknown fields; IgnoredAny consumes them without storage.
use serde::de::{self, Deserialize, Deserializer, IgnoredAny, Visitor};
use std::fmt;
use std::marker::PhantomData;
#[derive(Debug)]
struct FlexibleStruct {
known_field: String,
}
impl<'de> Deserialize<'de> for FlexibleStruct {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FlexibleVisitor;
impl<'de> Visitor<'de> for FlexibleVisitor {
type Value = FlexibleStruct;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a map with known_field and any other fields")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
let mut known_field = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"known_field" => {
known_field = Some(map.next_value()?);
}
_ => {
// Ignore unknown fields with IgnoredAny
map.next_value::<IgnoredAny>()?;
}
}
}
Ok(FlexibleStruct {
known_field: known_field.ok_or_else(|| de::Error::missing_field("known_field"))?,
})
}
}
deserializer.deserialize_map(FlexibleVisitor)
}
}
fn main() {
let json = r#"{
"known_field": "value",
"unknown1": "ignored",
"unknown2": {"nested": "object"},
"unknown3": [1, 2, 3]
}"#;
let result: FlexibleStruct = serde_json::from_str(json).unwrap();
println!("{:?}", result);
}Custom deserializers use IgnoredAny to skip unknown fields during manual parsing.
use serde::de::IgnoredAny;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct ApiResponse {
status: String,
data: UserData,
#[serde(flatten)]
_extra: IgnoredAny,
}
#[derive(Deserialize, Debug)]
struct UserData {
id: u64,
username: String,
#[serde(flatten)]
_extra: IgnoredAny,
}
fn main() {
// Complex nested structure with unknown fields at each level
let json = r#"{
"status": "ok",
"data": {
"id": 123,
"username": "alice",
"profile_picture": "avatar.png",
"bio": "Developer"
},
"meta": {
"timestamp": "2024-01-15T10:00:00Z",
"version": "2.0"
},
"unknown_top_level": true
}"#;
let response: ApiResponse = serde_json::from_str(json).unwrap();
println!("{:?}", response);
}IgnoredAny handles nested unknown fields at any depth.
use serde::de::IgnoredAny;
use serde::Deserialize;
use std::mem;
fn main() {
// IgnoredAny has zero size
println!("IgnoredAny size: {} bytes", mem::size_of::<IgnoredAny>());
// Compare with serde_json::Value which stores data
println!("Value size: {} bytes", mem::size_of::<serde_json::Value>());
// For large unknown data, IgnoredAny saves memory
let json = r#"{
"known": "value",
"large_unknown": {
"nested": {
"deeply": {
"array": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
"more": "data"
}
}
}
}"#;
#[derive(Deserialize)]
struct Efficient {
known: String,
#[serde(flatten)]
_extra: std::collections::HashMap<String, IgnoredAny>,
}
let parsed: Efficient = serde_json::from_str(json).unwrap();
// Unknown data was parsed but not stored
}IgnoredAny is zero-sized; unknown data is parsed but not stored in memory.
use serde::de::IgnoredAny;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(tag = "type")]
enum Message {
#[serde(rename = "user")]
User { name: String, email: String },
#[serde(rename = "system")]
System { message: String },
}
#[derive(Deserialize, Debug)]
struct Envelope {
#[serde(flatten)]
message: Message,
#[serde(flatten)]
_unknown: IgnoredAny,
}
fn main() {
let json = r#"{
"type": "user",
"name": "Bob",
"email": "bob@example.com",
"timestamp": "2024-01-15",
"metadata": {"source": "api"}
}"#;
let envelope: Envelope = serde_json::from_str(json).unwrap();
println!("{:?}", envelope);
}IgnoredAny works with tagged enums to ignore extra fields alongside enum variants.
use serde::de::IgnoredAny;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct RequestV1 {
#[serde(rename = "apiVersion")]
api_version: String,
payload: String,
#[serde(flatten)]
_future_fields: IgnoredAny,
}
#[derive(Deserialize, Debug)]
struct RequestV2 {
#[serde(rename = "apiVersion")]
api_version: String,
payload: String,
#[serde(rename = "metadata")]
metadata: Option<Metadata>,
#[serde(flatten)]
_future_fields: IgnoredAny,
}
#[derive(Deserialize, Debug)]
struct Metadata {
timestamp: String,
#[serde(flatten)]
_extra: IgnoredAny,
}
fn main() {
// V2 request received by V1 handler
let v2_json = r#"{
"apiVersion": "2.0",
"payload": "data",
"metadata": {
"timestamp": "2024-01-15",
"request_id": "abc123"
}
}"#;
// V1 handler ignores V2-specific fields
let v1_parsed: RequestV1 = serde_json::from_str(v2_json).unwrap();
println!("V1 handler parsed: {:?}", v1_parsed);
// V2 handler can access metadata
let v2_parsed: RequestV2 = serde_json::from_str(v2_json).unwrap();
println!("V2 handler parsed: {:?}", v2_parsed);
}IgnoredAny enables version-tolerant APIs where older clients ignore newer fields.
use serde::de::IgnoredAny;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Document {
id: String,
title: String,
// Skip expensive content parsing when we only need metadata
#[serde(rename = "content", skip_deserializing)]
content: Option<String>,
#[serde(flatten)]
_unknown: IgnoredAny,
}
fn main() {
// Large document with expensive content
let json = r#"{
"id": "doc-123",
"title": "Important Document",
"content": "Very long content that would be expensive to parse...",
"metadata": {
"author": "Alice",
"created": "2024-01-15"
}
}"#;
let doc: Document = serde_json::from_str(json).unwrap();
println!("{:?}", doc);
// content is skipped, unknown fields ignored
}IgnoredAny combined with skip_deserializing avoids parsing unnecessary data.
use serde::de::{IgnoredAny, SeqAccess, Visitor};
use serde::Deserialize;
use std::fmt;
#[derive(Debug)]
struct FirstThree(Vec<i32>);
impl<'de> Deserialize<'de> for FirstThree {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct FirstThreeVisitor;
impl<'de> Visitor<'de> for FirstThreeVisitor {
type Value = FirstThree;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a sequence")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut result = Vec::new();
// Take first three elements
for _ in 0..3 {
if let Some(elem) = seq.next_element::<i32>()? {
result.push(elem);
}
}
// Ignore remaining elements
while seq.next_element::<IgnoredAny>()?.is_some() {}
Ok(FirstThree(result))
}
}
deserializer.deserialize_seq(FirstThreeVisitor)
}
}
fn main() {
let json = r#"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"#;
let result: FirstThree = serde_json::from_str(json).unwrap();
println!("First three: {:?}", result.0);
}IgnoredAny can skip remaining elements in sequences.
use serde::de::IgnoredAny;
use serde::Deserialize;
// Approach 1: Default - unknown fields ignored
#[derive(Deserialize, Debug)]
struct DefaultBehavior {
name: String,
}
// Approach 2: Deny unknown - strict validation
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct StrictValidation {
name: String,
}
// Approach 3: IgnoredAny with flatten - explicit forward compatibility
#[derive(Deserialize, Debug)]
struct ForwardCompatible {
name: String,
#[serde(flatten)]
_extra: IgnoredAny,
}
// Approach 4: Capture unknown - debugging/inspection
#[derive(Deserialize, Debug)]
struct CaptureExtras {
name: String,
#[serde(flatten)]
extra: std::collections::HashMap<String, serde_json::Value>,
}
fn main() {
let json = r#"{"name": "test", "unknown": "field"}"#;
// Default: unknown ignored
let d: DefaultBehavior = serde_json::from_str(json).unwrap();
println!("Default: {:?}", d);
// Strict: error on unknown
match serde_json::from_str::<StrictValidation>(json) {
Ok(v) => println!("Strict: {:?}", v),
Err(e) => println!("Strict error: {}", e),
}
// Forward compatible: explicitly ignored
let f: ForwardCompatible = serde_json::from_str(json).unwrap();
println!("Forward compatible: {:?}", f);
// Capture: store for inspection
let c: CaptureExtras = serde_json::from_str(json).unwrap();
println!("Captured: {:?}", c.extra);
}Choose the approach based on your validation and compatibility needs.
use serde::de::IgnoredAny;
use serde::Deserialize;
use std::collections::HashMap;
// API response type that handles version evolution
#[derive(Deserialize, Debug)]
struct ApiResponse<T> {
success: bool,
data: T,
error: Option<String>,
#[serde(flatten)]
_future: IgnoredAny,
}
#[derive(Deserialize, Debug)]
struct User {
id: u64,
username: String,
email: String,
#[serde(flatten)]
_future: IgnoredAny,
}
#[derive(Deserialize, Debug)]
struct ListResponse<T> {
items: Vec<T>,
total: u64,
#[serde(flatten)]
_future: IgnoredAny,
}
fn main() {
// API v2 adds pagination fields, v1 client ignores them
let json = r#"{
"success": true,
"data": {
"items": [
{"id": 1, "username": "alice", "email": "alice@example.com", "role": "admin"},
{"id": 2, "username": "bob", "email": "bob@example.com", "role": "user"}
],
"total": 2,
"page": 1,
"per_page": 10,
"has_more": false
},
"error": null,
"request_id": "req-123",
"timestamp": "2024-01-15T10:00:00Z"
}"#;
let response: ApiResponse<ListResponse<User>> = serde_json::from_str(json).unwrap();
println!("{:?}", response);
}IgnoredAny at multiple levels enables robust API client design.
IgnoredAny characteristics:
| Property | Value | |----------|-------| | Memory size | Zero bytes | | Storage | None (data consumed and discarded) | | Supported types | All Serde types (any valid JSON/data) | | Performance | Fast (parses without allocation) |
Forward compatibility approaches:
| Approach | Behavior | Use Case |
|----------|----------|----------|
| Default | Ignore unknown | Maximum compatibility |
| deny_unknown_fields | Error on unknown | Strict validation |
| IgnoredAny with flatten | Explicitly ignore | Documented compatibility |
| HashMap<String, Value> | Capture unknown | Debug/inspection |
When to use IgnoredAny:
| Scenario | Recommendation |
|----------|---------------|
| API client library | Use IgnoredAny for forward compatibility |
| Configuration parsing | Use IgnoredAny to support config evolution |
| Strict protocol validation | Use deny_unknown_fields instead |
| Debug unknown fields | Use HashMap<String, Value> instead |
| Custom deserializer | Use IgnoredAny for skipping |
Key insight: serde::de::IgnoredAny solves a fundamental versioning problem—how can code parse data without knowing all possible fields that might be present in future versions? Without IgnoredAny, clients face a choice between loose parsing (ignoring unknown fields by default, which provides no documentation of this behavior) or strict parsing (rejecting unknown fields, which breaks when APIs evolve). IgnoredAny makes forward compatibility explicit and intentional: a struct declares that it expects known fields and will silently ignore everything else. The zero-sized type means no memory overhead; the data is parsed just enough to skip it correctly (tracking nested structures, arrays, strings) without storing any values. This enables API designs where newer servers can send additional fields to older clients without breaking them, configuration files can gain new options without requiring parser updates, and protocols can evolve while maintaining interoperability. The pattern of #[serde(flatten)] _extra: IgnoredAny documents the intent clearly: "this struct accepts unknown fields and ignores them." For library authors, this means clients built against version 1.0 of an API continue working when the server adds fields in version 1.1, 1.2, and beyond—the unknown fields flow through IgnoredAny and disappear, leaving only the fields the client understands.