How does serde::de::DeserializeSeed enable context-aware deserialization with external state injection?
serde::de::DeserializeSeed is a trait that separates the deserialization "recipe" from the deserialization "target", allowing external state to be passed into the deserialization processāenabling patterns like deserializing references to already-existing data, resolving IDs to objects, and injecting configuration without modifying the data types themselves. Unlike the standard Deserialize trait which is self-contained, DeserializeSeed carries a seed value that travels through the entire deserialization process, providing context at every level.
Basic DeserializeSeed Definition
use serde::de::{Deserialize, DeserializeSeed, Deserializer};
// The DeserializeSeed trait
pub trait DeserializeSeed<'de>: Sized {
type Value;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>;
}
// Compare with Deserialize trait
pub trait Deserialize<'de>: Sized {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>;
}DeserializeSeed takes self and can carry context; Deserialize is static.
Simple Seed Example: Injecting External State
use serde::de::{Deserialize, DeserializeSeed, Deserializer, Visitor, Error};
use serde::Deserializer as SerdeDeserializer;
use std::collections::HashMap;
// A registry that maps IDs to objects
struct Registry {
users: HashMap<u64, String>,
}
// Seed that resolves user IDs to names
struct UserIdSeed<'a> {
registry: &'a Registry,
}
impl<'de> DeserializeSeed<'de> for UserIdSeed<'_> {
type Value = String;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Deserialize the ID, then resolve it
let id: u64 = Deserialize::deserialize(deserializer)?;
self.registry.users.get(&id)
.cloned()
.ok_or_else(|| D::Error::custom(format!("Unknown user ID: {}", id)))
}
}
fn resolve_user_id() {
let mut registry = Registry { users: HashMap::new() };
registry.users.insert(1, "Alice".to_string());
registry.users.insert(2, "Bob".to_string());
let seed = UserIdSeed { registry: ®istry };
let json = r#"1"#;
let name: String = seed.deserialize(&mut serde_json::Deserializer::from_str(json)).unwrap();
println!("Resolved: {}", name); // "Alice"
}The seed carries the registry context into deserialization.
Why DeserializeSeed Exists
use serde::Deserialize;
// Problem: How to inject context without modifying the type?
#[derive(Deserialize)]
struct Document {
author_id: u64, // We want to resolve this to a String
content: String,
}
// Option 1: Deserialize twice (inefficient)
// Option 2: Store ID and resolve later (loses information at type level)
// Option 3: Custom Deserialize implementation (verbose, repetitive)
// Option 4: DeserializeSeed (clean separation of concerns)
// DeserializeSeed allows:
// - Injecting configuration
// - Resolving references
// - Providing default values
// - Validating with external context
// - Creating cyclic referencesDeserializeSeed solves the context injection problem elegantly.
Seed for Collections
use serde::de::{DeserializeSeed, Deserializer, SeqAccess, Visitor};
use std::collections::HashMap;
struct Registry {
items: HashMap<String, Item>,
}
#[derive(Clone)]
struct Item {
name: String,
value: i32,
}
// Seed for deserializing item references
struct ItemRefSeed<'a> {
registry: &'a Registry,
}
impl<'de> DeserializeSeed<'de> for ItemRefSeed<'_> {
type Value = Item;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let name: String = Deserialize::deserialize(deserializer)?;
self.registry.items.get(&name)
.cloned()
.ok_or_else(|| serde::de::Error::custom(format!("Unknown item: {}", name)))
}
}
// Seed for deserializing a Vec of item references
struct ItemRefVecSeed<'a> {
registry: &'a Registry,
}
impl<'de> DeserializeSeed<'de> for ItemRefVecSeed<'_> {
type Value = Vec<Item>;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_seq(ItemRefVecVisitor { registry: self.registry })
}
}
struct ItemRefVecVisitor<'a> {
registry: &'a Registry,
}
impl<'de> Visitor<'de> for ItemRefVecVisitor<'_> {
type Value = Vec<Item>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a sequence of item names")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut items = Vec::new();
while let Some(item) = seq.next_element_seed(ItemRefSeed { registry: self.registry })? {
items.push(item);
}
Ok(items)
}
}Collections require the seed to be passed to each element.
Implementing DeserializeSeed for Structs
use serde::de::{DeserializeSeed, Deserializer, Visitor, MapAccess};
use std::marker::PhantomData;
// A struct that needs context to deserialize
struct ProcessedDocument {
author: String, // Resolved from ID
content: String,
}
// Context for deserialization
struct Context {
user_names: HashMap<u64, String>,
}
// Seed for deserializing ProcessedDocument
struct ProcessedDocumentSeed<'a> {
context: &'a Context,
}
impl<'de> DeserializeSeed<'de> for ProcessedDocumentSeed<'_> {
type Value = ProcessedDocument;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(ProcessedDocumentVisitor {
context: self.context,
author_id: None,
content: None,
})
}
}
struct ProcessedDocumentVisitor<'a> {
context: &'a Context,
author_id: Option<u64>,
content: Option<String>,
}
impl<'de> Visitor<'de> for ProcessedDocumentVisitor<'_> {
type Value = ProcessedDocument;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a document object")
}
fn visit_map<M>(mut self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"author_id" => {
self.author_id = Some(map.next_value()?);
}
"content" => {
self.content = Some(map.next_value()?);
}
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
let author_id = self.author_id.ok_or_else(||
serde::de::Error::missing_field("author_id"))?;
let content = self.content.ok_or_else(||
serde::de::Error::missing_field("content"))?;
let author = self.context.user_names.get(&author_id)
.cloned()
.ok_or_else(|| serde::de::Error::custom("Unknown author ID"))?;
Ok(ProcessedDocument { author, content })
}
}Struct deserialization requires passing the seed through the visitor.
Using seq_next_element_seed for Collections
use serde::de::{DeserializeSeed, Deserializer, SeqAccess, Visitor};
// Seed for a list of references
struct ReferenceListSeed<'a, T> {
resolver: &'a Resolver<T>,
}
struct Resolver<T> {
items: HashMap<String, T>,
}
impl<'de, T: Clone + 'static> DeserializeSeed<'de> for ReferenceListSeed<'_, T> {
type Value = Vec<T>;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Use a visitor that deserializes a sequence
deserializer.deserialize_seq(ReferenceListVisitor {
resolver: self.resolver,
_phantom: PhantomData,
})
}
}
struct ReferenceListVisitor<'a, T> {
resolver: &'a Resolver<T>,
_phantom: PhantomData<T>,
}
impl<'de, T: Clone> Visitor<'de> for ReferenceListVisitor<'_, T> {
type Value = Vec<T>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a sequence of reference keys")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut items = Vec::new();
// Key: use next_element_seed to pass seed to each element
while let Some(key) = seq.next_element::<String>()? {
let item = self.resolver.items.get(&key)
.cloned()
.ok_or_else(|| serde::de::Error::custom(format!("Unknown key: {}", key)))?;
items.push(item);
}
Ok(items)
}
}Use next_element_seed when elements need the seed context.
Seeding Nested Structures
use serde::de::{DeserializeSeed, Deserializer, MapAccess, Visitor};
use std::collections::HashMap;
struct AppContext {
base_url: String,
user_cache: HashMap<u64, String>,
}
#[derive(Debug)]
struct ApiRequest {
endpoint: String, // Will be prefixed with base_url
user: String, // Resolved from user_id
}
struct ApiRequestSeed<'a> {
context: &'a AppContext,
}
impl<'de> DeserializeSeed<'de> for ApiRequestSeed<'_> {
type Value = ApiRequest;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(ApiRequestVisitor {
context: self.context,
endpoint: None,
user_id: None,
})
}
}
struct ApiRequestVisitor<'a> {
context: &'a AppContext,
endpoint: Option<String>,
user_id: Option<u64>,
}
impl<'de> Visitor<'de> for ApiRequestVisitor<'_> {
type Value = ApiRequest;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "an API request object")
}
fn visit_map<M>(mut self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"endpoint" => {
self.endpoint = Some(map.next_value()?);
}
"user_id" => {
self.user_id = Some(map.next_value()?);
}
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
let endpoint = self.endpoint.ok_or_else(||
serde::de::Error::missing_field("endpoint"))?;
let user_id = self.user_id.ok_or_else(||
serde::de::Error::missing_field("user_id"))?;
// Inject context: prefix endpoint with base_url
let full_endpoint = format!("{}/{}", self.context.base_url, endpoint);
// Inject context: resolve user_id to name
let user = self.context.user_cache.get(&user_id)
.cloned()
.ok_or_else(|| serde::de::Error::custom("Unknown user"))?;
Ok(ApiRequest {
endpoint: full_endpoint,
user,
})
}
}
fn seeded_deserialization() {
let mut context = AppContext {
base_url: "https://api.example.com".to_string(),
user_cache: HashMap::new(),
};
context.user_cache.insert(1, "Alice".to_string());
let json = r#"{"endpoint": "users", "user_id": 1}"#;
let seed = ApiRequestSeed { context: &context };
let request: ApiRequest = seed.deserialize(
&mut serde_json::Deserializer::from_str(json)
).unwrap();
println!("{:?}", request);
// ApiRequest { endpoint: "https://api.example.com/users", user: "Alice" }
}Nested structures carry context through the entire deserialization.
Default Values via Seed
use serde::de::{DeserializeSeed, Deserializer};
struct DefaultValueSeed<'a, T> {
default: &'a T,
}
impl<'de, T: Clone + serde::de::DeserializeOwned> DeserializeSeed<'de> for DefaultValueSeed<'_, T> {
type Value = T;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// If deserialization fails, return default
match T::deserialize(deserializer) {
Ok(value) => Ok(value),
Err(_) => Ok(self.default.clone()),
}
}
}
// Or provide defaults for missing fields
struct OptionalFieldSeed<'a> {
default_value: &'a str,
}
impl<'de> DeserializeSeed<'de> for OptionalFieldSeed<'_> {
type Value = String;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Use serde_json::Value to handle any type
let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
match value {
Some(serde_json::Value::String(s)) => Ok(s),
Some(_) => Err(serde::de::Error::custom("Expected string")),
None => Ok(self.default_value.to_string()),
}
}
}Seeds can provide default values when fields are missing or invalid.
Validation with Context
use serde::de::{DeserializeSeed, Deserializer, Error};
struct ValidatedIdSeed<'a> {
valid_ids: &'a [u64],
}
impl<'de> DeserializeSeed<'de> for ValidatedIdSeed<'_> {
type Value = u64;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let id: u64 = Deserialize::deserialize(deserializer)?;
if self.valid_ids.contains(&id) {
Ok(id)
} else {
Err(D::Error::custom(format!("Invalid ID: {}", id)))
}
}
}
fn validation_example() {
let valid_ids = [1, 2, 3, 4, 5];
let seed = ValidatedIdSeed { valid_ids: &valid_ids };
let json = "3";
let id: u64 = seed.deserialize(&mut serde_json::Deserializer::from_str(json)).unwrap();
println!("Valid ID: {}", id);
let json = "99";
let result: Result<u64, _> = seed.deserialize(&mut serde_json::Deserializer::from_str(json));
assert!(result.is_err());
}Seeds enable validation using external context.
Compare with Standard Deserialize
use serde::Deserialize;
// Standard Deserialize: no context
#[derive(Deserialize)]
struct SimpleDocument {
id: u64,
title: String,
}
// To resolve references, you'd need to:
// 1. Deserialize IDs first
// 2. Then resolve in a separate pass
// With DeserializeSeed: context in one pass
struct ResolvedDocument {
id: u64,
title: String,
author: String, // Resolved from author_id
}
struct ResolvedDocumentSeed<'a> {
user_lookup: &'a dyn Fn(u64) -> Option<String>,
}
impl<'de> DeserializeSeed<'de> for ResolvedDocumentSeed<'_> {
type Value = ResolvedDocument;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Custom deserialization that uses user_lookup
// ... visitor implementation
}
}Deserialize is static; DeserializeSeed carries context.
Practical Example: Dependency Injection
use serde::de::{DeserializeSeed, Deserializer, MapAccess, Visitor};
use std::collections::HashMap;
// A service registry
struct ServiceRegistry {
services: HashMap<String, Box<dyn std::any::Any>>,
}
// A configuration that references services
struct Config {
database: Box<dyn Database>,
cache: Box<dyn Cache>,
}
trait Database: std::fmt::Debug {}
trait Cache: std::fmt::Debug {}
#[derive(Debug)]
struct PostgresDatabase;
impl Database for PostgresDatabase {}
#[derive(Debug)]
struct RedisCache;
impl Cache for RedisCache {}
// Seed that resolves service references
struct ConfigSeed<'a> {
registry: &'a ServiceRegistry,
}
impl<'de> DeserializeSeed<'de> for ConfigSeed<'_> {
type Value = Config;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Deserialize service names, then resolve from registry
// This is a simplified example - real implementation would
// properly deserialize and downcast
todo!()
}
}Seeds enable dependency injection during deserialization.
Working with Generics
use serde::de::{DeserializeSeed, Deserializer};
use std::marker::PhantomData;
// Generic seed that wraps any type
struct WrappingSeed<T, S> {
_phantom: PhantomData<(T, S)>,
}
// Seed that deserializes a reference by ID
struct ReferenceSeed<'a, T> {
resolver: &'a dyn Fn(&str) -> Option<T>,
}
impl<'de, T: Clone> DeserializeSeed<'de> for ReferenceSeed<'_, T> {
type Value = T;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let key: String = Deserialize::deserialize(deserializer)?;
(self.resolver)(&key)
.ok_or_else(|| serde::de::Error::custom(format!("Key not found: {}", key)))
}
}
// Using generic seeds
fn generic_example() {
let items: HashMap<String, i32> = [("key1", 42), ("key2", 100)]
.into_iter().map(|(k, v)| (k.to_string(), v)).collect();
let resolver = |key: &str| items.get(key).cloned();
let seed = ReferenceSeed { resolver: &resolver };
let json = r#""key1""#;
let value: i32 = seed.deserialize(&mut serde_json::Deserializer::from_str(json)).unwrap();
println!("Resolved value: {}", value); // 42
}Generic seeds work with any resolvable type.
Seed for Recursive Structures
use serde::de::{DeserializeSeed, Deserializer, MapAccess, Visitor};
use std::cell::RefCell;
use std::rc::Rc;
// A tree structure with shared nodes
#[derive(Debug)]
struct TreeNode {
name: String,
children: Vec<Rc<RefCell<TreeNode>>>,
}
// Context for building nodes with sharing
struct TreeNodeSeed<'a> {
node_cache: &'a RefCell<HashMap<String, Rc<RefCell<TreeNode>>>>,
}
impl<'de> DeserializeSeed<'de> for TreeNodeSeed<'_> {
type Value = Rc<RefCell<TreeNode>>;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Deserialize node, using cache for shared references
// This enables deserializing cyclic/shared structures
deserializer.deserialize_map(TreeNodeVisitor {
node_cache: self.node_cache,
})
}
}
struct TreeNodeVisitor<'a> {
node_cache: &'a RefCell<HashMap<String, Rc<RefCell<TreeNode>>>>,
}
impl<'de> Visitor<'de> for TreeNodeVisitor<'_> {
type Value = Rc<RefCell<TreeNode>>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a tree node")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut name = None;
let mut children: Vec<String> = Vec::new();
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"name" => name = Some(map.next_value()?),
"children" => children = map.next_value()?,
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
}
}
let name = name.ok_or_else(|| serde::de::Error::missing_field("name"))?;
// Check cache first
if let Some(existing) = self.node_cache.borrow().get(&name) {
return Ok(existing.clone());
}
// Create node (without children yet)
let node = Rc::new(RefCell::new(TreeNode {
name: name.clone(),
children: Vec::new(),
}));
// Add to cache before resolving children (handles cycles)
self.node_cache.borrow_mut().insert(name, node.clone());
// Resolve children references would go here...
Ok(node)
}
}Seeds enable shared references and cycles during deserialization.
Summary Table
use serde::de::DeserializeSeed;
fn summary() {
// | Aspect | Deserialize | DeserializeSeed |
// |---------------------|----------------------|----------------------|
// | Self parameter | None (static) | &mut self |
// | External context | No | Yes (via self) |
// | Implementation | derive macro | Manual |
// | Use case | Standard types | Context-dependent |
// | Typical pattern | Data-only | Reference resolution |
// | Performance | Slightly faster | Extra indirection |
}Choose based on whether you need external context.
Synthesis
Quick reference:
use serde::de::{DeserializeSeed, Deserializer};
// Basic seed that carries context
struct MySeed<'a> {
context: &'a Context,
}
impl<'de> DeserializeSeed<'de> for MySeed<'_> {
type Value = MyType;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Use self.context during deserialization
let raw: RawType = Deserialize::deserialize(deserializer)?;
// Transform using context
Ok(transform(raw, self.context))
}
}
// Usage
let seed = MySeed { context: &context };
let value: MyType = seed.deserialize(&mut deserializer)?;When to use DeserializeSeed:
// Use DeserializeSeed when:
// - Resolving ID references to actual objects
// - Injecting configuration during deserialization
// - Validating against external state
// - Providing default values from context
// - Building cyclic/shared structures
// - Dependency injection during parsing
// Use Deserialize when:
// - Self-contained data types
// - No external dependencies
// - Standard derive macro works
// - Simple validation (no context needed)Key insight: DeserializeSeed fills a critical gap in serde's design by allowing deserialization to depend on external state. The standard Deserialize trait is intentionally self-containedāimplementations cannot access anything beyond the data stream itself. This is correct for most cases, but breaks down when deserialized data contains references (by ID, path, or key) to external objects, or when values need validation against runtime configuration. DeserializeSeed solves this by treating the deserialization "recipe" as a value that can carry context. The seed's deserialize method takes self, allowing it to capture references, configuration, or callbacks that travel through the entire deserialization process. For collections and nested structures, the seed must be passed to each sub-element via next_element_seed and next_value_seed methods on SeqAccess and MapAccess. This pattern enables powerful use cases like resolving { "user_id": 42 } to { "user": User { name: "Alice", ... } } without modifying the data format or requiring post-processing passesāall while maintaining serde's streaming, zero-copy deserialization guarantees.
