Loading pageâŚ
Rust walkthroughs
Loading pageâŚ
serde::de::Visitor::visit_map enable custom map deserialization logic?Serde's Visitor trait is the mechanism by which deserializers delegate type-specific parsing to data structures. The visit_map method is invoked when a deserializer encounters a map-like data structure (like a JSON object) and needs to populate a Rust type. This method receives a MapAccess implementation that provides sequential access to key-value pairs, allowing custom deserialization logic like filtering keys, transforming values, validating invariants, or handling heterogeneous map structures. Unlike derive-based deserialization which follows a fixed schema, visit_map enables arbitrary logic during deserializationâsuch as case-insensitive key matching, default value injection, or converting between map representationsâwhile maintaining Serde's streaming, zero-copy design.
use serde::de::{Visitor, MapAccess, Error};
use std::fmt;
// Visitor is the core trait for custom deserialization
trait Visitor<'de>: fmt::Debug {
// ... many other visit methods for different types ...
// Called when deserializing a map/object
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>;
}The visit_map method receives a MapAccess that yields key-value pairs sequentially.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, Error};
use std::collections::HashMap;
use std::fmt;
struct HashMapVisitor<K, V>(std::marker::PhantomData<(K, V)>);
impl<'de, K, V> Visitor<'de> for HashMapVisitor<K, V>
where
K: Deserialize<'de> + std::hash::Hash + Eq,
V: Deserialize<'de>,
{
type Value = HashMap<K, V>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut result = HashMap::new();
// Iterate over key-value pairs
while let Some((key, value)) = map.next_entry()? {
result.insert(key, value);
}
Ok(result)
}
}
fn deserialize_hashmap<'de, D, K, V>(deserializer: D) -> Result<HashMap<K, V>, D::Error>
where
D: Deserializer<'de>,
K: Deserialize<'de> + std::hash::Hash + Eq,
V: Deserialize<'de>,
{
deserializer.deserialize_map(HashMapVisitor(std::marker::PhantomData))
}The basic pattern: create a MapAccess, iterate entries, build the result.
use serde::de::MapAccess;
fn map_access_methods<'de, M: MapAccess<'de>>(mut map: M) -> Result<(), M::Error> {
// Get size hint if available
let size = map.size_hint();
// Get next key only
while let Some(key) = map.next_key()? {
// Get corresponding value
let value = map.next_value()?;
println!("Key: {:?}, Value: {:?}", key, value);
}
// Or get key-value pair together
while let Some((key, value)) = map.next_entry()? {
println!("Entry: {:?} => {:?}", key, value);
}
Ok(())
}MapAccess provides sequential iteration over map entries.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, Error};
use std::collections::HashMap;
use std::fmt;
struct CaseInsensitiveVisitor;
impl<'de> Visitor<'de> for CaseInsensitiveVisitor {
type Value = HashMap<String, i32>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map with string keys")
}
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, value)) = map.next_entry::<String, i32>()? {
// Convert key to lowercase for case-insensitive handling
let normalized_key = key.to_lowercase();
result.insert(normalized_key, value);
}
Ok(result)
}
}
fn example_json() {
// JSON: {"Name": 1, "NAME": 2, "name": 3}
// Result: {"name": 3} (last value wins)
}Custom logic during iteration enables key transformation.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, Error};
use std::fmt;
#[derive(Debug)]
struct Config {
server_host: String,
server_port: u16,
debug_mode: bool,
}
struct ConfigVisitor;
impl<'de> Visitor<'de> for ConfigVisitor {
type Value = Config;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a config object")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut server_host = None;
let mut server_port = None;
let mut debug_mode = false; // Default value
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"serverHost" | "server_host" | "host" => {
server_host = Some(map.next_value()?);
}
"serverPort" | "server_port" | "port" => {
server_port = Some(map.next_value()?);
}
"debugMode" | "debug_mode" | "debug" => {
debug_mode = map.next_value()?;
}
_ => {
// Skip unknown fields
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
Ok(Config {
server_host: server_host.ok_or_else(|| {
Error::custom("missing field 'serverHost'")
})?,
server_port: server_port.ok_or_else(|| {
Error::custom("missing field 'serverPort'")
})?,
debug_mode,
})
}
}
impl<'de> Deserialize<'de> for Config {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(ConfigVisitor)
}
}visit_map enables flexible field naming and default values.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, Error};
use std::collections::HashMap;
use std::fmt;
struct NestedConfig {
database: DatabaseConfig,
cache: CacheConfig,
}
#[derive(Default)]
struct DatabaseConfig {
host: String,
port: u16,
}
#[derive(Default)]
struct CacheConfig {
enabled: bool,
ttl_seconds: u64,
}
struct NestedConfigVisitor;
impl<'de> Visitor<'de> for NestedConfigVisitor {
type Value = NestedConfig;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a nested config object")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut database = DatabaseConfig::default();
let mut cache = CacheConfig::default();
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"database" => {
// Deserialize nested map
database = map.next_value()?;
}
"cache" => {
cache = map.next_value()?;
}
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
Ok(NestedConfig { database, cache })
}
}Nested maps are handled by recursively deserializing values.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, Error};
use std::fmt;
struct ValidatedUser {
username: String,
email: String,
age: u8,
}
struct ValidatedUserVisitor;
impl<'de> Visitor<'de> for ValidatedUserVisitor {
type Value = ValidatedUser;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a user object with validated fields")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut username = None;
let mut email = None;
let mut age = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"username" => {
let value: String = map.next_value()?;
// Validate during deserialization
if value.len() < 3 {
return Err(Error::custom("username must be at least 3 characters"));
}
if !value.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(Error::custom("username can only contain alphanumeric characters and underscores"));
}
username = Some(value);
}
"email" => {
let value: String = map.next_value()?;
if !value.contains('@') {
return Err(Error::custom("invalid email format"));
}
email = Some(value);
}
"age" => {
let value: u8 = map.next_value()?;
if value < 13 {
return Err(Error::custom("user must be at least 13 years old"));
}
age = Some(value);
}
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
Ok(ValidatedUser {
username: username.ok_or_else(|| Error::custom("missing 'username'"))?,
email: email.ok_or_else(|| Error::custom("missing 'email'"))?,
age: age.ok_or_else(|| Error::custom("missing 'age'"))?,
})
}
}Validation errors can be returned immediately during deserialization.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, Error};
use std::collections::HashMap;
use std::fmt;
enum Value {
String(String),
Number(f64),
Boolean(bool),
Nested(HashMap<String, Value>),
}
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
type Value = Value;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a JSON value")
}
// Handle other types...
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
// Recursively deserialize nested map
let inner = HashMapVisitor.deserialize_map(map)?;
Ok(Value::Nested(inner))
}
}
struct HashMapVisitor;
impl<'de> Visitor<'de> for HashMapVisitor {
type Value = HashMap<String, Value>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map")
}
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>()? {
// Each value can be any Value variant
let value: Value = map.next_value()?;
result.insert(key, value);
}
Ok(result)
}
}Recursive deserialization handles nested heterogeneous structures.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, Error};
use std::collections::HashMap;
use std::fmt;
struct FlattenedConfig {
// Standard fields
name: String,
version: String,
// All other fields go here
extra: HashMap<String, serde_json::Value>,
}
struct FlattenedConfigVisitor;
impl<'de> Visitor<'de> for FlattenedConfigVisitor {
type Value = FlattenedConfig;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a config object with extra fields")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut name = None;
let mut version = None;
let mut extra = HashMap::new();
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"name" => name = Some(map.next_value()?),
"version" => version = Some(map.next_value()?),
_ => {
// Capture unknown fields as raw JSON
let value: serde_json::Value = map.next_value()?;
extra.insert(key, value);
}
}
}
Ok(FlattenedConfig {
name: name.ok_or_else(|| Error::custom("missing 'name'"))?,
version: version.ok_or_else(|| Error::custom("missing 'version'"))?,
extra,
})
}
}Capture unknown fields into a generic map for flexible schemas.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess};
use std::fmt;
// Zero-copy struct borrows from input
#[derive(Debug)]
struct BorrowedConfig<'a> {
name: &'a str,
value: &'a str,
}
struct BorrowedConfigVisitor;
impl<'de> Visitor<'de> for BorrowedConfigVisitor {
type Value = BorrowedConfig<'de>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a borrowed config")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut name = None;
let mut value = None;
while let Some(key) = map.next_key::<&str>()? {
match key {
"name" => name = Some(map.next_value()?),
"value" => value = Some(map.next_value()?),
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
Ok(BorrowedConfig {
name: name.ok_or_else(|| serde::de::Error::custom("missing name"))?,
value: value.ok_or_else(|| serde::de::Error::custom("missing value"))?,
})
}
}&str keys and values borrow directly from input, avoiding allocations.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, Error};
use std::fmt;
enum Status {
Active { id: u32, reason: String },
Inactive { id: u32, since: String },
Pending { id: u32 },
}
struct StatusVisitor;
impl<'de> Visitor<'de> for StatusVisitor {
type Value = Status;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a status object with type field")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut status_type = None;
let mut id = None;
let mut reason = None;
let mut since = None;
// First pass: collect all fields
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"type" => status_type = Some(map.next_value()?),
"id" => id = Some(map.next_value()?),
"reason" => reason = Some(map.next_value()?),
"since" => since = Some(map.next_value()?),
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
let id = id.ok_or_else(|| Error::custom("missing 'id'"))?;
let status_type = status_type.ok_or_else(|| Error::custom("missing 'type'"))?;
// Determine variant based on type field
match status_type.as_str() {
"active" => {
Ok(Status::Active {
id,
reason: reason.unwrap_or_default(),
})
}
"inactive" => {
Ok(Status::Inactive {
id,
since: since.ok_or_else(|| Error::custom("missing 'since'"))?,
})
}
"pending" => Ok(Status::Pending { id }),
_ => Err(Error::custom(format!("unknown status type: {}", status_type))),
}
}
}Enums can be deserialized from maps with a discriminator field.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess};
use std::collections::HashMap;
use std::fmt;
struct OptimizedMapVisitor;
impl<'de> Visitor<'de> for OptimizedMapVisitor {
type Value = HashMap<String, String>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
// Use size hint to pre-allocate
let size_hint = map.size_hint().unwrap_or(0);
let mut result = HashMap::with_capacity(size_hint);
while let Some((key, value)) = map.next_entry()? {
result.insert(key, value);
}
Ok(result)
}
}size_hint() enables pre-allocation for better performance.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, IgnoredAny};
use std::fmt;
struct StrictConfig {
allowed_field: String,
}
struct StrictConfigVisitor;
impl<'de> Visitor<'de> for StrictConfigVisitor {
type Value = StrictConfig;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a strict config object")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut allowed_field = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"allowed_field" => {
allowed_field = Some(map.next_value()?);
}
_ => {
// Silently ignore unknown fields
// IgnoredAny is more efficient than a concrete type
map.next_value::<IgnoredAny>()?;
}
}
}
Ok(StrictConfig {
allowed_field: allowed_field.unwrap_or_default(),
})
}
}IgnoredAny efficiently skips unknown fields without parsing them fully.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, Error};
use std::fmt;
struct ErrorHandlingVisitor;
impl<'de> Visitor<'de> for ErrorHandlingVisitor {
type Value = Result<String, String>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an object with result field")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut success = None;
let mut error = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"success" => success = Some(map.next_value()?),
"error" => error = Some(map.next_value()?),
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
// Custom validation logic
match (success, error) {
(Some(value), None) => Ok(Ok(value)),
(None, Some(err)) => Ok(Err(err)),
(Some(_), Some(_)) => Err(Error::custom("both 'success' and 'error' present")),
(None, None) => Err(Error::custom("missing both 'success' and 'error'")),
}
}
}Complex error conditions can be validated during deserialization.
use serde::de::{Deserialize, Deserializer, Visitor, MapAccess, Error};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug)]
struct EnvConfig {
database_url: String,
max_connections: u32,
feature_flags: HashMap<String, bool>,
optional_setting: Option<String>,
}
struct EnvConfigVisitor;
impl<'de> Visitor<'de> for EnvConfigVisitor {
type Value = EnvConfig;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("environment configuration")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut database_url = None;
let mut max_connections: u32 = 10; // Default
let mut feature_flags = HashMap::new();
let mut optional_setting = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"DATABASE_URL" | "database_url" | "dbUrl" => {
database_url = Some(map.next_value()?);
}
"MAX_CONNECTIONS" | "max_connections" | "maxConnections" => {
let value: u32 = map.next_value()?;
if value == 0 {
return Err(Error::custom("max_connections must be positive"));
}
max_connections = value;
}
"FEATURE_FLAGS" | "feature_flags" => {
feature_flags = map.next_value()?;
}
"OPTIONAL_SETTING" | "optional_setting" => {
optional_setting = Some(map.next_value()?);
}
_ => {
// Unknown env var, skip
map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
Ok(EnvConfig {
database_url: database_url
.ok_or_else(|| Error::custom("DATABASE_URL is required"))?,
max_connections,
feature_flags,
optional_setting,
})
}
}
impl<'de> Deserialize<'de> for EnvConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(EnvConfigVisitor)
}
}Environment-style configs often have multiple naming conventions.
Key components:
| Component | Purpose |
|-----------|---------|
| Visitor trait | Defines visit_map for custom deserialization |
| MapAccess trait | Sequential iteration over key-value pairs |
| next_key() | Get next key from map |
| next_value() | Get value corresponding to current key |
| next_entry() | Get key-value pair together |
| size_hint() | Pre-allocation hint |
Common patterns:
| Pattern | Use Case |
|---------|----------|
| Field matching | Flexible key names ("host" or "hostname") |
| Default values | Provide defaults for missing fields |
| Validation | Check constraints during deserialization |
| Unknown field capture | Store unrecognized fields in HashMap |
| Zero-copy | Borrow &str directly from input |
| Nested deserialization | Recursively deserialize inner maps |
Key insight: visit_map is the extension point for map deserialization in Serde, providing full control over how key-value pairs are processed. Unlike derive-based deserialization which follows a fixed schema, visit_map enables arbitrary logic: transforming keys, validating values, handling multiple naming conventions, capturing unknown fields, or implementing heterogeneous maps. The MapAccess parameter provides a streaming interface that yields entries one at a time, supporting both bounded and unbounded maps. This design maintains Serde's zero-copy capabilityâkeys and values can be borrowed directly from the input when lifetimes permit. The method returns Result<Self::Value, M::Error>, allowing custom error messages for validation failures or missing required fields. For complex deserialization needs like tagged enums, flattened structs, or protocol-specific formats, visit_map is the fundamental building block that derive macros ultimately use under the hood.