How does serde::de::SeqAccess::next_element_seed enable context-aware sequence deserialization?

next_element_seed deserializes sequence elements using a seed value that provides external context, allowing element deserialization to depend on information not present in the serialized data itself. This mechanism enables patterns like deserializing references by ID, resolving dependencies between objects, and maintaining graph connectivity during deserialization.

The Basic SeqAccess Interface

use serde::de::{SeqAccess, Visitor, Deserialize};
use std::fmt;
 
// The SeqAccess trait provides two ways to read elements:
trait SeqAccess<'de> {
    // Simple: deserialize next element using its own Deserialize impl
    fn next_element<T>(&mut self) -> Result<Option<T>, Self::Error>
    where T: Deserialize<'de>;
    
    // Seeded: deserialize next element with a seed providing context
    fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
    where T: DeserializeSeed<'de>;
}
 
// The simple method is sufficient when elements contain all needed data:
fn simple_deserialize<'de, A>(mut seq: A) -> Result<Vec<i32>, A::Error>
where A: SeqAccess<'de>
{
    let mut result = Vec::new();
    while let Some(value) = seq.next_element::<i32>()? {
        result.push(value);
    }
    Ok(result)
}

The simple next_element works when elements are self-contained. Seeds are needed when elements require external context.

What is DeserializeSeed?

use serde::de::{DeserializeSeed, Deserializer, Visitor};
use std::fmt;
 
// DeserializeSeed is like Deserialize, but takes external context:
pub trait DeserializeSeed<'de>: Sized {
    // The type produced by this seed
    type Value;
    
    // Deserialize using the seed as context
    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
    where D: Deserializer<'de>;
}
 
// Example: A seed that knows the expected type name
struct TypeNameSeed(&'static str);
 
impl<'de> DeserializeSeed<'de> for TypeNameSeed {
    type Value = String;
    
    fn deserialize<D>(self, deserializer: D) -> Result<String, D::Error>
    where D: Deserializer<'de>
    {
        // The seed provides context (the type name)
        struct TypeNameVisitor(&'static str);
        
        impl<'de> Visitor<'de> for TypeNameVisitor {
            type Value = String;
            
            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "a {} value", self.0)
            }
            
            fn visit_str<E>(self, v: &str) -> Result<String, E>
            where E: serde::de::Error
            {
                Ok(v.to_string())
            }
        }
        
        deserializer.deserialize_str(TypeNameVisitor(self.0))
    }
}

DeserializeSeed implementations carry context that influences how they deserialize.

Using next_element_seed for Context

use serde::de::{SeqAccess, DeserializeSeed, Visitor, Error};
use std::collections::HashMap;
use std::fmt;
 
// Scenario: Deserialize IDs that reference objects in a registry
struct Registry {
    objects: HashMap<u64, String>,
}
 
// Seed that resolves IDs from the registry
struct ResolveIdSeed<'a> {
    registry: &'a Registry,
}
 
impl<'de> DeserializeSeed<'de> for ResolveIdSeed<'_> {
    type Value = String;
    
    fn deserialize<D>(self, deserializer: D) -> Result<String, D::Error>
    where D: serde::Deserializer<'de>
    {
        struct IdVisitor<'a>(&'a Registry);
        
        impl<'de> Visitor<'de> for IdVisitor<'_> {
            type Value = String;
            
            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "an object ID")
            }
            
            fn visit_u64<E>(self, id: u64) -> Result<String, E>
            where E: Error
            {
                self.0.objects.get(&id)
                    .cloned()
                    .ok_or_else(|| E::custom(format!("Unknown ID: {}", id)))
            }
        }
        
        deserializer.deserialize_u64(IdVisitor(self.registry))
    }
}
 
// Deserializing a sequence of IDs into resolved names
fn resolve_ids<'de, A>(seq: &mut A, registry: &Registry) -> Result<Vec<String>, A::Error>
where A: SeqAccess<'de>
{
    let mut result = Vec::new();
    let seed = ResolveIdSeed { registry };
    
    // Use the same seed for each element
    while let Some(name) = seq.next_element_seed(seed)? {
        result.push(name);
    }
    
    Ok(result)
}

The seed provides registry access to each element, enabling ID resolution during deserialization.

Varying Seeds Per Element

use serde::de::{SeqAccess, DeserializeSeed, Deserializer, Visitor};
use std::fmt;
 
// Scenario: Each element has a different expected type
struct TypeAwareSeed {
    expected_type: &'static str,
}
 
impl<'de> DeserializeSeed<'de> for TypeAwareSeed {
    type Value = serde_json::Value;
    
    fn deserialize<D>(self, deserializer: D) -> Result<serde_json::Value, D::Error>
    where D: Deserializer<'de>
    {
        // Validate the deserialized value matches expected type
        struct TypeVisitor(&'static str);
        
        impl<'de> Visitor<'de> for TypeVisitor {
            type Value = serde_json::Value;
            
            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "a {} value", self.0)
            }
            
            fn visit_i64<E>(self, v: i64) -> Result<serde_json::Value, E>
            where E: serde::de::Error
            {
                if self.0 == "int" {
                    Ok(serde_json::Value::Number(v.into()))
                } else {
                    Err(E::invalid_type(serde::de::Unexpected::Signed(v), &self))
                }
            }
            
            fn visit_str<E>(self, v: &str) -> Result<serde_json::Value, E>
            where E: serde::de::Error
            {
                if self.0 == "string" {
                    Ok(serde_json::Value::String(v.to_string()))
                } else {
                    Err(E::invalid_type(serde::de::Unexpected::Str(v), &self))
                }
            }
        }
        
        deserializer.deserialize_any(TypeVisitor(self.expected_type))
    }
}
 
// Different seeds for different positions
fn positional_deserialize<'de, A>(seq: &mut A) -> Result<Vec<serde_json::Value>, A::Error>
where A: SeqAccess<'de>
{
    let mut result = Vec::new();
    let types = ["int", "string", "int", "string"];
    
    for expected_type in types {
        let seed = TypeAwareSeed { expected_type };
        match seq.next_element_seed(seed)? {
            Some(value) => result.push(value),
            None => break,
        }
    }
    
    Ok(result)
}

Different seeds can be used for each element position, enabling positional type expectations.

Graph Deserialization with Seeds

use serde::de::{SeqAccess, DeserializeSeed, Deserializer, Visitor, MapAccess, Error};
use std::collections::HashMap;
use std::fmt;
 
// A graph where nodes reference each other by ID
#[derive(Debug)]
struct Node {
    id: u64,
    name: String,
    references: Vec<u64>,  // IDs of referenced nodes
}
 
// Context for deserialization
struct GraphContext {
    nodes: HashMap<u64, Node>,
}
 
// First pass: deserialize nodes without resolving references
struct NodeSeed;
 
impl<'de> DeserializeSeed<'de> for NodeSeed {
    type Value = Node;
    
    fn deserialize<D>(self, deserializer: D) -> Result<Node, D::Error>
    where D: Deserializer<'de>
    {
        // Standard node deserialization
        #[derive(serde::Deserialize)]
        struct NodeData {
            id: u64,
            name: String,
            references: Vec<u64>,
        }
        
        let data: NodeData = Deserialize::deserialize(deserializer)?;
        Ok(Node {
            id: data.id,
            name: data.name,
            references: data.references,
        })
    }
}
 
// Second pass: resolve references into actual pointers
struct ResolvedNode {
    id: u64,
    name: String,
    resolved_refs: Vec<usize>,  // Indices into node array
}
 
struct ResolveSeed<'a> {
    id_to_index: &'a HashMap<u64, usize>,
}
 
impl<'de> DeserializeSeed<'de> for ResolveSeed<'_> {
    type Value = Vec<usize>;
    
    fn deserialize<D>(self, deserializer: D) -> Result<Vec<usize>, D::Error>
    where D: Deserializer<'de>
    {
        // Deserialize reference IDs, then resolve them
        let ids: Vec<u64> = Vec::deserialize(deserializer)?;
        
        ids.into_iter()
            .map(|id| {
                self.id_to_index.get(&id)
                    .copied()
                    .ok_or_else(|| D::Error::custom(format!("Unknown node ID: {}", id)))
            })
            .collect()
    }
}
 
// Complete graph deserialization
fn deserialize_graph<'de, A>(mut seq: A) -> Result<Vec<ResolvedNode>, A::Error>
where A: SeqAccess<'de>
{
    // First pass: collect all nodes
    let mut nodes = Vec::new();
    while let Some(node) = seq.next_element_seed(NodeSeed)? {
        nodes.push(node);
    }
    
    // Build ID to index mapping
    let id_to_index: HashMap<u64, usize> = nodes.iter()
        .enumerate()
        .map(|(i, node)| (node.id, i))
        .collect();
    
    // Second pass: resolve references (conceptually)
    let resolved: Vec<ResolvedNode> = nodes.into_iter()
        .map(|node| {
            let resolved_refs = node.references.into_iter()
                .map(|id| id_to_index[&id])
                .collect();
            ResolvedNode {
                id: node.id,
                name: node.name,
                resolved_refs,
            }
        })
        .collect();
    
    Ok(resolved)
}

Seeds enable multi-pass deserialization where context from one pass informs the next.

Deserializing with External Configuration

use serde::de::{SeqAccess, DeserializeSeed, Deserializer, Visitor};
use std::fmt;
 
// Scenario: Deserialize values with a configurable format
struct ConfiguredValueSeed<'a> {
    format: &'a str,  // "json", "string", "raw"
}
 
impl<'de> DeserializeSeed<'de> for ConfiguredValueSeed<'_> {
    type Value = Vec<u8>;
    
    fn deserialize<D>(self, deserializer: D) -> Result<Vec<u8>, D::Error>
    where D: Deserializer<'de>
    {
        struct ConfiguredVisitor<'a>(&'a str);
        
        impl<'de> Visitor<'de> for ConfiguredVisitor<'_> {
            type Value = Vec<u8>;
            
            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "a {} value", self.0)
            }
            
            fn visit_str<E>(self, v: &str) -> Result<Vec<u8>, E>
            where E: serde::de::Error
            {
                match self.0 {
                    "json" => {
                        // Parse JSON, extract bytes
                        Ok(v.as_bytes().to_vec())
                    }
                    "string" => Ok(v.as_bytes().to_vec()),
                    "raw" => Ok(v.as_bytes().to_vec()),
                    _ => Err(E::custom("Unknown format"))
                }
            }
            
            fn visit_bytes<E>(self, v: &[u8]) -> Result<Vec<u8>, E>
            where E: serde::de::Error
            {
                Ok(v.to_vec())
            }
        }
        
        deserializer.deserialize_any(ConfiguredVisitor(self.format))
    }
}
 
// Deserialize sequence with per-element configuration
fn configured_sequence<'de, A>(
    seq: &mut A,
    formats: &[&str]
) -> Result<Vec<Vec<u8>>, A::Error>
where A: SeqAccess<'de>
{
    let mut result = Vec::new();
    
    for format in formats {
        let seed = ConfiguredValueSeed { format };
        match seq.next_element_seed(seed)? {
            Some(bytes) => result.push(bytes),
            None => break,
        }
    }
    
    Ok(result)
}

External configuration can be passed through seeds to influence element deserialization.

Seed State Mutation Between Elements

use serde::de::{SeqAccess, DeserializeSeed, Deserializer, Visitor, Error};
use std::fmt;
use std::cell::Cell;
 
// Scenario: Track position in sequence for validation
struct PositionSeed<'a> {
    position: &'a Cell<usize>,
}
 
impl<'de> DeserializeSeed<'de> for PositionSeed<'_> {
    type Value = i32;
    
    fn deserialize<D>(self, deserializer: D) -> Result<i32, D::Error>
    where D: Deserializer<'de>
    {
        struct PositionVisitor {
            position: Cell<usize>,
        }
        
        impl<'de> Visitor<'de> for PositionVisitor {
            type Value = i32;
            
            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "an integer at position {}", self.position.get())
            }
            
            fn visit_i64<E>(self, v: i64) -> Result<i32, E>
            where E: Error
            {
                let pos = self.position.get();
                // Position-specific validation
                if pos == 0 && v < 0 {
                    return Err(E::custom("First element must be non-negative"));
                }
                self.position.set(pos + 1);
                Ok(v as i32)
            }
        }
        
        let pos = self.position.get();
        deserializer.deserialize_i32(PositionVisitor { position: Cell::new(pos) })
    }
}
 
// Using position-aware seeds
fn position_validated_sequence<'de, A>(seq: &mut A) -> Result<Vec<i32>, A::Error>
where A: SeqAccess<'de>
{
    let mut result = Vec::new();
    let position = Cell::new(0);
    let seed = PositionSeed { position: &position };
    
    while let Some(value) = seq.next_element_seed(seed)? {
        result.push(value);
    }
    
    Ok(result)
}

Seeds can track and mutate state across element deserializations.

Implementing SeqAccess with Seed Support

use serde::de::{SeqAccess, Error};
use std::slice::Iter;
 
// A custom SeqAccess that supports seeds
struct SliceSeqAccess<'a, T> {
    iter: Iter<'a, T>,
}
 
impl<'a, T> SliceSeqAccess<'a, T> {
    fn new(slice: &'a [T]) -> Self {
        SliceSeqAccess { iter: slice.iter() }
    }
}
 
impl<'de, T> SeqAccess<'de> for SliceSeqAccess<'de, T>
where T: Clone + serde::Serialize
{
    type Error = serde_json::Error;
    
    fn next_element<V>(&mut self) -> Result<Option<V>, Self::Error>
    where V: serde::de::Deserialize<'de>
    {
        match self.iter.next() {
            Some(item) => {
                let json = serde_json::to_string(item)?;
                let value = serde_json::from_str(&json)?;
                Ok(Some(value))
            }
            None => Ok(None),
        }
    }
    
    fn next_element_seed<S>(&mut self, seed: S) -> Result<Option<S::Value>, Self::Error>
    where S: serde::de::DeserializeSeed<'de>
    {
        match self.iter.next() {
            Some(item) => {
                let json = serde_json::to_string(item)?;
                let deserializer = serde_json::Deserializer::from_str(&json);
                let value = seed.deserialize(deserializer)?;
                Ok(Some(value))
            }
            None => Ok(None),
        }
    }
    
    fn size_hint(&mut self) -> Option<usize> {
        Some(self.iter.len())
    }
}

Custom SeqAccess implementations should support both next_element and next_element_seed.

Practical Example: Deserializing Heterogeneous Types

use serde::de::{SeqAccess, DeserializeSeed, Deserializer, Visitor, Error};
use std::fmt;
 
// Scenario: A sequence with heterogenous types determined by an external schema
enum SchemaType {
    Integer,
    String,
    Boolean,
    Nested(Schema),  // Recursive schema
}
 
struct Schema {
    fields: Vec<SchemaType>,
}
 
// Seed that knows what type to expect
struct SchemaSeed<'a> {
    schema_type: &'a SchemaType,
}
 
impl<'de> DeserializeSeed<'de> for SchemaSeed<'_> {
    type Value = serde_json::Value;
    
    fn deserialize<D>(self, deserializer: D) -> Result<serde_json::Value, D::Error>
    where D: Deserializer<'de>
    {
        match self.schema_type {
            SchemaType::Integer => {
                let v: i64 = Deserialize::deserialize(deserializer)?;
                Ok(serde_json::Value::Number(v.into()))
            }
            SchemaType::String => {
                let v: String = Deserialize::deserialize(deserializer)?;
                Ok(serde_json::Value::String(v))
            }
            SchemaType::Boolean => {
                let v: bool = Deserialize::deserialize(deserializer)?;
                Ok(serde_json::Value::Bool(v))
            }
            SchemaType::Nested(nested_schema) => {
                // Recursively use schema for nested objects
                let v: serde_json::Value = Deserialize::deserialize(deserializer)?;
                Ok(v)
            }
        }
    }
}
 
use serde::Deserialize;
 
// Deserialize with external schema
fn schema_driven_deserialize<'de, A>(
    seq: &mut A,
    schema: &Schema
) -> Result<Vec<serde_json::Value>, A::Error>
where A: SeqAccess<'de>
{
    let mut result = Vec::new();
    
    for schema_type in &schema.fields {
        let seed = SchemaSeed { schema_type };
        match seq.next_element_seed(seed)? {
            Some(value) => result.push(value),
            None => break,
        }
    }
    
    Ok(result)
}

Seeds enable schema-driven deserialization where external type information guides parsing.

Default Implementation in SeqAccess

use serde::de::{SeqAccess, DeserializeSeed, Deserialize};
 
// Most SeqAccess implementations use this default pattern:
trait SeqAccessDefault<'de> {
    type Error;
    
    // Default implementation of next_element using next_element_seed
    fn next_element<T>(&mut self) -> Result<Option<T>, Self::Error>
    where T: Deserialize<'de>
    {
        // Use a "no-op" seed that just calls T::deserialize
        struct IdSeed;
        
        impl<'de> DeserializeSeed<'de> for IdSeed {
            type Value = T;
            
            fn deserialize<D>(self, deserializer: D) -> Result<T, D::Error>
            where D: Deserializer<'de>
            {
                T::deserialize(deserializer)
            }
        }
        
        self.next_element_seed(IdSeed)
    }
    
    fn next_element_seed<S>(&mut self, seed: S) -> Result<Option<S::Value>, Self::Error>
    where S: DeserializeSeed<'de>
    {
        // Must be implemented by the concrete type
        todo!()
    }
}
 
// This shows that next_element is just next_element_seed with an identity seed

The next_element method is implemented using next_element_seed with a trivial seed.

Summary Comparison

fn comparison() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Aspect              β”‚ next_element      β”‚ next_element_seed           β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ Context              β”‚ None               β”‚ Seed provides context       β”‚
    // β”‚ Use case             β”‚ Self-contained     β”‚ Needs external data         β”‚
    // β”‚ Dependencies          β”‚ None              β”‚ Can resolve references      β”‚
    // β”‚ Configuration        β”‚ None               β”‚ Per-element config          β”‚
    // β”‚ State tracking       β”‚ None               β”‚ Can track position          β”‚
    // β”‚ Type determination   β”‚ From Deserialize   β”‚ From seed + Deserialize    β”‚
    // β”‚ Implementation       β”‚ Uses seed internallyβ”‚ Core implementation        β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
}

Key Points Summary

fn key_points() {
    // 1. next_element_seed provides external context to element deserialization
    // 2. DeserializeSeed is like Deserialize but carries context
    // 3. Use when elements need data not in the serialized format
    // 4. Common pattern: ID resolution from a registry
    // 5. Seeds can vary per element (different expected types per position)
    // 6. Seeds can track state across elements (position, validation)
    // 7. Enables graph deserialization with reference resolution
    // 8. Supports schema-driven deserialization with external type info
    // 9. next_element is implemented using next_element_seed with identity seed
    // 10. Custom SeqAccess should implement both methods
    // 11. Multi-pass deserialization uses seeds for each pass
    // 12. Configuration can be passed through seeds
    // 13. Seeds can implement validation based on external rules
}

Key insight: next_element_seed bridges the gap between serialized data and the context needed to interpret it. While next_element works for self-contained values, many real-world scenarios require external contextβ€”resolving IDs from a registry, validating against a schema, tracking position for error messages, or maintaining consistency across a graph. The seed pattern allows this context to flow into the deserialization process without polluting the data format itself. Implement DeserializeSeed when deserialization needs information beyond what's in the serialized bytes, and use next_element_seed when deserializing sequences of such values.