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 names

The 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 rename

Renames 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 varies

num_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 serialized

SerializeStructVariant 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 fields

Serialization 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", &timestamps.created_at)?;
                sv.serialize_field("updatedAt", &timestamps.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 fields

flatten 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 format

Externally 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 normally

Internally 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 key

Adjacently 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 structure

Untagged 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 failed

Field 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.