How does serde::ser::SerializeStructVariant::serialize_field handle enum variant field serialization?
SerializeStructVariant::serialize_field serializes individual fields within a named enum variant, enabling the Serialize trait implementation for struct-like enum variants that require field-by-field serialization. This method is called within the context of a SerializeStructVariant returned by Serializer::serialize_struct_variant, allowing each field to be serialized with its name and value. The pattern mirrors SerializeStruct::serialize_field but operates within the enum variant context, handling the additional complexity of variant identification and nested structure serialization.
The Serialization Context for Enum Variants
use serde::{ser::{Serialize, Serializer, SerializeStructVariant}, Serialize};
// Enum variants come in three forms:
//
// 1. Unit variants: SomeEnum::Variant
// 2. Newtype variants: SomeEnum::Variant(T)
// 3. Struct variants: SomeEnum::Variant { field1: T, field2: U }
//
// SerializeStructVariant handles struct variants:
#[derive(Serialize)]
enum Message {
// Unit variant - uses serialize_unit_variant
Quit,
// Newtype variant - uses serialize_newtype_variant
Id(u64),
// Struct variant - uses serialize_struct_variant + serialize_field
Move {
x: i32,
y: i32,
},
// Complex struct variant
User {
id: u64,
name: String,
email: String,
active: bool,
},
}
// The struct variant Move serializes as:
// JSON: {"Move": {"x": 1, "y": 2}}
//
// Serialization steps:
// 1. Serializer::serialize_struct_variant called with variant name
// 2. Returns SerializeStructVariant instance
// 3. serialize_field called for each field (x, y)
// 4. end() called to complete serialization
fn show_enum_serialization() {
let msg = Message::Move { x: 10, y: 20 };
// This internally calls:
// 1. serialize_struct_variant("Message", 0, "Move", 2)
// 2. serialize_field("x", &10)
// 3. serialize_field("y", &20)
// 4. end()
let json = serde_json::to_string(&msg).unwrap();
println!("{}", json); // {"Move":{"x":10,"y":20}}
}The SerializeStructVariant trait is the serializer's interface for serializing struct-like enum variants, where serialize_field handles individual field serialization.
The Trait Signature
use serde::ser::{Error, SerializeStructVariant};
use serde::__private::doc::ser::SerializeStructVariant as DocSerializeStructVariant;
// The SerializeStructVariant trait:
//
// pub trait SerializeStructVariant {
// type Ok;
// type Error: Error;
//
// fn serialize_field<T: ?Sized + Serialize>(
// &mut self,
// key: &'static str,
// value: &T,
// ) -> Result<(), Self::Error>;
//
// fn end(self) -> Result<Self::Ok, Self::Error>;
// }
//
// Key components:
// - serialize_field: Called for each field with name and value
// - end: Called after all fields to complete serialization
// - Takes &mut self: Fields serialized incrementally
// - Returns Result with serializer-specific Ok type
// Implementing Serialize for a struct variant manually:
struct Point {
x: i32,
y: i32,
}
enum Shape {
Point(Point),
Rectangle {
width: u32,
height: u32,
},
}
impl Serialize for Shape {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Shape::Point(p) => {
// Newtype variant - different method
serializer.serialize_newtype_variant("Shape", 0, "Point", p)
}
Shape::Rectangle { width, height } => {
// Struct variant - uses SerializeStructVariant
let mut sv = serializer.serialize_struct_variant(
"Shape", // Type name
1, // Variant index
"Rectangle", // Variant name
2, // Number of fields
)?;
// Serialize each field
sv.serialize_field("width", width)?;
sv.serialize_field("height", height)?;
// Complete serialization
sv.end()
}
}
}
}
fn show_manual_implementation() {
let shape = Shape::Rectangle { width: 100, height: 50 };
let json = serde_json::to_string(&shape).unwrap();
println!("{}", json); // {"Rectangle":{"width":100,"height":50}}
}The serialize_field method takes a field name and value, serializing them incrementally within the variant context.
Implementing a Custom Serializer for Struct Variants
use serde::ser::{self, Serializer, SerializeStructVariant, Error};
use std::io::Write;
// Custom serializer that shows struct variant serialization
struct SimpleJsonSerializer;
impl Error for SimpleJsonSerializer {
type Error = String;
fn custom<T: std::fmt::Display>(msg: T) -> Self::Error {
msg.to_string()
}
}
impl Serializer for SimpleJsonSerializer {
type Ok = String;
type Error = String;
type SerializeStructVariant = StructVariantSerializer;
// ... other methods omitted ...
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
variant_name: &'static str,
_num_fields: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
// Start the variant structure
// JSON format: {"VariantName": {fields...}}
Ok(StructVariantSerializer {
variant_name: variant_name.to_string(),
fields: Vec::new(),
})
}
}
struct StructVariantSerializer {
variant_name: String,
fields: Vec<(String, String)>,
}
impl SerializeStructVariant for StructVariantSerializer {
type Ok = String;
type Error = String;
fn serialize_field<T: ?Sized + ser::Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
// Serialize each field
let serialized = value.serialize(SimpleJsonSerializer)?;
self.fields.push((key.to_string(), serialized));
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
// Build the final JSON structure
let fields_json: Vec<String> = self.fields
.into_iter()
.map(|(k, v)| format!("\"{}\":{}", k, v))
.collect();
let fields_str = fields_json.join(",");
// Wrap in variant name
Ok(format!("{{\"{}\":{{{}}}}}", self.variant_name, fields_str))
}
}A custom serializer implementation shows how serialize_field collects fields incrementally before end() produces the final output.
Derive Attribute and Generated Code
use serde::Serialize;
// When using #[derive(Serialize)], the generated code for struct variants:
#[derive(Serialize)]
enum Event {
// Generated serialization uses serialize_struct_variant
KeyEvent {
key_code: u32,
pressed: bool,
},
}
// The derived implementation is equivalent to:
impl Serialize for Event {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Event::KeyEvent { key_code, pressed } => {
// Create struct variant serializer
let mut sv = serializer.serialize_struct_variant(
"Event", // Enum name
0, // Variant index
"KeyEvent", // Variant name
2, // Field count
)?;
// Serialize each field
sv.serialize_field("key_code", key_code)?;
sv.serialize_field("pressed", pressed)?;
// Complete
sv.end()
}
}
}
}
// Field order matches definition order
// Field names are &'static str (compile-time constants)
#[derive(Serialize)]
enum Complex {
Data {
id: u64,
name: String,
values: Vec<i32>,
metadata: Option<String>,
},
}
fn show_complex_serialization() {
let event = Complex::Data {
id: 1,
name: "test".to_string(),
values: vec![1, 2, 3],
metadata: Some("meta".to_string()),
};
let json = serde_json::to_string_pretty(&event).unwrap();
println!("{}", json);
// {
// "Data": {
// "id": 1,
// "name": "test",
// "values": [1, 2, 3],
// "metadata": "meta"
// }
// }
}The derive macro generates calls to serialize_struct_variant and serialize_field in field definition order, using static field names.
Field Ordering and Skip Attributes
use serde::{Serialize, Serializer, ser::SerializeStructVariant};
#[derive(Serialize)]
enum Config {
Server {
// Fields serialized in definition order
host: String,
port: u16,
// Skip attributes prevent serialization
#[serde(skip)]
internal_state: String,
// Skip if None
#[serde(skip_serializing_if = "Option::is_none")]
timeout: Option<u32>,
// Rename field
#[serde(rename = "maxConn")]
max_connections: u32,
},
}
fn show_field_attributes() {
let config = Config::Server {
host: "localhost".to_string(),
port: 8080,
internal_state: "secret".to_string(), // Not serialized
timeout: None, // Not serialized (skip_serializing_if)
max_connections: 100,
};
let json = serde_json::to_string(&config).unwrap();
println!("{}", json);
// {"Server":{"host":"localhost","port":8080,"maxConn":100}}
// Note:
// - internal_state skipped entirely
// - timeout omitted because None
// - max_connections renamed to maxConn
}
// Custom serialization with conditional fields:
impl Serialize for Config {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Config::Server { host, port, internal_state: _, timeout, max_connections } => {
// Count non-skipped fields
let field_count = 2 + timeout.as_ref().map(|_| 1).unwrap_or(0) + 1;
let mut sv = serializer.serialize_struct_variant(
"Config",
0,
"Server",
field_count,
)?;
// Always serialize
sv.serialize_field("host", host)?;
sv.serialize_field("port", port)?;
// Skip internal_state entirely
// Conditionally serialize
if let Some(t) = timeout {
sv.serialize_field("timeout", t)?;
}
// Rename on serialization
sv.serialize_field("maxConn", max_connections)?;
sv.end()
}
}
}
}Attributes like skip, skip_serializing_if, and rename modify how serialize_field is called in generated code.
Different Format Representations
use serde::{Serialize, Serializer, ser::SerializeStructVariant};
#[derive(Serialize)]
enum Status {
Active {
user_id: u64,
since: String,
},
}
fn show_format_differences() {
let status = Status::Active {
user_id: 123,
since: "2024-01-01".to_string(),
};
// JSON: {"Active": {"user_id": 123, "since": "2024-01-01"}}
let json = serde_json::to_string(&status).unwrap();
println!("JSON: {}", json);
// Other formats represent struct variants differently:
//
// Bincode: Variant index + field values (binary)
// MessagePack: Array with variant index + field values
// XML: <Active><user_id>123</user_id><since>2024-01-01</since></Active>
//
// The Serializer implementation decides the format.
// serialize_field provides the interface for each field.
}
// Each format implements SerializeStructVariant differently:
// JSON format:
// {"VariantName": {"field1": value1, "field2": value2}}
// - Variant wrapped in object with variant name key
// - Fields are object properties
// Externally tagged (JSON alternative):
// {"VariantName": {"field1": value1, "field2": value2}}
// Same as above (serde_json default)
// Internally tagged:
// {"field1": value1, "field2": value2, "type": "VariantName"}
// - Fields at top level
// - Variant name in special "type" field
// Adjacently tagged:
// {"t": "VariantName", "c": {"field1": value1, "field2": value2}}
// - Variant name and content separated
// Untagged:
// {"field1": value1, "field2": value2}
// - No variant name, just fields
// - Used for deserialization flexibility
#[derive(Serialize)]
#[serde(tag = "type")] // Internally tagged
enum TaggedStatus {
Active { user_id: u64, since: String },
Inactive { reason: String },
}
fn show_internally_tagged() {
let status = TaggedStatus::Active {
user_id: 123,
since: "2024-01-01".to_string(),
};
let json = serde_json::to_string(&status).unwrap();
println!("Internally tagged: {}", json);
// {"user_id":123,"since":"2024-01-01","type":"Active"}
// Note: Internally tagged uses SerializeStruct, not SerializeStructVariant
// The variant name becomes a regular field
}Different serialization formats implement SerializeStructVariant to produce their specific representations of enum variants.
Nested Struct Variants
use serde::{Serialize, Serializer, ser::SerializeStructVariant};
#[derive(Serialize)]
enum Command {
Create {
entity: Entity,
options: CreateOptions,
},
}
#[derive(Serialize)]
struct Entity {
name: String,
value: i32,
}
#[derive(Serialize)]
struct CreateOptions {
overwrite: bool,
validate: bool,
}
fn show_nested_serialization() {
let cmd = Command::Create {
entity: Entity {
name: "item".to_string(),
value: 42,
},
,
options: CreateOptions {
overwrite: true,
validate: false,
},
};
let json = serde_json::to_string_pretty(&cmd).unwrap();
println!("{}", json);
// {
// "Create": {
// "entity": {
// "name": "item",
// "value": 42
// },
// "options": {
// "overwrite": true,
// "validate": false
// }
// }
// }
}
// Nested serialization flows naturally:
// 1. serialize_struct_variant("Command", 0, "Create", 2)
// 2. serialize_field("entity", &entity)
// -> entity.serialize() -> serialize_struct("Entity", 2) -> ...
// 3. serialize_field("options", &options)
// -> options.serialize() -> serialize_struct("CreateOptions", 2) -> ...
// 4. end()
// Each field's value serializes recursively through the same patternNested structures serialize recursivelyβeach serialize_field call invokes the field's Serialize implementation, which may itself call more serialization methods.
Error Handling During Field Serialization
use serde::{ser::{self, Serializer, SerializeStructVariant, Error}, Serialize};
// Errors during serialize_field propagate up:
enum Data {
Record { id: u64, value: String },
}
impl Serialize for Data {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Data::Record { id, value } => {
let mut sv = serializer.serialize_struct_variant(
"Data", 0, "Record", 2
)?;
// Error if serialization fails
sv.serialize_field("id", id)?;
sv.serialize_field("value", value)?;
sv.end()
}
}
}
}
// Common error scenarios:
//
// 1. Type conversion errors
// - Integer overflow in format that doesn't support full range
// - Invalid characters in string
//
// 2. IO errors (for serializers writing to streams)
// - Disk full
// - Connection closed
//
// 3. Custom validation errors
// - Field-specific validation failure
// Example with custom validation:
struct ValidatedField {
value: String,
}
impl Serialize for ValidatedField {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Validate before serializing
if self.value.len() > 100 {
return Err(S::Error::custom("field too long"));
}
self.value.serialize(serializer)
}
}
#[derive(Serialize)]
enum Container {
Item {
#[serde(serialize_with = "validate_item")]
data: String,
},
}
fn validate_item<S>(value: &str, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if value.contains(|c: char| !c.is_ascii()) {
return Err(S::Error::custom("only ASCII characters allowed"));
}
value.serialize(serializer)
}
// The ? operator propagates errors from serialize_field
// Each field serialization can fail independentlyErrors from serialize_field propagate through the ? operator, allowing the serializer to report issues like overflow, IO errors, or custom validation failures.
Comparison with Other Serialization Methods
use serde::{Serialize, Serializer, ser::{SerializeStruct, SerializeStructVariant}};
// serialize_field vs other serialization methods:
enum Example {
// Unit variant: serialize_unit_variant
Unit,
// Newtype variant: serialize_newtype_variant
Newtype(String),
// Struct variant: serialize_struct_variant + serialize_field
Struct { field1: i32, field2: String },
// Tuple variant: serialize_tuple_variant
Tuple(i32, String, bool),
}
impl Serialize for Example {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Example::Unit => {
// Unit variant - no fields
serializer.serialize_unit_variant("Example", 0, "Unit")
}
Example::Newtype(s) => {
// Newtype variant - single unnamed field
serializer.serialize_newtype_variant("Example", 1, "Newtype", s)
}
Example::Struct { field1, field2 } => {
// Struct variant - named fields
let mut sv = serializer.serialize_struct_variant(
"Example", 2, "Struct", 2
)?;
sv.serialize_field("field1", field1)?;
sv.serialize_field("field2", field2)?;
sv.end()
}
Example::Tuple(a, b, c) => {
// Tuple variant - unnamed fields
let mut tv = serializer.serialize_tuple_variant(
"Example", 3, "Tuple", 3
)?;
tv.serialize_element(a)?;
tv.serialize_element(b)?;
tv.serialize_element(c)?;
tv.end()
}
}
}
}
// Key differences:
//
// serialize_unit_variant:
// - No fields
// - Single call, no builder pattern
//
// serialize_newtype_variant:
// - Single field (unnamed)
// - Single call with value
//
// serialize_struct_variant:
// - Named fields
// - Builder pattern: create -> serialize_field each -> end
// - Field names included in serialization
//
// serialize_tuple_variant:
// - Unnamed fields
// - Builder pattern: create -> serialize_element each -> end
// - No field names, just values
// Compare to SerializeStruct (non-enum):
struct RegularStruct {
field1: i32,
field2: String,
}
impl Serialize for RegularStruct {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Uses SerializeStruct (not SerializeStructVariant)
let mut s = serializer.serialize_struct("RegularStruct", 2)?;
s.serialize_field("field1", &self.field1)?;
s.serialize_field("field2", &self.field2)?;
s.end()
}
}
// The difference is enum context:
// - SerializeStruct: regular struct
// - SerializeStructVariant: enum variant with named fieldsserialize_struct_variant and its serialize_field method specifically handle enum variants with named fields, distinct from unit variants, newtype variants, tuple variants, and regular structs.
Summary
use serde::{Serialize, Serializer, ser::SerializeStructVariant};
fn summary() {
// === SerializeStructVariant::serialize_field ===
//
// Purpose: Serialize a single field within a struct-like enum variant
//
// Context:
// - Called within Serialize implementation for enum variants
// - Part of builder pattern: create -> serialize_field each -> end
// - Handles named fields only (not tuple elements)
//
// Signature:
// fn serialize_field<T: ?Sized + Serialize>(
// &mut self,
// key: &'static str, // Field name (compile-time constant)
// value: &T, // Field value
// ) -> Result<(), Self::Error>;
//
// Flow:
// 1. Serializer::serialize_struct_variant(name, index, variant, len)
// -> returns SerializeStructVariant
// 2. sv.serialize_field("field1", &value1)?
// 3. sv.serialize_field("field2", &value2)?
// 4. ... for each field
// 5. sv.end() -> final serialized output
//
// Key characteristics:
// - Field names are static strings
// - Fields serialized in definition order (by default)
// - Each field value uses its own Serialize implementation
// - Errors propagate with ?
// - Takes &mut self for incremental building
//
// Comparison with related methods:
//
// ββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββ
// β Method β Use Case β
// ββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββ€
// β serialize_unit_variant β Unit enum variant (no data) β
// β serialize_newtype_variant β Newtype variant (single unnamed field)β
// β serialize_struct_variant β Struct variant (named fields) β
// β ββ serialize_field β ββ Serialize each named field β
// β serialize_tuple_variant β Tuple variant (unnamed fields) β
// β ββ serialize_element β ββ Serialize each unnamed field β
// β serialize_struct β Regular struct (not enum variant) β
// β ββ serialize_field β ββ Serialize each named field β
// ββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββ
//
// Format differences:
// - JSON: {"VariantName": {"field": value}}
// - Bincode: Variant index + field values (binary)
// - Each format implements SerializeStructVariant
//
// Attributes affecting serialize_field:
// - #[serde(skip)]: Field not serialized
// - #[serde(skip_serializing_if)]: Conditional serialization
// - #[serde(rename)]: Different field name in output
//
// Common patterns:
// - Use #[derive(Serialize)] for automatic implementation
// - Manual impl when custom serialization needed
// - Compose with nested structures naturally
}Key insight: SerializeStructVariant::serialize_field is the per-field hook for serializing named fields within enum variants, called repeatedly within a builder pattern that starts with serialize_struct_variant and ends with end(). The method takes a static field name and the value, letting each format implement how to represent field data within a variant context. This enables different serializers to represent struct variants appropriatelyβJSON wraps them as {"VariantName": {"field": value}}, while binary formats might use indices and packed values. The derive macro handles the common case automatically, but manual implementations provide full control over field ordering, conditional inclusion, and custom validation.
