How does serde::de::Visitor::visit_map enable custom map deserialization logic?
visit_map is the method on the Visitor trait that gets called when a deserializer encounters a map-like structure during deserialization, giving you full control over how key-value pairs are consumed and transformed into your custom data structure. By implementing visit_map, you can validate keys as they arrive, build custom data structures incrementally, handle missing or extra fields with custom logic, and transform data during deserializationāall while streaming through the map without requiring the entire structure to be buffered first. This enables efficient, flexible deserialization that goes far beyond what derive-based serialization can accomplish.
The Visitor Trait and Map Deserialization
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
use std::marker::PhantomData;
struct MyMapVisitor<K, V> {
_key: PhantomData<K>,
_value: PhantomData<V>,
}
impl<'de, K, V> Visitor<'de> for MyMapVisitor<K, V>
where
K: serde::Deserialize<'de>,
V: serde::Deserialize<'de>,
{
type Value = std::collections::HashMap<K, V>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut result = std::collections::HashMap::with_capacity(map.size_hint().unwrap_or(0));
while let Some((key, value)) = map.next_entry()? {
result.insert(key, value);
}
Ok(result)
}
}The visit_map method receives a MapAccess object that you iterate through to consume entries.
Streaming Access to Map Entries
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
struct StreamingMapVisitor;
impl<'de> Visitor<'de> for StreamingMapVisitor {
type Value = Vec<(String, String)>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut entries = Vec::new();
// Stream entries one at a time - no buffering needed
while let Some(entry) = map.next_entry::<String, String>()? {
entries.push(entry);
}
Ok(entries)
}
}MapAccess::next_entry retrieves entries one at a time, enabling memory-efficient streaming.
Custom Validation During Deserialization
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
struct ValidatedConfigVisitor;
#[derive(Debug)]
struct Config {
name: String,
count: u32,
enabled: bool,
}
impl<'de> Visitor<'de> for ValidatedConfigVisitor {
type Value = Config;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a configuration map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut name = None;
let mut count = None;
let mut enabled = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"name" => {
name = Some(map.next_value()?);
}
"count" => {
let c: u32 = map.next_value()?;
if c > 100 {
return Err(M::Error::custom("count cannot exceed 100"));
}
count = Some(c);
}
"enabled" => {
enabled = Some(map.next_value()?);
}
unknown => {
// Skip unknown fields or error
return Err(M::Error::unknown_field(unknown, &["name", "count", "enabled"]));
}
}
}
let name = name.ok_or_else(|| M::Error::missing_field("name"))?;
let count = count.ok_or_else(|| M::Error::missing_field("count"))?;
let enabled = enabled.unwrap_or(false);
Ok(Config { name, count, enabled })
}
}Validate values as they arrive, returning errors for invalid data immediately.
Handling Missing and Extra Fields
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
#[derive(Debug, Default)]
struct FlexibleConfig {
name: String,
value: i32,
enabled: bool,
}
struct FlexibleConfigVisitor;
impl<'de> Visitor<'de> for FlexibleConfigVisitor {
type Value = FlexibleConfig;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a configuration object")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
// Start with defaults
let mut config = FlexibleConfig::default();
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"name" => config.name = map.next_value()?,
"value" => config.value = map.next_value()?,
"enabled" => config.enabled = map.next_value()?,
// Silently ignore unknown fields
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
Ok(config)
}
}Use defaults for missing fields and ignore unknown fields gracefully.
Building Custom Data Structures
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
use std::collections::BTreeMap;
struct SortedMapVisitor;
impl<'de> Visitor<'de> for SortedMapVisitor {
type Value = BTreeMap<String, i32>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a sorted map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
// BTreeMap maintains sorted order automatically
let mut result = BTreeMap::new();
while let Some((key, value)) = map.next_entry::<String, i32>()? {
if result.contains_key(&key) {
return Err(M::Error::custom(format!("duplicate key: {}", key)));
}
result.insert(key, value);
}
Ok(result)
}
}Build any data structure you needāthe visitor controls the output type completely.
Transforming Keys and Values
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
#[derive(Debug)]
struct CaseInsensitiveMap {
entries: Vec<(String, String)>,
}
struct CaseInsensitiveVisitor;
impl<'de> Visitor<'de> for CaseInsensitiveVisitor {
type Value = CaseInsensitiveMap;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a map with case-insensitive keys")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut entries = Vec::new();
while let Some((key, value)) = map.next_entry::<String, String>()? {
// Transform key to lowercase
let normalized_key = key.to_lowercase();
entries.push((normalized_key, value));
}
Ok(CaseInsensitiveMap { entries })
}
}Transform data during deserialization without a separate pass.
Size Hints for Allocation Optimization
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
use std::collections::HashMap;
struct OptimizedMapVisitor;
impl<'de> Visitor<'de> for OptimizedMapVisitor {
type Value = HashMap<String, String>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
// Use size_hint for efficient allocation
let capacity = map.size_hint().unwrap_or(0);
let mut result = HashMap::with_capacity(capacity);
while let Some((key, value)) = map.next_entry::<String, String>()? {
result.insert(key, value);
}
Ok(result)
}
}size_hint() provides an estimated entry count for pre-allocation.
Manual Key-Value Iteration
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
struct ManualMapVisitor;
impl<'de> Visitor<'de> for ManualMapVisitor {
type Value = (Vec<String>, Vec<i32>);
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut keys = Vec::new();
let mut values = Vec::new();
// Manual key-by-key iteration for more control
while let Some(key) = map.next_key::<String>()? {
let value = map.next_value::<i32>()?;
keys.push(key);
values.push(value);
}
Ok((keys, values))
}
}next_key and next_value separate the key and value retrieval for fine-grained control.
Deserializing Enums from Maps
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
#[derive(Debug)]
enum FieldType {
Integer(i64),
Float(f64),
Text(String),
Boolean(bool),
}
struct FieldTypeVisitor;
impl<'de> Visitor<'de> for FieldTypeVisitor {
type Value = FieldType;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a field type object")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut type_name: Option<String> = None;
let mut value_str: Option<String> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"type" => type_name = Some(map.next_value()?),
"value" => value_str = Some(map.next_value()?),
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
}
}
let type_name = type_name.ok_or_else(|| M::Error::missing_field("type"))?;
let value_str = value_str.ok_or_else(|| M::Error::missing_field("value"))?;
match type_name.as_str() {
"integer" => {
let v: i64 = value_str.parse().map_err(M::Error::custom)?;
Ok(FieldType::Integer(v))
}
"float" => {
let v: f64 = value_str.parse().map_err(M::Error::custom)?;
Ok(FieldType::Float(v))
}
"text" => Ok(FieldType::Text(value_str)),
"boolean" => {
let v: bool = value_str.parse().map_err(M::Error::custom)?;
Ok(FieldType::Boolean(v))
}
_ => Err(M::Error::custom("unknown type")),
}
}
}Deserialize tagged unions from map structures with custom field processing.
Custom Deserializable Type
use serde::de::{Visitor, MapAccess, Error, Deserialize, Deserializer};
use std::fmt;
#[derive(Debug)]
struct User {
id: u64,
username: String,
email: Option<String>,
}
impl<'de> Deserialize<'de> for User {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct UserVisitor;
impl<'de> Visitor<'de> for UserVisitor {
type Value = User;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a user object")
}
fn visit_map<M>(self, mut map: M) -> Result<User, M::Error>
where
M: MapAccess<'de>,
{
let mut id = None;
let mut username = None;
let mut email = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"id" => id = Some(map.next_value()?),
"username" => username = Some(map.next_value()?),
"email" => email = Some(map.next_value()?),
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
}
}
let id = id.ok_or_else(|| M::Error::missing_field("id"))?;
let username = username.ok_or_else(|| M::Error::missing_field("username"))?;
Ok(User { id, username, email })
}
}
deserializer.deserialize_map(UserVisitor)
}
}Full Deserialize implementation with visit_map for complete control.
Nested Map Handling
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
use std::collections::HashMap;
#[derive(Debug)]
struct NestedConfig {
inner: HashMap<String, String>,
}
struct NestedConfigVisitor;
impl<'de> Visitor<'de> for NestedConfigVisitor {
type Value = NestedConfig;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a nested configuration")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut inner = HashMap::new();
// Flatten nested structure
while let Some(key) = map.next_key::<String>()? {
if key.starts_with("config.") {
// Handle flattened keys like "config.name"
let inner_key = key.strip_prefix("config.").unwrap().to_string();
let value: String = map.next_value()?;
inner.insert(inner_key, value);
} else {
// Handle direct key-value pairs
let value: String = map.next_value()?;
inner.insert(key, value);
}
}
Ok(NestedConfig { inner })
}
}Process nested or namespaced keys during deserialization.
Error Handling with Context
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
struct ContextualErrorVisitor;
impl<'de> Visitor<'de> for ContextualErrorVisitor {
type Value = (String, i32);
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a map with 'name' and 'age' fields")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut name = None;
let mut age = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"name" => {
let n: String = map.next_value()?;
if n.is_empty() {
return Err(M::Error::custom("name cannot be empty"));
}
name = Some(n);
}
"age" => {
let a: i32 = map.next_value()?;
if a < 0 {
return Err(M::Error::custom(format!("age {} is negative", a)));
}
if a > 150 {
return Err(M::Error::custom(format!("age {} is unreasonably large", a)));
}
age = Some(a);
}
k => {
return Err(M::Error::unknown_field(k, &["name", "age"]));
}
}
}
let name = name.ok_or_else(|| M::Error::missing_field("name"))?;
let age = age.ok_or_else(|| M::Error::missing_field("age"))?;
Ok((name, age))
}
}Provide detailed, contextual errors during deserialization.
Using IgnoredAny for Skipping Values
use serde::de::{Visitor, MapAccess, IgnoredAny};
use std::fmt;
struct SelectiveFieldVisitor;
impl<'de> Visitor<'de> for SelectiveFieldVisitor {
type Value = String;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a map with a 'target' field")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut target = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"target" => {
target = Some(map.next_value()?);
}
// Skip all other fields without deserializing them
_ => {
map.next_value::<IgnoredAny>()?;
}
}
}
target.ok_or_else(|| M::Error::missing_field("target"))
}
}IgnoredAny skips values without deserializing them, saving work.
Comparison: Manual visit_map vs Derive
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
// Derive approach: Simple, but limited control
#[derive(Deserialize, Serialize)]
struct DeriveConfig {
name: String,
count: u32,
#[serde(default)]
enabled: bool,
}
// Manual approach: Full control over deserialization
struct ManualConfig {
name: String,
count: u32,
enabled: bool,
}
// Manual implementation allows:
// - Custom validation during deserialization
// - Transformation of values
// - Custom error messages
// - Handling arbitrary field names
// - Building any data structureDerive is simpler; manual visit_map provides complete control.
Real-World Example: Configuration with Aliases
use serde::de::{Visitor, MapAccess, Error, Deserializer, Deserialize};
use std::fmt;
#[derive(Debug)]
struct ServerConfig {
host: String,
port: u16,
timeout_seconds: u64,
}
impl<'de> Deserialize<'de> for ServerConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct ServerConfigVisitor;
impl<'de> Visitor<'de> for ServerConfigVisitor {
type Value = ServerConfig;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a server configuration object")
}
fn visit_map<M>(self, mut map: M) -> Result<ServerConfig, M::Error>
where
M: MapAccess<'de>,
{
let mut host = None;
let mut port = None;
let mut timeout_seconds = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
// Accept multiple aliases for host
"host" | "hostname" | "server" | "address" => {
host = Some(map.next_value()?);
}
// Accept multiple aliases for port
"port" | "port_number" => {
port = Some(map.next_value()?);
}
// Accept multiple aliases for timeout
"timeout" | "timeout_seconds" | "timeout_secs" => {
timeout_seconds = Some(map.next_value()?);
}
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
let host = host.ok_or_else(|| {
M::Error::custom("missing host (try 'host', 'hostname', 'server', or 'address')")
})?;
let port = port.ok_or_else(|| M::Error::missing_field("port"))?;
let timeout_seconds = timeout_seconds.unwrap_or(30);
Ok(ServerConfig { host, port, timeout_seconds })
}
}
deserializer.deserialize_map(ServerConfigVisitor)
}
}Support multiple field aliases without derive macro complexity.
Synthesis
Quick reference:
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
struct MyVisitor;
impl<'de> Visitor<'de> for MyVisitor {
type Value = MyType;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "expected format description")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
// Pre-allocate using size_hint
let capacity = map.size_hint().unwrap_or(0);
// Iterate through entries
while let Some((key, value)) = map.next_entry()? {
// Process each key-value pair
// Validate, transform, build data structure
}
// Or iterate key-by-key for more control
while let Some(key) = map.next_key::<String>()? {
let value = map.next_value::<String>()?;
// Process with full control
}
// Return the built value
Ok(/* ... */)
}
}
// Key methods on MapAccess:
// - next_entry() -> Result<Option<(K, V)>, E>
// - next_key() -> Result<Option<K>, E>
// - next_value() -> Result<V, E>
// - size_hint() -> Option<usize>Key insight: visit_map provides a streaming interface to map deserialization where you control every aspect: how entries are consumed, how values are validated and transformed, how errors are reported, and what data structure is ultimately built. The MapAccess trait gives you entry-by-entry iteration, letting you process large maps efficiently without buffering the entire structure. Use manual visit_map when you need validation beyond simple type checking, want to transform data during deserialization, need custom error messages with context, or must handle unusual field names or structures that don't map cleanly to Rust struct fields.
