How does serde::ser::Serializer::serialize_struct_variant handle enum variant serialization with field-level metadata preservation?
serialize_struct_variant serializes enum variants that contain named fields, delegating field-level metadata handling to a SerializeStructVariant type that processes each field's attributes during serialization. This method is the Serializer trait's entry point for variants like Variant { field1: T1, field2: T2 }, and the returned SerializeStructVariant handles individual field serialization including skip attributes, rename directives, and custom serialization logic.
The Role of serialize_struct_variant
use serde::{Serializer, Serialize};
// Consider an enum with struct variants:
#[derive(Serialize)]
enum Message {
// Unit variant - handled by serialize_unit_variant
Quit,
// Newtype variant - handled by serialize_newtype_variant
Move { x: i32, y: i32 },
// Struct variant - handled by serialize_struct_variant
Write {
#[serde(rename = "msg")]
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
priority: Option<u32>,
},
}
// The Serialize implementation for Message::Write calls:
fn serialize_struct_variant_example<S: Serializer>(serializer: S) -> Result<S::Ok, S::Error> {
// This is what #[derive(Serialize)] generates internally:
let mut sv = serializer.serialize_struct_variant(
"Message", // The name of the enum
0, // Variant index (determined by order or repr)
"Write", // Variant name
2, // Number of fields (before skipping)
)?;
sv.serialize_field("msg", &"hello")?;
// Note: "priority" field skipped if None due to skip_serializing_if
sv.end()
}
// The serializer trait defines:
pub trait Serializer {
type SerializeStructVariant: SerializeStructVariant;
fn serialize_struct_variant(
self,
name: &'static str, // Enum type name
variant_index: u32, // Variant discriminant
variant_name: &'static str, // Variant name
num_fields: usize, // Field count hint
) -> Result<Self::SerializeStructVariant, Self::Error>;
}
// The associated type SerializeStructVariant handles field-by-field serialization:
pub trait SerializeStructVariant {
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error>;
fn end(self) -> Result<Self::Ok, Self::Error>;
}serialize_struct_variant creates a SerializeStructVariant instance that collects fields one by one, allowing the serializer to determine the final format.
Derived Implementation Pattern
use serde::{Serialize, Serializer, ser::SerializeStructVariant};
// What #[derive(Serialize)] generates for a struct variant:
enum Status {
Active {
#[serde(rename = "userId")]
user_id: u64,
#[serde(rename = "createdAt")]
created_at: String,
#[serde(skip)]
internal_state: Vec<u8>,
},
Inactive,
}
// The generated implementation (conceptually):
impl Serialize for Status {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Status::Active { user_id, created_at, internal_state } => {
// Field count after skip_serializing_if evaluation
let field_count = 2; // user_id and created_at, internal_state skipped
let mut sv = serializer.serialize_struct_variant(
"Status",
0, // variant index for Active
"Active",
field_count,
)?;
// Renamed field names are used in serialize_field
sv.serialize_field("userId", user_id)?;
sv.serialize_field("createdAt", created_at)?;
// internal_state not serialized due to #[serde(skip)]
sv.end()
}
Status::Inactive => {
serializer.serialize_unit_variant("Status", 1, "Inactive")
}
}
}
}
// Key insight: the derive macro processes all field attributes
// and generates code that uses the transformed field namesThe derive macro processes #[serde(...)] attributes at compile time, generating code that uses the final field names.
Field Rename Processing
use serde::{Serialize, Serializer};
// Field renames are resolved before serialize_struct_variant is called:
#[derive(Serialize)]
struct UserDetails {
#[serde(rename = "userName")]
user_name: String,
#[serde(rename = "emailAddress")]
email: String,
}
// The rename attribute modifies what's passed to serialize_field:
fn demonstrate_rename<S: Serializer>(details: &UserDetails, serializer: S)
-> Result<S::Ok, S::Error>
{
// The serializer sees the renamed keys, not the Rust field names
// For an enum variant, it would be:
// let mut sv = serializer.serialize_struct_variant(...)?;
// sv.serialize_field("userName", &details.user_name)?; // renamed
// sv.serialize_field("emailAddress", &details.email)?; // renamed
// sv.end()
todo!()
}
// Multiple rename contexts:
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
enum ApiResponse {
UserCreated {
#[serde(rename = "ID")] // Overrides rename_all
id: u64,
user_name: String, // Becomes "userName" from rename_all
},
#[serde(rename = "userDeleted")] // Variant-level rename
UserDeleted {
user_id: u64, // Becomes "userId"
},
}
// The derive macro applies the rename hierarchy:
// 1. Field-specific rename takes precedence
// 2. Variant-level rename_all applies to variant name
// 3. Enum-level rename_all applies to all fields without explicit renameRenames are resolved during code generation; the serializer receives the final names.
Skip Attributes and Field Counting
use serde::{Serialize, Serializer};
// The num_fields parameter to serialize_struct_variant is a hint:
#[derive(Serialize)]
enum Data {
Record {
id: u64,
#[serde(skip)]
cache: Vec<String>, // Never serialized
#[serde(skip_serializing_if = "Option::is_none")]
metadata: Option<String>,
name: String,
},
}
// Generated serialization:
impl Serialize for Data {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Data::Record { id, cache: _, metadata, name } => {
// Field count is determined at serialization time for skip_serializing_if
// But for skip, it's known at compile time
let num_fields = if metadata.is_none() { 2 } else { 3 };
let mut sv = serializer.serialize_struct_variant(
"Data",
0,
"Record",
num_fields, // Hint: 2 or 3 depending on metadata
)?;
sv.serialize_field("id", id)?;
// cache: skipped entirely
if metadata.is_some() {
sv.serialize_field("metadata", metadata)?;
}
sv.serialize_field("name", name)?;
sv.end()
}
}
}
}
// Important: num_fields is a hint, not a contract
// The serializer must handle the actual number of fields serialized
// For skip (always skipped), the field never appears in num_fields
// For skip_serializing_if (conditionally skipped), count variesnum_fields is a hint for serializers to pre-allocate, but actual field counts can differ due to skip_serializing_if.
The SerializeStructVariant Trait
use serde::ser::{SerializeStructVariant, Error};
// SerializeStructVariant is a stateful builder:
struct JsonStructVariant {
// Internal state for building JSON output
enum_name: &'static str,
variant_name: &'static str,
fields: Vec<(&'static str, String)>,
}
impl SerializeStructVariant for JsonStructVariant {
type Ok = String;
type Error = serde_json::Error;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
// Serialize the value to a string
let value_str = serde_json::to_string(value)?;
self.fields.push((key, value_str));
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
// Build final JSON representation
let fields_json: Vec<String> = self.fields
.iter()
.map(|(k, v)| format!("\"{}\":{}", k, v))
.collect();
Ok(format!(
"{{\"{}\":{{\"{}\":{{{}}}}}}}",
self.enum_name,
self.variant_name,
fields_json.join(",")
))
}
}
// This produces output like:
// {"Data":{"Record":{"id":123,"name":"test"}}}
// The trait allows serializers to:
// 1. Accumulate fields in memory
// 2. Write fields to a stream incrementally
// 3. Apply custom formatting
// 4. Track which fields have been serializedSerializeStructVariant accumulates field data and produces the final serialized form when end() is called.
Serializer Implementation Example
use serde::{
Serializer,
ser::{self, SerializeStructVariant, Impossible},
};
use std::fmt;
// A simple serializer showing how serialize_struct_variant works:
struct SimpleSerializer;
impl Serializer for SimpleSerializer {
type Ok = String;
type Error = ser::Error;
type SerializeStructVariant = StructVariantState;
// Other associated types omitted for brevity
fn serialize_struct_variant(
self,
name: &'static str,
_variant_index: u32,
variant_name: &'static str,
_num_fields: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
// Create state to accumulate fields
Ok(StructVariantState {
name,
variant_name,
fields: Vec::new(),
})
}
// Other serialize methods...
}
struct StructVariantState {
name: &'static str,
variant_name: &'static str,
fields: Vec<(&'static str, String)>,
}
impl SerializeStructVariant for StructVariantState {
type Ok = String;
type Error = ser::Error;
fn serialize_field<T: ?Sized + serde::Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
// Each field is serialized with its (possibly renamed) key
// The key comes from the Serialize implementation, after rename processing
let serialized = format!("{:?}", value); // Simplified
self.fields.push((key, serialized));
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
// Combine all fields into final representation
let fields: Vec<String> = self.fields
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect();
Ok(format!(
"{}::{}({})",
self.name,
self.variant_name,
fields.join(", ")
))
}
}
// Output example: "Message::Write(msg=hello)"The serializer decides the format; serialize_struct_variant provides structure while SerializeStructVariant handles field accumulation.
Metadata Preservation Through Field Serialization
use serde::{Serialize, Serializer, ser::SerializeStructVariant};
// Field-level metadata beyond renaming:
#[derive(Serialize)]
enum Config {
Server {
#[serde(rename = "host")]
#[serde(alias = "hostname")] // Only affects deserialization
address: String,
#[serde(rename = "port")]
#[serde(default)] // Only affects deserialization
port: u16,
#[serde(
serialize_with = "serialize_bool_as_int",
rename = "tlsEnabled"
)]
tls: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
routes: Vec<String>,
},
}
fn serialize_bool_as_int<S: Serializer>(b: &bool, s: S) -> Result<S::Ok, S::Error> {
s.serialize_u8(if *b { 1 } else { 0 })
}
// Generated serialization incorporates all serialization metadata:
impl Serialize for Config {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Config::Server { address, port, tls, routes } => {
// Calculate field count considering skip_serializing_if
let mut field_count = 3; // address, port, tls always present
if !routes.is_empty() {
field_count += 1;
}
let mut sv = serializer.serialize_struct_variant(
"Config",
0,
"Server",
field_count,
)?;
sv.serialize_field("host", address)?; // Renamed from "address"
sv.serialize_field("port", port)?; // Renamed from "port"
// Custom serialization for tls field
// The serialize_with function is called, not the field directly
let tls_serialized = if *tls { 1u8 } else { 0u8 };
sv.serialize_field("tlsEnabled", &tls_serialized)?; // Renamed and transformed
if !routes.is_empty() {
sv.serialize_field("routes", routes)?;
}
sv.end()
}
}
}
}
// Metadata that affects serialization:
// - rename: changes field key in output
// - serialize_with: custom serialization function
// - skip_serializing_if: conditional inclusion
// - skip: always exclude
// Metadata that only affects deserialization (not visible here):
// - alias: alternative names for deserialization
// - default: default value for missing fieldsSerialization metadata is applied during Serialize implementation; the serializer sees the transformed values and keys.
Flatten and Nested Structures
use serde::{Serialize, Serializer, ser::SerializeStructVariant};
// Flatten merges fields from nested structures:
#[derive(Serialize)]
struct Timestamps {
created_at: String,
updated_at: String,
}
#[derive(Serialize)]
enum Document {
Article {
title: String,
#[serde(flatten)]
timestamps: Timestamps, // Fields hoisted to parent level
#[serde(flatten)]
metadata: std::collections::HashMap<String, String>,
},
}
// With flatten, the serialization structure changes:
impl Serialize for Document {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Document::Article { title, timestamps, metadata } => {
// num_fields accounts for flattened fields
let field_count = 1 + 2 + metadata.len();
let mut sv = serializer.serialize_struct_variant(
"Document",
0,
"Article",
field_count,
)?;
// Regular field
sv.serialize_field("title", title)?;
// Flattened struct fields
sv.serialize_field("createdAt", ×tamps.created_at)?;
sv.serialize_field("updatedAt", ×tamps.updated_at)?;
// Flattened map entries
for (k, v) in metadata {
// Flatten uses SerializeMap or SerializeStruct
// The field name comes from the map key
sv.serialize_field(k, v)?;
}
sv.end()
}
}
}
}
// Flatten makes nested structure fields appear at the variant level
// The serializer treats flattened fields like regular fieldsflatten hoists fields from nested structures into the parent, changing what serialize_field receives.
Externally Tagged Format
use serde::{Serialize, Serializer};
// Externally tagged is the default enum representation:
#[derive(Serialize)]
enum Event {
Click { x: i32, y: i32 },
KeyPress { key: char },
}
// JSON output:
// {"Click": {"x": 10, "y": 20}}
// {"KeyPress": {"key": "a"}}
// The serializer's format determines this representation:
fn externally_tagged_format() {
// For JSON, serialize_struct_variant produces:
// { variant_name: { field1: value1, field2: value2, ... } }
// The outer object key is the variant name
// The value is an object with the variant's fields
// This is why SerializeStructVariant needs both:
// - variant_name (for the outer key)
// - fields (for the inner object)
}
// How serde_json implements this:
impl serde_json::Serializer {
// serialize_struct_variant creates an object with one key:
// { "variant_name": { /* fields */ } }
}
// The Serialize implementation calls serialize_struct_variant
// which tells the serializer both the variant name and fields
// The serializer decides the actual output formatExternally tagged format uses the variant name as an outer key; serialize_struct_variant provides both the name and fields.
Internally Tagged Format
use serde::ser::{Serializer, SerializeStructVariant};
// Internally tagged moves the variant tag inside:
#[derive(Serialize)]
#[serde(tag = "type")]
enum Event {
Click { x: i32, y: i32 },
KeyPress { key: char },
}
// JSON output:
// {"type": "Click", "x": 10, "y": 20}
// {"type": "KeyPress", "key": "a"}
// The serialize_struct_variant implementation handles this:
impl Serialize for Event {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Event::Click { x, y } => {
// For internally tagged, serializer.serialize_struct_variant
// adds the tag field automatically
let mut sv = serializer.serialize_struct_variant(
"Event",
0,
"Click",
3, // type + x + y
)?;
// Serializer implementation adds "type": "Click"
// Then the regular fields
sv.serialize_field("x", x)?;
sv.serialize_field("y", y)?;
sv.end()
}
Event::KeyPress { key } => {
let mut sv = serializer.serialize_struct_variant(
"Event",
1,
"KeyPress",
2,
)?;
sv.serialize_field("key", key)?;
sv.end()
}
}
}
}
// The serializer implementation for serde_json handles this by:
// 1. Recognizing internally tagged format from Serializer settings
// 2. Prepending the tag field to the output
// 3. Serializing remaining fields normallyInternally tagged format prepends a tag field; the serializer implementation handles this based on #[serde(tag = ...)].
Adjacently Tagged Format
use serde::{Serialize, Serializer};
// Adjacently tagged separates variant name and fields:
#[derive(Serialize)]
#[serde(tag = "type", content = "content")]
enum Event {
Click { x: i32, y: i32 },
KeyPress { key: char },
}
// JSON output:
// {"type": "Click", "content": {"x": 10, "y": 20}}
// {"type": "KeyPress", "content": {"key": "a"}}
// The serializer wraps fields in a "content" object:
fn adjacently_tagged_example() {
// serialize_struct_variant produces:
// { "type": "variant_name", "content": { /* fields */ } }
// Two fields at the top level:
// - "type" with variant name
// - "content" with field object
}
// Compare to internally tagged:
// - Internally: fields at same level as tag
// - Adjacently: fields wrapped in "content" key
// This is useful when fields might conflict with the tag keyAdjacently tagged format separates the tag and content into distinct top-level keys.
Untagged Format
use serde::{Serialize, Serializer};
// Untagged serializes only the variant fields, no tag:
#[derive(Serialize)]
#[serde(untagged)]
enum Response {
Success { result: String },
Error { error: String },
}
// JSON output:
// {"result": "ok"} // No indication of Success variant
// {"error": "failed"} // No indication of Error variant
// For untagged, serialize_struct_variant behaves differently:
impl Serialize for Response {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Response::Success { result } => {
// Untagged: serialize just the fields, no variant wrapper
// This typically uses serialize_struct instead of serialize_struct_variant
let mut s = serializer.serialize_struct("Response", 1)?;
s.serialize_field("result", result)?;
s.end()
}
Response::Error { error } => {
let mut s = serializer.serialize_struct("Response", 1)?;
s.serialize_field("error", error)?;
s.end()
}
}
}
}
// Untagged enums lose type information during serialization
// Deserialization must infer the variant from field structureUntagged format bypasses serialize_struct_variant entirely, using serialize_struct to output only the variant's fields.
Custom SerializeStructVariant Implementation
use serde::ser::{self, SerializeStructVariant, Ok, Error};
use std::collections::HashMap;
// A custom SerializeStructVariant that tracks metadata:
struct MetadataAwareStructVariant {
enum_name: &'static str,
variant_name: &'static str,
fields: HashMap<&'static str, String>,
field_order: Vec<&'static str>, // Preserve order
metadata: VariantMetadata,
}
struct VariantMetadata {
skipped_fields: Vec<&'static str>,
renamed_fields: HashMap<&'static str, &'static str>,
}
impl SerializeStructVariant for MetadataAwareStructVariant {
type Ok = String;
type Error = ser::Error;
fn serialize_field<T: ?Sized + ser::Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
// Track field metadata
self.field_order.push(key);
// Use renamed key if applicable
let output_key = self.metadata.renamed_fields.get(key).copied().unwrap_or(key);
// Serialize value
let value_str = format!("{:?}", value); // Simplified
self.fields.insert(output_key, value_str);
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
// Build output with metadata
let fields: Vec<String> = self.field_order
.iter()
.filter_map(|k| {
self.fields.get(k).map(|v| {
format!("{}:{}", k, v)
})
})
.collect();
// Include metadata about skipped fields
let skipped_info = if self.metadata.skipped_fields.is_empty() {
String::new()
} else {
format!(" // Skipped: {:?}", self.metadata.skipped_fields)
};
Ok(format!(
"{}::{} {{ {} }}{}",
self.enum_name,
self.variant_name,
fields.join(", "),
skipped_info
))
}
}
// This could produce output like:
// Message::Write { msg: "hello" } // Skipped: ["priority"]Custom implementations can track additional metadata, preserve field order, or apply custom formatting.
Error Handling During Field Serialization
use serde::{ser, Serialize, Serializer};
use std::fmt;
// Field serialization can fail:
#[derive(Debug)]
enum SerializationError {
FieldError(String),
InvalidValue(String),
}
impl std::fmt::Display for SerializationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SerializationError::FieldError(s) => write!(f, "Field error: {}", s),
SerializationError::InvalidValue(s) => write!(f, "Invalid value: {}", s),
}
}
}
impl std::error::Error for SerializationError {}
impl ser::Error for SerializationError {
fn custom<T: fmt::Display>(msg: T) -> Self {
SerializationError::FieldError(msg.to_string())
}
}
// A field's Serialize implementation can fail:
struct Range {
start: i32,
end: i32,
}
impl Serialize for Range {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if self.start > self.end {
// This error propagates through serialize_field
return Err(ser::Error::custom("start must be <= end"));
}
let mut s = serializer.serialize_struct("Range", 2)?;
s.serialize_field("start", &self.start)?;
s.serialize_field("end", &self.end)?;
s.end()
}
}
// When serialize_field returns Err:
// - The SerializeStructVariant should preserve error context
// - The end() method is not called
// - The overall serialization fails with the field's error
// Best practice: field errors include context about which field failedField errors propagate through serialize_field; implementations should preserve context about which field caused the failure.
Summary Table
fn summary() {
// ββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββ
// β serialize_struct_variant β β
// ββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββ€
// β Purpose β Serialize enum variants with named fields β
// β Called by β Derived Serialize implementation β
// β Inputs β enum name, variant index, variant name, β
// β β field count hint β
// β Returns β SerializeStructVariant builder β
// β β β
// β After rename processing β Variant and field names already resolved β
// β After skip processing β skip fields not included in count β
// β After skip_serializing_if β Conditionally skipped fields evaluated β
// ββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββ
//
// ββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββ
// β SerializeStructVariant β β
// ββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββ€
// β serialize_field β Add one field to the variant β
// β end β Finalize and produce output β
// β β β
// β Key parameter β Already renamed field name β
// β Value parameter β Field value (possibly transformed) β
// β β β
// β Accumulates state β Fields, their names and values β
// β Determines format β How variant + fields are structured β
// ββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββ
//
// ββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββ
// β Metadata preserved β β
// ββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββ€
// β rename β Applied to field name in serialize_field β
// β rename_all β Applied to all fields without explicit β
// β β rename; handled by derive macro β
// β skip β Field never reaches serialize_field β
// β skip_serializing_if β Conditionally calls serialize_field β
// β serialize_with β Custom function called before serialize β
// β flatten β Fields hoisted; appears as regular fields β
// β β in serialize_field calls β
// ββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββ
//
// ββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββ
// β Enum representation β β
// ββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββ€
// β External (default) β {variant: {fields}} β
// β Internal (tag) β {tag, ...fields} β
// β Adjacent (tag + content) β {tag, content: {fields}} β
// β Untagged β {fields} (no variant marker) β
// β β β
// β Handled by β Serializer implementation, not the trait β
// β Affects β How serialize_struct_variant structures β
// β β its output β
// ββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββ
//
// ββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββ
// β Implementation flow β β
// ββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββ€
// β 1. Derive macro β Processes all #[serde(...)] attributes β
// β 2. Generate code β Uses transformed names, skip conditions β
// β 3. Serialize impl β Calls serialize_struct_variant with info β
// β 4. Serializer β Creates SerializeStructVariant β
// β 5. For each field β Calls serialize_field(key, value) β
// β 6. After fields β Calls end() to finalize β
// β 7. Output β Formatted according to serializer β
// ββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββ
//
// ββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββ
// β Key insight β β
// ββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββ€
// β Separation of concerns β β
// ββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββββββ€
// β Derived code β Handles metadata transformation β
// β β rename, skip, skip_serializing_if, etc. β
// β β Passes clean data to serializer β
// β β β
// β Serializer β Handles output format β
// β β external/internal/adjacent tagging β
// β β field ordering, structure layout β
// β β β
// β SerializeStructVariant β Accumulates fields and finalizes β
// β β the variant representation β
// ββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββββββ
//
// === Key Insight ===
//
// serialize_struct_variant does NOT handle field metadata directly.
// Instead, it provides a clean interface where:
//
// 1. The derived Serialize implementation processes all metadata attributes
// at compile time (rename, skip, etc.)
//
// 2. The serialize_struct_variant call receives already-transformed names
// and counts
//
// 3. The SerializeStructVariant::serialize_field calls receive final field
// names and values
//
// 4. The Serializer implementation decides the output format (externally
// tagged, internally tagged, etc.)
//
// This separation means:
// - Different serializers (JSON, YAML, etc.) can produce different formats
// from the same struct variant
// - The same serializer can handle all enums uniformly
// - Field metadata is processed once at compile time
// - Runtime errors come from serialization failures, not metadata
}
// The core abstraction:
trait Serializer {
// Enum variants with fields use this
type SerializeStructVariant: SerializeStructVariant;
fn serialize_struct_variant(
self,
name: &'static str, // Type name
variant_index: u32, // Discriminant
variant_name: &'static str, // Variant name (possibly renamed)
num_fields: usize, // Field count (after skips)
) -> Result<Self::SerializeStructVariant, Self::Error>;
}
trait SerializeStructVariant {
type Ok;
type Error;
// Each field (with renamed key if applicable)
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str, // Final field name
value: &T, // Field value (possibly transformed)
) -> Result<(), Self::Error>;
fn end(self) -> Result<Self::Ok, Self::Error>;
}Key insight: serialize_struct_variant preserves field-level metadata through a two-stage processβthe derive macro transforms all metadata (renames, skips, custom serialization) before calling the method, passing the final names and values. The SerializeStructVariant trait then accumulates these transformed fields and produces the output. The serializer implementation determines the variant representation format (externally tagged, internally tagged, etc.), while the field metadata is resolved at compile time. This separation allows different serializers to produce different formats from the same enum while ensuring consistent metadata handling across all formats.
