What is the purpose of serde::de::Visitor::visit_newtype_struct for newtype deserialization patterns?
visit_newtype_struct is the Visitor trait method that Serde's deserializer calls when encountering a newtype struct during deserialization, allowing you to access the inner value and reconstruct the newtype wrapper. In Serde's data model, a newtype struct is represented as a named tuple struct containing exactly one fieldâwhen a deserializer sees #[derive(Deserialize)] on a struct Wrapper(T), it calls visit_newtype_struct with a sub-deserializer for the inner type T. This design enables custom deserialization logic for newtypes: you can apply transformations, validations, or custom parsing to the inner value before wrapping it. Without implementing visit_newtype_struct, the default Visitor implementation would fail to deserialize newtype structs because it doesn't know how to handle themâyou must provide this method when implementing a Visitor that encounters newtype structs in your data format.
What Is a Newtype Struct in Serde
use serde::{Deserialize, Serialize, Deserializer, de::Visitor};
use std::fmt;
// A newtype struct is a tuple struct with exactly one field
// This pattern wraps a type to add semantic meaning or custom behavior
#[derive(Debug, Serialize, Deserialize)]
struct UserId(u64);
#[derive(Debug, Serialize, Deserialize)]
struct Email(String);
#[derive(Debug, Serialize, Deserialize)]
struct Percentage(f64);
// These are NOT newtype structs (more than one field):
// #[derive(Serialize, Deserialize)]
// struct Point(i32, i32); // Tuple struct with 2 fields - NOT a newtype
// This IS a newtype struct:
// #[derive(Serialize, Deserialize)]
// struct Wrapper(i32); // Exactly one field - IS a newtype
fn main() {
let user_id = UserId(42);
let email = Email("user@example.com".to_string());
let percent = Percentage(0.75);
println!("UserId: {:?}", user_id);
println!("Email: {:?}", email);
println!("Percentage: {:?}", percent);
// When serialized, newtypes are represented as their inner value
// UserId serializes as just the u64: 42
// Email serializes as just the String: "user@example.com"
}Newtype structs wrap exactly one field; Serde treats them specially during serialization and deserialization.
The Default Derive Implementation
use serde::{Deserialize, Serialize};
// When you derive Deserialize, Serde generates code that:
// 1. Expects the deserializer to call visit_newtype_struct
// 2. Deserializes the inner value
// 3. Wraps it in the newtype constructor
#[derive(Debug, Serialize, Deserialize)]
struct UserId(u64);
// What #[derive(Deserialize)] generates conceptually:
// impl<'de> Deserialize<'de> for UserId {
// fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
// where D: Deserializer<'de>
// {
// deserializer.deserialize_newtype_struct("UserId", UserIdVisitor)
// }
// }
//
// struct UserIdVisitor;
//
// impl<'de> Visitor<'de> for UserIdVisitor {
// fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
// where D: Deserializer<'de>
// {
// let inner: u64 = Deserialize::deserialize(deserializer)?;
// Ok(UserId(inner)) // Wrap the inner value
// }
// }
fn main() {
// When deserializing, the inner value is extracted and wrapped
let json = "42";
let user_id: UserId = serde_json::from_str(json).unwrap();
println!("Deserialized: {:?}", user_id);
// The serialized form is just the inner value
let serialized = serde_json::to_string(&user_id).unwrap();
println!("Serialized: {}", serialized); // "42", not {"UserId":42}
}The derive macro handles newtype deserialization by deserializing the inner value and wrapping it.
The Visitor Trait's visit_newtype_struct Method
use serde::de::{Visitor, Deserializer, Error};
use std::fmt;
// The Visitor trait defines visit_newtype_struct for handling newtypes
struct MyVisitor;
impl<'de> Visitor<'de> for MyVisitor {
type Value = String; // What this visitor produces
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a string or a newtype struct containing a string")
}
// This method is called when the deserializer encounters a newtype struct
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// The deserializer gives us access to the inner value
// We deserialize the inner type and can transform it
let inner: String = Deserialize::deserialize(deserializer)?;
// We could apply transformations here
// For example, validate, transform, or enrich the value
Ok(inner)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(v.to_string())
}
}
use serde::Deserialize;
fn main() {
// This demonstrates how visit_newtype_struct fits into the visitor pattern
println!("Visitor defined with visit_newtype_struct");
}visit_newtype_struct receives a deserializer for the inner value, letting you deserialize and transform it.
Custom Newtype Deserialization
use serde::{Deserialize, Deserializer, Serialize};
use serde::de::{Visitor, Error};
use std::fmt;
use std::marker::PhantomData;
// A validated wrapper that ensures the inner value is non-negative
#[derive(Debug)]
struct NonNegative<T>(T);
// Custom deserialization that validates the value
impl<'de, T> Deserialize<'de> for NonNegative<T>
where
T: Deserialize<'de> + PartialOrd + Default + std::ops::Neg<Output = T> + Copy,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// Tell the deserializer we're expecting a newtype struct
deserializer.deserialize_newtype_struct("NonNegative", NonNegativeVisitor {
_marker: PhantomData,
})
}
}
struct NonNegativeVisitor<T> {
_marker: PhantomData<T>,
}
impl<'de, T> Visitor<'de> for NonNegativeVisitor<T>
where
T: Deserialize<'de> + PartialOrd + Default + Copy,
{
type Value = NonNegative<T>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a non-negative number")
}
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let value: T = Deserialize::deserialize(deserializer)?;
// Validate that the value is non-negative
if value < T::default() {
return Err(D::Error::custom("value must be non-negative"));
}
Ok(NonNegative(value))
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: Error,
{
if v < 0 {
return Err(E::custom("value must be non-negative"));
}
Ok(NonNegative(v as T))
}
}
fn main() {
// Valid value
let json = "42";
let result: Result<NonNegative<i64>, _> = serde_json::from_str(json);
println!("Valid: {:?}", result);
// Invalid value (negative)
let json = "-5";
let result: Result<NonNegative<i64>, _> = serde_json::from_str(json);
println!("Invalid: {:?}", result);
}Custom visit_newtype_struct implementations can validate or transform the inner value during deserialization.
Deserializing Without the Wrapper
use serde::{Deserialize, Deserializer};
use serde::de::Visitor;
use std::fmt;
// Sometimes the serialized format doesn't include the wrapper
// but you want to deserialize into a newtype
#[derive(Debug)]
struct Port(u16);
// Custom deserialization that reads a number directly
impl<'de> Deserialize<'de> for Port {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_u16(PortVisitor)
}
}
struct PortVisitor;
impl<'de> Visitor<'de> for PortVisitor {
type Value = Port;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a port number (0-65535)")
}
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Handle being called as a newtype struct
let port: u16 = Deserialize::deserialize(deserializer)?;
if port > 65535 {
return Err(D::Error::custom("port out of range"));
}
Ok(Port(port))
}
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E> {
Ok(Port(v))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v > 65535 {
return Err(E::custom("port out of range"));
}
Ok(Port(v as u16))
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v < 0 || v > 65535 {
return Err(E::custom("port out of range"));
}
Ok(Port(v as u16))
}
}
fn main() {
// Deserialize from a plain number
let json = "8080";
let port: Port = serde_json::from_str(json).unwrap();
println!("Port: {:?}", port);
// Works because we implement the numeric visit methods too
}You can deserialize newtypes from formats that don't explicitly use newtype representation.
Transforming Inner Values
use serde::{Deserialize, Deserializer};
use serde::de::Visitor;
use std::fmt;
// A newtype that normalizes its value during deserialization
#[derive(Debug)]
struct Lowercase(String);
impl<'de> Deserialize<'de> for Lowercase {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_newtype_struct("Lowercase", LowercaseVisitor)
}
}
struct LowercaseVisitor;
impl<'de> Visitor<'de> for LowercaseVisitor {
type Value = Lowercase;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a string")
}
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let inner: String = Deserialize::deserialize(deserializer)?;
Ok(Lowercase(inner.to_lowercase()))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Lowercase(v.to_lowercase()))
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E> {
Ok(Lowercase(v.to_lowercase()))
}
}
fn main() {
let json = r#""HeLLo WoRLD""#;
let lowercase: Lowercase = serde_json::from_str(json).unwrap();
println!("Lowercase: {:?}", lowercase); // Lowercase("hello world")
}visit_newtype_struct can transform the inner value before constructing the newtype.
Forwarding to Inner Type's Visitor
use serde::{Deserialize, Deserializer};
use serde::de::Visitor;
use std::fmt;
// A wrapper that adds logging on deserialization
#[derive(Debug)]
struct Logged<T>(T);
struct LoggedVisitor<T> {
_inner: std::marker::PhantomData<T>,
}
impl<'de, T> Visitor<'de> for LoggedVisitor<T>
where
T: Deserialize<'de>,
{
type Value = Logged<T>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a value of type T")
}
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
// Simply deserialize the inner value and wrap it
// This is what #[derive(Deserialize)] generates for newtypes
let inner: T = Deserialize::deserialize(deserializer)?;
println!("Deserialized inner value");
Ok(Logged(inner))
}
}
impl<'de, T> Deserialize<'de> for Logged<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_newtype_struct("Logged", LoggedVisitor {
_inner: std::marker::PhantomData,
})
}
}
fn main() {
let json = "42";
let logged: Logged<i32> = serde_json::from_str(json).unwrap();
println!("Logged: {:?}", logged);
}Generic newtypes can forward deserialization to the inner type's Deserialize implementation.
Custom Format Implementations
use serde::de::{Deserializer, Visitor};
use std::fmt;
// When implementing a Deserializer, you must call visit_newtype_struct
// when the input represents a newtype struct
// Example: A custom format that represents newtypes as {"type": ..., "value": ...}
#[derive(Debug)]
struct CustomNewtype<T>(T);
struct CustomNewtypeVisitor<T> {
_marker: std::marker::PhantomData<T>,
}
impl<'de, T> Visitor<'de> for CustomNewtypeVisitor<T>
where
T: Deserialize<'de>,
{
type Value = CustomNewtype<T>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a newtype struct")
}
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let inner: T = Deserialize::deserialize(deserializer)?;
Ok(CustomNewtype(inner))
}
}
// When your Deserializer encounters a newtype struct in your format,
// it should call visit_newtype_struct on the visitor
use serde::Deserialize;
fn main() {
// When you implement a Deserializer, you call visit_newtype_struct
// when your format has a newtype representation
// For JSON, newtypes are just their inner value:
// A newtype struct UserId(42) serializes as just 42 in JSON
// The JSON deserializer calls visit_newtype_struct with a deserializer for 42
println!("Custom format Deserializers call visit_newtype_struct");
}Custom Deserializer implementations must call visit_newtype_struct when their format represents a newtype struct.
Visiting Newtypes from Different Formats
use serde::{Deserialize, Deserializer};
use serde::de::Visitor;
use std::fmt;
// Different formats represent newtypes differently:
// - JSON: newtypes are transparent (just the inner value)
// - Some formats: newtypes might have explicit wrapper
#[derive(Debug)]
struct Wrapper(i32);
impl<'de> Deserialize<'de> for Wrapper {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// This visitor can handle being called in different ways
deserializer.deserialize_any(WrapperVisitor)
}
}
struct WrapperVisitor;
impl<'de> Visitor<'de> for WrapperVisitor {
type Value = Wrapper;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "an integer")
}
// Called when format explicitly indicates a newtype struct
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
let value: i32 = Deserialize::deserialize(deserializer)?;
Ok(Wrapper(value))
}
// Called for integer values (JSON newtype representation)
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Wrapper(v as i32))
}
// Also handle other integer types
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Wrapper(v as i32))
}
}
fn main() {
// In JSON, newtypes deserialize from just the inner value
let json = "42";
let wrapper: Wrapper = serde_json::from_str(json).unwrap();
println!("Wrapper from JSON: {:?}", wrapper);
// visit_i64 is called, not visit_newtype_struct,
// because JSON doesn't have explicit newtype representation
}Different formats may call visit_newtype_struct or other visit methods depending on their representation.
The Full Visitor Pattern
use serde::de::Visitor;
use std::fmt;
// Complete visitor showing all newtype-related methods
struct MyNewtype(i32);
struct MyNewtypeVisitor;
impl<'de> Visitor<'de> for MyNewtypeVisitor {
type Value = MyNewtype;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "an integer or a newtype struct containing an integer")
}
// The key method for newtype struct deserialization
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
// The deserializer gives us the inner value
// We deserialize it and wrap it in our type
let inner: i32 = serde::Deserialize::deserialize(deserializer)?;
Ok(MyNewtype(inner))
}
// Also implement scalar visit methods for formats that don't
// have explicit newtype representation (like JSON)
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(MyNewtype(v))
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v < i32::MIN as i64 || v > i32::MAX as i64 {
return Err(E::custom("value out of range for i32"));
}
Ok(MyNewtype(v as i32))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v > i32::MAX as u64 {
return Err(E::custom("value out of range for i32"));
}
Ok(MyNewtype(v as i32))
}
}
fn main() {
// A visitor for a newtype should typically:
// 1. Implement visit_newtype_struct for explicit newtype representation
// 2. Implement the inner type's visit methods for transparent representation
println!("Complete visitor pattern implemented");
}A complete newtype visitor handles both explicit newtype representation and transparent representation.
Synthesis
When visit_newtype_struct is called:
| Format | Representation | Method Called |
|---|---|---|
| JSON | Transparent (just inner value) | visit_i64, visit_str, etc. |
| Bincode | Transparent (just inner value) | Depends on inner type |
| Custom | Format-specific | visit_newtype_struct if format has wrapper |
Typical implementation pattern:
impl<'de> Visitor<'de> for MyNewtypeVisitor {
type Value = MyNewtype;
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where D: Deserializer<'de>
{
// 1. Deserialize inner value
let inner: InnerType = Deserialize::deserialize(deserializer)?;
// 2. Optionally validate or transform
let validated = validate(inner)?;
// 3. Wrap in newtype
Ok(MyNewtype(validated))
}
// Also implement scalar visit methods for transparent formats
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> {
Ok(MyNewtype(v as i32))
}
}Key insight: visit_newtype_struct exists in Serde's Visitor trait because newtype structs are a fundamental pattern in Rust that deserve special treatment during deserialization. The method receives a deserializer for the inner value, giving you full control over how that inner value is deserialized before you wrap it in your newtype. This enables validation (rejecting invalid inner values), transformation (normalizing strings, clamping numbers), logging or side effects, and any other custom logic that should happen during deserialization. For most newtypes, the derive macro generates a visit_newtype_struct implementation that simply deserializes the inner value and passes it to your constructor. But when you need custom behaviorâvalidating that a Port(u16) is in range, normalizing an Email(String) to lowercase, or deserializing from an alternative representationâyou implement visit_newtype_struct yourself. The method is also essential when implementing custom Deserializers: when your format represents a newtype struct (however it chooses to do so), your deserializer must call visit_newtype_struct on the visitor so that any newtype-specific deserialization logic runs correctly.
