How does serde::de::Visitor::visit_map enable custom deserialization logic for map structures?
serde::de::Visitor::visit_map is the method called by a deserializer when it encounters a map-like data structure (JSON objects, TOML tables, etc.), allowing the visitor to control exactly how keys and values are read, validated, and transformed. When implementing Visitor for custom deserialization, the visit_map method receives a MapAccess argument that provides sequential access to map entriesāthe visitor can process entries in any order, validate presence of required fields, handle unknown keys, and construct the final data structure. This enables deserialization logic that goes beyond what #[derive(Deserialize)] can express: field renaming, validation, defaults, aliases, and custom error messages.
What is the Visitor Trait?
use serde::de::{Visitor, Error};
// Visitor is the core trait for custom deserialization
// It has methods for every data type serde can deserialize
pub trait Visitor<'de> {
// Called for primitive types
fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E>;
fn visit_u64<E: Error>(self, v: u64) -> Result<Self::Value, E>;
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E>;
// Called for sequence types
fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
where
S: SeqAccess<'de>;
// Called for map types
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>;
// The type produced by this visitor
type Value;
}Visitor defines how each type of input should be processed during deserialization.
The MapAccess Trait
use serde::de::{MapAccess, Error};
// MapAccess provides sequential access to map entries
pub trait MapAccess<'de> {
// Returns the next key, or None if no more entries
fn next_key<K>(&mut self) -> Result<Option<K>, Self::Error>
where
K: Deserialize<'de>;
// Returns the next value (must be called after next_key)
fn next_value<V>(&mut self) -> Result<V, Self::Error>
where
V: Deserialize<'de>;
// Returns the next key-value pair as a tuple
fn next_entry<K, V>(&mut self) -> Result<Option<(K, V)>, Self::Error>
where
K: Deserialize<'de>,
V: Deserialize<'de>;
// Returns the number of remaining entries (if known)
fn size_hint(&self) -> Option<usize>;
}MapAccess lets you read map entries one at a time, in any order.
Basic visit_map Implementation
use serde::de::{Visitor, MapAccess, Error};
use std::collections::HashMap;
use std::marker::PhantomData;
struct HashMapVisitor<K, V> {
_marker: PhantomData<(K, V)>,
}
impl<'de, K, V> Visitor<'de> for HashMapVisitor<K, V>
where
K: serde::Deserialize<'de> + std::hash::Hash + Eq,
V: serde::Deserialize<'de>,
{
type Value = HashMap<K, V>;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "a map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut result = HashMap::new();
// Read entries one at a time
while let Some((key, value)) = map.next_entry()? {
result.insert(key, value);
}
Ok(result)
}
}
// Usage
fn deserialize_hashmap<'de, D>(deserializer: D) -> Result<HashMap<String, i32>, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(HashMapVisitor {
_marker: PhantomData,
})
}The basic pattern reads entries until next_entry returns None.
Deserializing a Struct with visit_map
use serde::de::{Visitor, MapAccess, Error};
use std::collections::HashMap;
#[derive(Debug)]
struct Config {
name: String,
count: u32,
enabled: bool,
}
struct ConfigVisitor;
impl<'de> Visitor<'de> for ConfigVisitor {
type Value = Config;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "a Config object with name, count, and enabled fields")
}
fn visit_map<M>(self, mut map: M) -> Result<Config, M::Error>
where
M: MapAccess<'de>,
{
let mut name = None;
let mut count = None;
let mut enabled = None;
// Read each field
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"name" => {
name = Some(map.next_value()?);
}
"count" => {
count = Some(map.next_value()?);
}
"enabled" => {
enabled = Some(map.next_value()?);
}
unknown => {
// Handle unknown fields
return Err(Error::unknown_field(unknown, &["name", "count", "enabled"]));
}
}
}
// Validate required fields
let name = name.ok_or_else(|| Error::missing_field("name"))?;
let count = count.ok_or_else(|| Error::missing_field("count"))?;
let enabled = enabled.ok_or_else(|| Error::missing_field("enabled"))?;
Ok(Config { name, count, enabled })
}
}
impl<'de> serde::Deserialize<'de> for Config {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_map(ConfigVisitor)
}
}visit_map allows field-by-field parsing with validation and custom error messages.
Handling Default Values
use serde::de::{Visitor, MapAccess, Error};
#[derive(Debug)]
struct ServerConfig {
host: String,
port: u16,
timeout: u32, // default: 30
retries: u32, // default: 3
}
struct ServerConfigVisitor;
impl<'de> Visitor<'de> for ServerConfigVisitor {
type Value = ServerConfig;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "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 = Some(30); // default
let mut retries = Some(3); // default
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"host" => host = Some(map.next_value()?),
"port" => port = Some(map.next_value()?),
"timeout" => timeout = Some(map.next_value()?),
"retries" => retries = Some(map.next_value()?),
unknown => return Err(Error::unknown_field(unknown, &["host", "port", "timeout", "retries"])),
}
}
Ok(ServerConfig {
host: host.ok_or_else(|| Error::missing_field("host"))?,
port: port.ok_or_else(|| Error::missing_field("port"))?,
timeout: timeout.unwrap(),
retries: retries.unwrap(),
})
}
}Defaults can be provided by initializing Option values with Some(default).
Field Aliases
use serde::de::{Visitor, MapAccess, Error};
#[derive(Debug)]
struct User {
username: String,
email: String,
}
struct UserVisitor;
impl<'de> Visitor<'de> for UserVisitor {
type Value = User;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "a user object")
}
fn visit_map<M>(self, mut map: M) -> Result<User, M::Error>
where
M: MapAccess<'de>,
{
let mut username = None;
let mut email = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
// Accept multiple aliases for the same field
"username" | "user_name" | "user" => {
username = Some(map.next_value()?);
}
"email" | "email_address" | "e_mail" => {
email = Some(map.next_value()?);
}
unknown => {
// Ignore unknown fields instead of erroring
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
Ok(User {
username: username.ok_or_else(|| Error::missing_field("username"))?,
email: email.ok_or_else(|| Error::missing_field("email"))?,
})
}
}Field aliases allow accepting multiple key names for the same field.
Ignoring Unknown Fields
use serde::de::{Visitor, MapAccess, IgnoredAny};
struct FlexibleVisitor;
impl<'de> Visitor<'de> for FlexibleVisitor {
type Value = HashMap<String, serde_json::Value>;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "a flexible object")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut result = HashMap::new();
while let Some(key) = map.next_key::<String>()? {
// Accept any value type
let value: serde_json::Value = map.next_value()?;
result.insert(key, value);
}
Ok(result)
}
}
// Alternatively, ignore unknown fields in a struct
fn visit_map_with_ignore<M>(map: M) -> Result<Config, M::Error>
where
M: MapAccess<'de>,
{
let mut config = ConfigVisitor;
// ... implementation that calls map.next_value::<IgnoredAny>() for unknown fields
todo!()
}IgnoredAny efficiently skips values without parsing them fully.
Enum Deserialization with visit_map
use serde::de::{Visitor, MapAccess, Error};
#[derive(Debug)]
enum Message {
Request { id: u32, data: String },
Response { id: u32, result: String },
Error { id: u32, code: u32 },
}
struct MessageVisitor;
impl<'de> Visitor<'de> for MessageVisitor {
type Value = Message;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "a message object with type field")
}
fn visit_map<M>(self, mut map: M) -> Result<Message, M::Error>
where
M: MapAccess<'de>,
{
// First, read the type to determine variant
let mut msg_type = None;
let mut id = None;
let mut data = None;
let mut result = None;
let mut code = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"type" => msg_type = Some(map.next_value()?),
"id" => id = Some(map.next_value()?),
"data" => data = Some(map.next_value()?),
"result" => result = Some(map.next_value()?),
"code" => code = Some(map.next_value()?),
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
}
}
let id = id.ok_or_else(|| Error::missing_field("id"))?;
let msg_type = msg_type.ok_or_else(|| Error::missing_field("type"))?;
match msg_type.as_str() {
"request" => {
let data = data.ok_or_else(|| Error::missing_field("data"))?;
Ok(Message::Request { id, data })
}
"response" => {
let result = result.ok_or_else(|| Error::missing_field("result"))?;
Ok(Message::Response { id, result })
}
"error" => {
let code = code.ok_or_else(|| Error::missing_field("code"))?;
Ok(Message::Error { id, code })
}
unknown => Err(Error::custom(format!("unknown message type: {}", unknown))),
}
}
}visit_map enables variant selection based on a discriminator field.
Deserializing into Existing Structure
use serde::de::{Visitor, MapAccess, Error};
#[derive(Debug)]
struct MutableConfig {
values: HashMap<String, String>,
}
struct UpdateConfigVisitor<'a> {
config: &'a mut MutableConfig,
}
impl<'de, 'a> Visitor<'de> for UpdateConfigVisitor<'a> {
type Value = ();
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "config updates")
}
fn visit_map<M>(self, mut map: M) -> Result<(), M::Error>
where
M: MapAccess<'de>,
{
while let Some(key) = map.next_key::<String>()? {
let value: String = map.next_value()?;
self.config.values.insert(key, value);
}
Ok(())
}
}
// Merge updates into existing config
fn merge_config(config: &mut MutableConfig, updates: &str) -> Result<(), serde_json::Error> {
let visitor = UpdateConfigVisitor { config };
let mut deserializer = serde_json::Deserializer::from_str(updates);
deserializer.deserialize_map(visitor)
}Visitors can mutate existing structures rather than creating new ones.
Size Hint Optimization
use serde::de::{Visitor, MapAccess};
use std::collections::HashMap;
struct OptimizedMapVisitor;
impl<'de> Visitor<'de> for OptimizedMapVisitor {
type Value = HashMap<String, String>;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "a map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
// Pre-allocate if size hint is available
let mut result = match map.size_hint() {
Some(size) => HashMap::with_capacity(size),
None => HashMap::new(),
};
while let Some((key, value)) = map.next_entry()? {
result.insert(key, value);
}
Ok(result)
}
}size_hint enables pre-allocation for better performance.
Nested Map Deserialization
use serde::de::{Visitor, MapAccess, Error};
use std::collections::HashMap;
#[derive(Debug)]
struct NestedConfig {
name: String,
settings: HashMap<String, String>,
}
struct NestedConfigVisitor;
impl<'de> Visitor<'de> for NestedConfigVisitor {
type Value = NestedConfig;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "a nested configuration object")
}
fn visit_map<M>(self, mut map: M) -> Result<NestedConfig, M::Error>
where
M: MapAccess<'de>,
{
let mut name = None;
let mut settings = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"name" => name = Some(map.next_value()?),
"settings" => settings = Some(map.next_value()?),
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
}
}
Ok(NestedConfig {
name: name.ok_or_else(|| Error::missing_field("name"))?,
settings: settings.ok_or_else(|| Error::missing_field("settings"))?,
})
}
}Nested maps are deserialized recursively via next_value.
Custom Error Messages
use serde::de::{Visitor, MapAccess, Error};
struct ValidatedConfigVisitor;
impl<'de> Visitor<'de> for ValidatedConfigVisitor {
type Value = Config;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "a valid configuration object")
}
fn visit_map<M>(self, mut map: M) -> Result<Config, M::Error>
where
M: MapAccess<'de>,
{
let mut name = None;
let mut count = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"name" => {
let value: String = map.next_value()?;
// Custom validation
if value.len() < 3 {
return Err(Error::custom("name must be at least 3 characters"));
}
name = Some(value);
}
"count" => {
let value: u32 = map.next_value()?;
// Range validation
if value > 100 {
return Err(Error::custom("count must not exceed 100"));
}
count = Some(value);
}
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
}
}
Ok(Config {
name: name.ok_or_else(|| Error::custom("name is required"))?,
count: count.ok_or_else(|| Error::custom("count is required"))?,
})
}
}Error::custom provides user-friendly error messages with context.
Comparison with Derive
use serde::{Deserialize, Serialize};
// With derive: limited customization
#[derive(Deserialize)]
struct SimpleConfig {
name: String,
count: u32,
#[serde(default)]
enabled: bool, // Simple default
}
// With visit_map: full control
struct ComplexConfig {
name: String,
count: u32,
enabled: bool,
}
// visit_map enables:
// - Custom validation per field
// - Field aliases
// - Computed fields
// - Cross-field validation
// - Custom error messages
// - Unknown field handling
// - Default value logicvisit_map provides control beyond what attributes can express.
Real-World Example: Environment Configuration
use serde::de::{Visitor, MapAccess, Error};
use std::collections::HashMap;
#[derive(Debug)]
struct AppConfig {
database_url: String,
max_connections: u32,
timeout_seconds: u32,
debug_mode: bool,
}
struct AppConfigVisitor {
defaults: AppConfigDefaults,
}
struct AppConfigDefaults {
max_connections: u32,
timeout_seconds: u32,
debug_mode: bool,
}
impl Default for AppConfigDefaults {
fn default() -> Self {
Self {
max_connections: 10,
timeout_seconds: 30,
debug_mode: false,
}
}
}
impl<'de> Visitor<'de> for AppConfigVisitor {
type Value = AppConfig;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "an application configuration object")
}
fn visit_map<M>(self, mut map: M) -> Result<AppConfig, M::Error>
where
M: MapAccess<'de>,
{
let mut database_url = None;
let mut max_connections = Some(self.defaults.max_connections);
let mut timeout_seconds = Some(self.defaults.timeout_seconds);
let mut debug_mode = Some(self.defaults.debug_mode);
// Support environment variable interpolation
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"database_url" | "DATABASE_URL" | "db_url" => {
let value: String = map.next_value()?;
database_url = Some(value);
}
"max_connections" | "MAX_CONNECTIONS" => {
max_connections = Some(map.next_value()?);
}
"timeout_seconds" | "TIMEOUT_SECONDS" | "timeout" => {
timeout_seconds = Some(map.next_value()?);
}
"debug_mode" | "DEBUG" | "debug" => {
debug_mode = Some(map.next_value()?);
}
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
}
}
Ok(AppConfig {
database_url: database_url.ok_or_else(|| {
Error::custom("database_url is required (set DATABASE_URL)")
})?,
max_connections: max_connections.unwrap(),
timeout_seconds: timeout_seconds.unwrap(),
debug_mode: debug_mode.unwrap(),
})
}
}Configuration deserialization with aliases, defaults, and custom messages.
Real-World Example: Tagged Union Deserialization
use serde::de::{Visitor, MapAccess, Error};
#[derive(Debug)]
enum ApiResponse {
Success { data: String },
Error { code: u32, message: String },
}
struct ApiResponseVisitor;
impl<'de> Visitor<'de> for ApiResponseVisitor {
type Value = ApiResponse;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "an API response object")
}
fn visit_map<M>(self, mut map: M) -> Result<ApiResponse, M::Error>
where
M: MapAccess<'de>,
{
let mut status = None;
let mut data = None;
let mut code = None;
let mut message = None;
// Two-pass: first get status, then parse rest
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"status" => status = Some(map.next_value()?),
"data" => data = Some(map.next_value()?),
"code" => code = Some(map.next_value()?),
"message" => message = Some(map.next_value()?),
_ => { map.next_value::<serde::de::IgnoredAny>()?; }
}
}
let status: String = status.ok_or_else(|| Error::missing_field("status"))?;
match status.as_str() {
"success" => {
Ok(ApiResponse::Success {
data: data.ok_or_else(|| Error::missing_field("data"))?,
})
}
"error" => {
Ok(ApiResponse::Error {
code: code.ok_or_else(|| Error::missing_field("code"))?,
message: message.ok_or_else(|| Error::missing_field("message"))?,
})
}
_ => Err(Error::custom("status must be 'success' or 'error'")),
}
}
}Tagged unions use a discriminator field to select the variant.
Real-World Example: Strict vs Lenient Parsing
use serde::de::{Visitor, MapAccess, Error};
#[derive(Debug)]
struct StrictUser {
username: String,
email: String,
}
struct LenientUser {
username: String,
email: String,
#[allow(dead_code)]
extra: HashMap<String, serde_json::Value>,
}
// Strict: reject unknown fields
struct StrictUserVisitor;
impl<'de> Visitor<'de> for StrictUserVisitor {
type Value = StrictUser;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "a user object with only username and email fields")
}
fn visit_map<M>(self, mut map: M) -> Result<StrictUser, M::Error>
where
M: MapAccess<'de>,
{
let mut username = None;
let mut email = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"username" => username = Some(map.next_value()?),
"email" => email = Some(map.next_value()?),
unknown => {
return Err(Error::unknown_field(unknown, &["username", "email"]));
}
}
}
Ok(StrictUser {
username: username.ok_or_else(|| Error::missing_field("username"))?,
email: email.ok_or_else(|| Error::missing_field("email"))?,
})
}
}
// Lenient: accept and store unknown fields
struct LenientUserVisitor;
impl<'de> Visitor<'de> for LenientUserVisitor {
type Value = LenientUser;
fn expecting(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "a user object")
}
fn visit_map<M>(self, mut map: M) -> Result<LenientUser, M::Error>
where
M: MapAccess<'de>,
{
let mut username = None;
let mut email = None;
let mut extra = HashMap::new();
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"username" => username = Some(map.next_value()?),
"email" => email = Some(map.next_value()?),
_ => {
let value: serde_json::Value = map.next_value()?;
extra.insert(key, value);
}
}
}
Ok(LenientUser {
username: username.ok_or_else(|| Error::missing_field("username"))?,
email: email.ok_or_else(|| Error::missing_field("email"))?,
extra,
})
}
}visit_map enables strict or lenient field handling as needed.
Synthesis
MapAccess methods:
| Method | Purpose | Returns |
|---|---|---|
next_key |
Get next key | Option<K> |
next_value |
Get value for last key | V |
next_entry |
Get key-value pair | Option<(K, V)> |
size_hint |
Pre-allocation hint | Option<usize> |
Common patterns:
| Pattern | Implementation |
|---|---|
| Required fields | Option<T> with ok_or_else |
| Optional fields | Option<T> defaults to None |
| Default values | Initialize Option<T> with Some(default) |
| Field aliases | Match multiple key names |
| Unknown fields | IgnoredAny or Error::unknown_field |
| Validation | Check values in match branches |
| Cross-field validation | Validate after collecting all fields |
When to use visit_map:
| Use Case | Alternative |
|---|---|
| Simple struct deserialization | #[derive(Deserialize)] |
| Field aliases | #[serde(alias = "...")] |
| Default values | #[serde(default)] |
| Custom validation | #[serde(validate = "...")] with validator crate |
| Unknown field handling | #[serde(deny_unknown_fields)] |
| Cross-field validation | visit_map required |
| Computed fields | visit_map required |
| Custom error messages | visit_map gives full control |
| Variant selection by field | visit_map required |
Key insight: visit_map provides fine-grained control over map deserialization by giving the visitor direct access to the MapAccess iterator. This enables patterns that derive-based deserialization cannot express: field aliases that accept multiple key names, cross-field validation that depends on multiple values, computed fields derived from other values, custom error messages with context, and dynamic handling of unknown fields. The pattern involves reading keys one at a time with next_key, matching on key names to determine field type, calling next_value to deserialize the corresponding value, and collecting into a result structure. The Option<T> pattern for each field allows distinguishing between "not present" (for required field errors) and "present with default" (for optional fields with defaults). After the loop, validate required fields and construct the final value. For strict parsing, return Error::unknown_field on unrecognized keys; for lenient parsing, use IgnoredAny to skip them. The size_hint method enables pre-allocation when the deserializer knows the entry count.
