How does serde::de::DeserializeSeed enable parameterized deserialization with external context?
DeserializeSeed is a trait that separates the deserialization logic from the data being deserialized, allowing external context to be passed into the deserialization process. Unlike the standard Deserialize trait which creates values purely from the input data, DeserializeSeed implementations carry additional state that can influence how values are constructed. This enables patterns like deserializing references into a shared arena, validating values against external configuration, or resolving string identifiers to existing objects. The seed pattern is essential when deserialization needs information beyond what's in the serialized format.
The Problem: Deserialization Without Context
use serde::Deserialize;
// Standard Deserialize only sees the input data
#[derive(Deserialize, Debug)]
struct User {
id: u64,
name: String,
role: String, // We want to resolve this to a Role enum
}
#[derive(Debug)]
enum Role {
Admin,
User,
Guest,
}
// Problem: How do we validate "role" against known roles?
// Standard Deserialize has no access to external context
fn standard_deserialize() {
let json = r#"{"id": 1, "name": "Alice", "role": "admin"}"#;
let user: User = serde_json::from_str(json).unwrap();
// We got a String, but we want a validated Role
// We'd need a second validation step
}Standard Deserialize can only use information present in the input data.
DeserializeSeed Trait Overview
use serde::de::{DeserializeSeed, Deserializer, ValueSeed};
// 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>;
// }
// Key difference from Deserialize:
// - Deserialize: fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
// - DeserializeSeed: fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
// ^^^^
// self is consumed, can carry state!
// This means DeserializeSeed can:
// 1. Carry external context (via self)
// 2. Return a different type than Self
// 3. Influence deserialization based on configurationThe key insight: DeserializeSeed::deserialize takes self, allowing state to travel into deserialization.
Basic Example: Validating Against Known Values
use serde::de::{DeserializeSeed, Deserializer, Unexpected, Visitor};
use serde::de::Error;
use std::collections::HashSet;
// A seed that validates strings against a known set
struct ValidatedStringSeed<'a> {
allowed_values: &'a HashSet<String>,
}
impl<'de> DeserializeSeed<'de> for ValidatedStringSeed<'_> {
type Value = String;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Custom visitor that validates the string
struct ValidatedStringVisitor<'a> {
allowed_values: &'a HashSet<String>,
}
impl<'de> Visitor<'de> for ValidatedStringVisitor<'_> {
type Value = String;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a string matching one of the allowed values")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
if self.allowed_values.contains(v) {
Ok(v.to_string())
} else {
Err(Error::invalid_value(Unexpected::Str(v), &self))
}
}
}
deserializer.deserialize_str(ValidatedStringVisitor {
allowed_values: self.allowed_values,
})
}
}
fn validate_with_seed() {
let mut allowed = HashSet::new();
allowed.insert("admin".to_string());
allowed.insert("user".to_string());
allowed.insert("guest".to_string());
let json = r#""admin""#;
let seed = ValidatedStringSeed { allowed_values: &allowed };
let result: String = serde_json::from_str_with_seed(json, seed).unwrap();
println!("Validated: {}", result); // "admin"
// Invalid value fails
let json = r#""unknown""#;
let seed = ValidatedStringSeed { allowed_values: &allowed };
let result = serde_json::from_str_with_seed::<String, _>(json, seed);
assert!(result.is_err());
}The seed carries allowed_values context into deserialization.
Resolving References: The Arena Pattern
use serde::de::{DeserializeSeed, Deserializer, Visitor, MapAccess, Error};
use std::collections::HashMap;
use std::sync::Arc;
// An arena holding shared values
struct Arena {
strings: HashMap<String, Arc<String>>,
}
impl Arena {
fn new() -> Self {
Self { strings: HashMap::new() }
}
fn intern(&mut self, s: &str) -> Arc<String> {
self.strings
.get(s)
.cloned()
.unwrap_or_else(|| {
let arc = Arc::new(s.to_string());
self.strings.insert(s.to_string(), arc.clone());
arc
})
}
}
// Seed that interns strings into an arena
struct InternedStringSeed<'a> {
arena: &'a mut Arena,
}
impl<'de> DeserializeSeed<'de> for InternedStringSeed<'_> {
type Value = Arc<String>;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
struct InternVisitor<'a> {
arena: &'a mut Arena,
}
impl<'de> Visitor<'de> for InternVisitor<'_> {
type Value = Arc<String>;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(self.arena.intern(v))
}
}
deserializer.deserialize_str(InternVisitor { arena: self.arena })
}
}
fn arena_example() {
let mut arena = Arena::new();
let json = r#""hello""#;
let seed = InternedStringSeed { arena: &mut arena };
let s1: Arc<String> = serde_json::from_str_with_seed(json, seed).unwrap();
// Reuse the same arena for subsequent strings
let json = r#""hello""#;
let seed = InternedStringSeed { arena: &mut arena };
let s2: Arc<String> = serde_json::from_str_with_seed(json, seed).unwrap();
// s1 and s2 point to the same allocation
assert!(Arc::ptr_eq(&s1, &s2));
}The seed provides mutable access to an arena, allowing string interning during deserialization.
Deserializing into Existing Collections
use serde::de::{DeserializeSeed, Deserializer, Visitor, SeqAccess};
use std::collections::BTreeMap;
// Seed that looks up IDs in a registry
struct IdLookupSeed<'a, T> {
registry: &'a BTreeMap<u64, T>,
_marker: std::marker::PhantomData<T>,
}
impl<'de, T: Clone> DeserializeSeed<'de> for IdLookupSeed<'_, T> {
type Value = T;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
where
D: Deserializer<'de>,
{
struct LookupVisitor<'a, T> {
registry: &'a BTreeMap<u64, T>,
_marker: std::marker::PhantomData<T>,
}
impl<'de, T: Clone> Visitor<'de> for LookupVisitor<'_, T> {
type Value = T;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a valid ID")
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.registry
.get(&v)
.cloned()
.ok_or_else(|| E::custom(format!("unknown id: {}", v)))
}
}
deserializer.deserialize_u64(LookupVisitor {
registry: self.registry,
_marker: std::marker::PhantomData,
})
}
}The seed carries a registry that maps IDs to values, enabling reference resolution.
Combining with Standard Deserialize
use serde::de::{DeserializeSeed, Deserializer, Visitor, MapAccess};
use serde::Deserialize;
use std::collections::HashMap;
// A config that affects deserialization
#[derive(Debug, Clone)]
struct DeserializationConfig {
default_role: String,
max_name_length: usize,
}
#[derive(Debug)]
struct ConfiguredUser {
id: u64,
name: String,
role: String,
}
// Seed that uses config to provide defaults
struct ConfiguredUserSeed<'a> {
config: &'a DeserializationConfig,
}
impl<'de> DeserializeSeed<'de> for ConfiguredUserSeed<'_> {
type Value = ConfiguredUser;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Custom visitor that applies config
struct ConfiguredUserVisitor<'a> {
config: &'a DeserializationConfig,
}
impl<'de> Visitor<'de> for ConfiguredUserVisitor<'_> {
type Value = ConfiguredUser;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a user object")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut id = None;
let mut name = None;
let mut role = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"id" => id = Some(map.next_value()?),
"name" => {
let n: String = map.next_value()?;
if n.len() > self.config.max_name_length {
return Err(serde::de::Error::custom("name too long"));
}
name = Some(n);
}
"role" => role = Some(map.next_value()?),
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
}
}
Ok(ConfiguredUser {
id: id.ok_or_else(|| serde::de::Error::missing_field("id"))?,
name: name.ok_or_else(|| serde::de::Error::missing_field("name"))?,
role: role.unwrap_or_else(|| self.config.default_role.clone()),
})
}
}
deserializer.deserialize_map(ConfiguredUserVisitor {
config: self.config,
})
}
}
fn configured_deserialize() {
let config = DeserializationConfig {
default_role: "user".to_string(),
max_name_length: 50,
};
// Missing role uses default
let json = r#"{"id": 1, "name": "Alice"}"#;
let seed = ConfiguredUserSeed { config: &config };
let user: ConfiguredUser = serde_json::from_str_with_seed(json, seed).unwrap();
println!("User: {:?}", user); // role is "user"
// Name too long fails
let json = r#"{"id": 2, "name": "A very long name that exceeds the maximum allowed length"}"#;
let seed = ConfiguredUserSeed { config: &config };
let result = serde_json::from_str_with_seed::<ConfiguredUser, _>(json, seed);
assert!(result.is_err());
}The seed provides configuration that affects validation and defaults.
Seed for Deserializing Recursive Structures
use serde::de::{DeserializeSeed, Deserializer, Visitor, SeqAccess};
use std::collections::HashMap;
// A registry that can resolve references
#[derive(Debug, Clone)]
enum Expr {
Literal(i64),
Add(Box<Expr>, Box<Expr>),
Var(String),
}
struct ExprSeed<'a> {
vars: &'a HashMap<String, i64>,
}
impl<'de> DeserializeSeed<'de> for ExprSeed<'_> {
type Value = Expr;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
struct ExprVisitor<'a> {
vars: &'a HashMap<String, i64>,
}
impl<'de> Visitor<'de> for ExprVisitor<'_> {
type Value = Expr;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "an expression")
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
Ok(Expr::Literal(v))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
// Variable reference - resolve or keep as symbolic
if self.vars.contains_key(v) {
Ok(Expr::Literal(self.vars[v]))
} else {
Ok(Expr::Var(v.to_string()))
}
}
fn visit_seq<M>(self, mut seq: M) -> Result<Self::Value, M::Error>
where
M: SeqAccess<'de>,
{
// [left, right] for Add
let left_seed = ExprSeed { vars: self.vars };
let left = seq.next_element_seed(left_seed)?
.ok_or_else(|| serde::de::Error::custom("expected left operand"))?;
let right_seed = ExprSeed { vars: self.vars };
let right = seq.next_element_seed(right_seed)?
.ok_or_else(|| serde::de::Error::custom("expected right operand"))?;
Ok(Expr::Add(Box::new(left), Box::new(right)))
}
}
deserializer.deserialize_any(ExprVisitor { vars: self.vars })
}
}
fn recursive_seed() {
let mut vars = HashMap::new();
vars.insert("x".to_string(), 10);
vars.insert("y".to_string(), 5);
// Resolve a variable
let json = r#""x""#;
let seed = ExprSeed { vars: &vars };
let expr: Expr = serde_json::from_str_with_seed(json, seed).unwrap();
println!("Expr: {:?}", expr); // Literal(10)
// Build an expression tree
let json = r#"[["x", "y"], 5]"#; // (x + y) + 5
let seed = ExprSeed { vars: &vars };
let expr: Expr = serde_json::from_str_with_seed(json, seed).unwrap();
println!("Expr: {:?}", expr);
}Seeds can pass context recursively through nested structures using next_element_seed.
Using Seeds with serde_json
use serde::de::DeserializeSeed;
// serde_json provides from_str_with_seed, from_reader_with_seed, etc.
fn serde_json_integration() {
use serde_json::Deserializer;
use std::io::Cursor;
let json = r#"{"key": "value"}"#;
// With a seed:
// let seed = MySeed::new();
// let result: MyValue = serde_json::from_str_with_seed(json, seed)?;
// Or with a streaming deserializer:
// let cursor = Cursor::new(json);
// let mut deserializer = Deserializer::from_reader(cursor);
// let result = seed.deserialize(&mut deserializer)?;
// The seed pattern integrates with all serde_json entry points
}serde_json provides from_str_with_seed, from_reader_with_seed, etc. for seed-based deserialization.
Implementing Deserialize for Types with Seeds
use serde::de::{Deserialize, Deserializer, DeserializeSeed};
// Sometimes you want both Deserialize and DeserializeSeed
#[derive(Debug)]
struct User {
id: u64,
name: String,
}
impl<'de> Deserialize<'de> for User {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// Standard implementation without context
UserSeed.deserialize(deserializer)
}
}
// Seed for when you have context
struct UserSeed;
impl<'de> DeserializeSeed<'de> for UserSeed {
type Value = User;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Implementation can use self for context
// This seed doesn't need context, so just implement standard logic
struct UserVisitor;
impl<'de> serde::de::Visitor<'de> for UserVisitor {
type Value = User;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a user object")
}
fn visit_map<M>(self, mut map: M) -> Result<User, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let mut id = None;
let mut name = None;
while let Some(key) = map.next_key()? {
match key {
"id" => id = Some(map.next_value()?),
"name" => name = Some(map.next_value()?),
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
}
}
Ok(User {
id: id.ok_or_else(|| serde::de::Error::missing_field("id"))?,
name: name.ok_or_else(|| serde::de::Error::missing_field("name"))?,
})
}
}
deserializer.deserialize_map(UserVisitor)
}
}Implement DeserializeSeed even for types that also implement Deserialize.
Seed with Phantom Data for Generic Types
use serde::de::{DeserializeSeed, Deserializer, Visitor};
use std::marker::PhantomData;
// A seed that deserializes a value and then transforms it
struct TransformSeed<F, T, R> {
transform: F,
_marker: PhantomData<(T, R)>,
}
impl<'de, F, T, R> DeserializeSeed<'de> for TransformSeed<F, T, R>
where
F: FnOnce(T) -> R,
T: serde::Deserialize<'de>,
{
type Value = R;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let value: T = serde::Deserialize::deserialize(deserializer)?;
Ok((self.transform)(value))
}
}
fn transform_seed_example() {
// Parse a number and double it
let json = "42";
let seed = TransformSeed::<_, i32, i32> {
transform: |x| x * 2,
_marker: PhantomData,
};
let result: i32 = serde_json::from_str_with_seed(json, seed).unwrap();
assert_eq!(result, 84);
// Parse a string and uppercase it
let json = r#""hello""#;
let seed = TransformSeed::<_, String, String> {
transform: |s| s.to_uppercase(),
_marker: PhantomData,
};
let result: String = serde_json::from_str_with_seed(json, seed).unwrap();
assert_eq!(result, "HELLO");
}Use PhantomData to carry type information when the seed struct doesn't store the types.
Real-World Use Cases
// 1. String interning (reduce memory for repeated strings)
// - Pass arena reference via seed
// - All strings interned during deserialization
// 2. Reference resolution (IDs to actual objects)
// - Pass object registry via seed
// - IDs resolved to object references
// 3. Schema-aware deserialization
// - Pass schema information via seed
// - Deserialize based on schema version
// 4. Configuration-driven defaults
// - Pass config via seed
// - Apply defaults for missing fields
// 5. Validation with external rules
// - Pass validator/allowed values via seed
// - Fail deserialization on invalid input
// 6. Contextual type resolution
// - Pass type registry via seed
// - Deserialize tagged unions with type lookupSeeds enable deserialization that depends on external state or configuration.
Comparison with Alternatives
use serde::Deserialize;
// Alternative 1: Post-deserialization processing
#[derive(Deserialize)]
struct RawUser {
id: u64,
role_id: u64,
}
fn post_process() {
let registry = get_role_registry();
let raw: RawUser = serde_json::from_str("...").unwrap();
let role = registry.get(raw.role_id).cloned();
// Two-step process: deserialize then validate/transform
}
// Alternative 2: DeserializeSeed (one-step)
// - Validation happens during deserialization
// - Can fail early on invalid input
// - No intermediate representation needed
// Alternative 3: DeserializeOwned with global state
// - Uses lazy_static or thread_local
// - Harder to test, couples to global state
// - Seeds make dependencies explicit
fn seed_approach() {
let registry = get_role_registry();
let seed = RoleResolvingSeed { registry: ®istry };
// All-in-one: deserialize and resolve
}DeserializeSeed integrates transformation into the deserialization process.
Synthesis
Quick reference:
use serde::de::{DeserializeSeed, Deserializer, Visitor};
use std::collections::HashSet;
// Define a seed struct that carries context
struct ValidatedSeed<'a> {
allowed: &'a HashSet<String>,
}
// Implement DeserializeSeed
impl<'de> DeserializeSeed<'de> for ValidatedSeed<'_> {
type Value = String; // Can differ from seed type
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Use self.allowed for validation
// Return validated value
// ... implementation ...
todo!()
}
}
// Use with serde_json or other serde formats
fn usage() {
let allowed = HashSet::from(["a".to_string(), "b".to_string()]);
let seed = ValidatedSeed { allowed: &allowed };
let result: String = serde_json::from_str_with_seed("\"a\"", seed).unwrap();
}
// Key patterns:
// 1. Seed carries context (references, configs, registries)
// 2. deserialize takes self, allowing state
// 3. Value type can differ from seed type
// 4. Works with nested structures via next_element_seed
// 5. Integrates with all serde deserializersKey insight: DeserializeSeed bridges the gap between pure data deserialization and context-aware value construction. The standard Deserialize trait is self-containedāthe type determines everything about how it deserializes. This works for data that's complete in the serialized form, but breaks down when deserialization needs external information: reference resolution requires an object registry, string interning needs an arena, validation may need allowed-value sets, and defaults depend on configuration. DeserializeSeed solves this by making the deserialization logic a first-class value that carries context. The self parameter in deserialize is the crucial differenceāit means you create a seed instance with whatever state is needed, then call deserialize on it. The seed can pass itself recursively through nested structures, maintaining context throughout the entire deserialization process. Use DeserializeSeed when you need to inject external context into deserialization, when values must be validated or transformed against external rules, or when deserialized objects need to reference existing data structures.
