What is the difference between serde::ser::SerializeMap::end and SerializeStruct::end for terminating serialization?

SerializeMap::end and SerializeStruct::end both finalize serialization of compound types, but they serve distinct semantic purposes: end on SerializeMap closes a dynamic key-value collection where keys and values are heterogeneous and not known ahead of time, while end on SerializeStruct closes a fixed schema structure where field names are predetermined and typed. The difference reflects serde's separation between map types (like HashMap, BTreeMap, or dynamic JSON objects) and struct types (like Rust structs with named fields), enabling serializers to produce different output formats based on the kind of data being serialized.

The Two Serializer Types

use serde::ser::{Serialize, Serializer, SerializeStruct, SerializeMap};
use serde_json;
 
// SerializeMap: for dynamic key-value collections
// SerializeStruct: for fixed-schema structures
 
#[derive(serde::Serialize)]
struct Person {
    name: String,
    age: u32,
}
 
fn serializer_types() {
    // Person serializes using SerializeStruct
    // - Fixed fields: "name" and "age"
    // - Schema known at compile time
    
    // HashMap<String, i32> serializes using SerializeMap
    // - Dynamic keys
    // - Values are the same type but could be heterogeneous in JSON
}

Serde distinguishes between map-like and struct-like serialization for format flexibility.

SerializeMap::end for Maps

use serde::ser::{Serializer, SerializeMap};
use std::collections::HashMap;
 
fn serialize_map() -> Result<(), Box<dyn std::error::Error>> {
    let mut map: HashMap<&str, i32> = HashMap::new();
    map.insert("one", 1);
    map.insert("two", 2);
    map.insert("three", 3);
    
    // When serializing a map, the serializer creates a SerializeMap
    // You add entries, then call end() to finish
    
    let json = serde_json::to_string(&map)?;
    println!("{}", json);  // {"one":1,"three":3,"two":2}
    
    // end() is called automatically by serde's derive macros
    // It signals that no more entries will be added
    
    Ok(())
}

SerializeMap::end finalizes a map after all key-value pairs have been added.

SerializeStruct::end for Structs

use serde::ser::{Serializer, SerializeStruct};
use serde_json;
 
#[derive(serde::Serialize)]
struct Config {
    host: String,
    port: u16,
    debug: bool,
}
 
fn serialize_struct() -> Result<(), Box<dyn std::error::Error>> {
    let config = Config {
        host: "localhost".to_string(),
        port: 8080,
        debug: false,
    };
    
    // Struct uses SerializeStruct, not SerializeMap
    // Fields are fixed: "host", "port", "debug"
    // The serializer knows the complete schema
    
    let json = serde_json::to_string(&config)?;
    println!("{}", json);  // {"host":"localhost","port":8080,"debug":false}
    
    // SerializeStruct::end is called to finalize the struct
    
    Ok(())
}

SerializeStruct::end finalizes a struct after all fields have been serialized.

Manual Serialization: Map end()

use serde::ser::{Serialize, Serializer, SerializeMap};
 
struct DynamicData {
    entries: Vec<(String, i32)>,
}
 
impl Serialize for DynamicData {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Create a map serializer
        let mut map = serializer.serialize_map(Some(self.entries.len()))?;
        
        // Add each key-value pair
        for (key, value) in &self.entries {
            map.serialize_entry(key, value)?;
        }
        
        // Finalize the map
        map.end()
    }
}
 
fn manual_map() -> Result<(), Box<dyn std::error::Error>> {
    let data = DynamicData {
        entries: vec![
            ("a".to_string(), 1),
            ("b".to_string(), 2),
        ],
    };
    
    let json = serde_json::to_string(&data)?;
    println!("{}", json);  // {"a":1,"b":2}
    
    Ok(())
}

serialize_map() returns a SerializeMap, and end() completes it.

Manual Serialization: Struct end()

use serde::ser::{Serialize, Serializer, SerializeStruct};
 
struct Point {
    x: f64,
    y: f64,
}
 
impl Serialize for Point {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Create a struct serializer
        // 2 = number of fields
        let mut s = serializer.serialize_struct("Point", 2)?;
        
        // Serialize each field by name
        s.serialize_field("x", &self.x)?;
        s.serialize_field("y", &self.y)?;
        
        // Finalize the struct
        s.end()
    }
}
 
fn manual_struct() -> Result<(), Box<dyn std::error::Error>> {
    let point = Point { x: 10.0, y: 20.0 };
    
    let json = serde_json::to_string(&point)?;
    println!("{}", json);  // {"x":10.0,"y":20.0}
    
    Ok(())
}

serialize_struct() returns a SerializeStruct, and end() completes it.

Semantic Differences

use serde::ser::{Serializer, SerializeMap, SerializeStruct};
use serde_json;
 
// The key differences between map and struct serialization:
 
// SerializeMap:
// - Keys are dynamic, not known at compile time
// - Key-value pairs can be heterogeneous in some formats
// - Number of entries may not be known ahead of time
// - Used for HashMap, BTreeMap, IndexMap, etc.
 
// SerializeStruct:
// - Fields are fixed at compile time
// - Field names are known when serialize_struct is called
// - Number of fields is known in advance
// - Used for Rust structs with named fields
 
fn semantic_difference() {
    // JSON format looks similar for both:
    // Map: {"key1": "value1", "key2": "value2"}
    // Struct: {"field1": "value1", "field2": "value2"}
    
    // But other formats treat them differently:
    // - Binary formats may encode struct fields by index
    // - Struct fields can have specific type metadata
    // - Maps may use length-prefix encoding
}

The difference matters for non-JSON formats where maps and structs encode differently.

Why Two Separate Types?

use serde::ser::{Serializer, SerializeMap, SerializeStruct};
 
// The separation enables:
 
// 1. Format-specific optimizations
// - Structs: serialize field indices instead of names
// - Maps: length-prefixed encoding for streaming
 
// 2. Type safety
// - Structs: fields are typed and validated at compile time
// - Maps: keys and values are dynamic
 
// 3. Different behavior in non-JSON formats
// Example: A binary format might encode:
// - Structs: [type_id, field_0, field_1, ...]
// - Maps: [length, [(key_0, value_0), (key_1, value_1), ...]]
 
// 4. Self-describing formats
// - Structs can embed type information
// - Maps might need separate schema

Separate types allow serializers to optimize differently for each case.

The end() Return Value

use serde::ser::{Serializer, SerializeMap, SerializeStruct};
 
// Both end() methods return Result<S::Ok, S::Error>
// - S::Ok is the serializer's output type
// - S::Error is the serializer's error type
 
// For JSON: S::Ok is typically ()
// For binary formats: S::Ok might be bytes written
 
// Example signatures:
// trait SerializeMap {
//     fn end(self) -> Result<Self::Ok, Self::Error>;
// }
// 
// trait SerializeStruct {
//     fn end(self) -> Result<Self::Ok, Self::Error>;
// }
 
// The end() call consumes the serializer (self, not &mut self)
// This prevents adding more entries after ending

end() consumes the serializer, returning the serialization result.

Self-consuming end()

use serde::ser::{Serializer, SerializeStruct};
 
struct Item {
    id: u32,
    name: String,
}
 
impl serde::Serialize for Item {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut s = serializer.serialize_struct("Item", 2)?;
        s.serialize_field("id", &self.id)?;
        s.serialize_field("name", &self.name)?;
        
        // end() takes self by value, consuming the struct serializer
        s.end()
        
        // Cannot use s after end() - it's been consumed
        // s.serialize_field("extra", &());  // Error: s moved
    }
}
 
// This prevents accidental use after finalization

end() takes self by value, ensuring the serializer cannot be used after finalization.

Nested Structures

use serde::ser::{Serializer, SerializeStruct, SerializeMap};
 
#[derive(serde::Serialize)]
struct Outer {
    inner: Inner,
    values: std::collections::HashMap<String, i32>,
}
 
#[derive(serde::Serialize)]
struct Inner {
    field: String,
}
 
fn nested_serialization() -> Result<(), Box<dyn std::error::Error>> {
    let mut map = std::collections::HashMap::new();
    map.insert("a".to_string(), 1);
    map.insert("b".to_string(), 2);
    
    let outer = Outer {
        inner: Inner { field: "value".to_string() },
        values: map,
    };
    
    let json = serde_json::to_string(&outer)?;
    // {"inner":{"field":"value"},"values":{"a":1,"b":2}}
    
    // The serialization calls:
    // 1. serialize_struct("Outer", 2) for Outer
    // 2. serialize_struct("Inner", 1) for Inner (nested)
    // 3. serialize_map() for values HashMap (nested)
    // Each has its own end() call
    
    Ok(())
}

Nested structures each have their own serializer with separate end() calls.

Error Handling in end()

use serde::ser::{Serializer, SerializeMap};
 
struct StrictMap<K, V> {
    data: std::collections::HashMap<K, V>,
}
 
impl<K, V> serde::Serialize for StrictMap<K, V>
where
    K: serde::Serialize,
    V: serde::Serialize,
{
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(Some(self.data.len()))?;
        
        for (k, v) in &self.data {
            map.serialize_entry(k, v)?;
        }
        
        // end() can return an error if:
        // - The underlying writer failed
        // - Format constraints were violated
        // - Buffer limits were exceeded
        map.end()
    }
}
 
// end() is the final point where errors can be raised
// For streaming formats, this may flush buffers

end() may fail for I/O errors or format constraint violations.

Format-Specific Behavior

use serde_json;
use serde::ser::{Serializer, SerializeStruct};
 
// JSON: maps and structs produce similar output
// {"field":"value"} for both
 
// But consider a hypothetical binary format:
// - Struct: [struct_type_id][field_1][field_2]
// - Map: [map_type_id][length][(key_1, val_1), (key_2, val_2)]
 
// Or a database format:
// - Struct: INSERT INTO table (field1, field2) VALUES (?, ?)
// - Map: Could be JSON blob or separate key-value table
 
// The separation allows format implementors to handle each case appropriately
 
#[derive(serde::Serialize)]
struct Record {
    id: u32,
    data: String,
}
 
fn format_differences() -> Result<(), Box<dyn std::error::Error>> {
    let record = Record { id: 1, data: "test".to_string() };
    
    // JSON serializes struct fields by name
    let json = serde_json::to_string(&record)?;
    // {"id":1,"data":"test"}
    
    // Another format might:
    // - Use field indices instead of names
    // - Include schema metadata
    // - Use different delimiters
    
    Ok(())
}

Different formats can encode maps and structs differently.

Skip Handling

use serde::ser::{Serializer, SerializeStruct, Skip};
 
#[derive(serde::Serialize)]
struct Optional {
    required: String,
    optional: Option<String>,
}
 
fn skip_fields() -> Result<(), Box<dyn std::error::Error>> {
    let data = Optional {
        required: "value".to_string(),
        optional: None,  // This field will be skipped
    };
    
    // When serialize_struct is created:
    // - serializer.serialize_struct("Optional", 1)?
    // Only 1 field (not 2) because optional is None
    
    let json = serde_json::to_string(&data)?;
    // {"required":"value"}  // optional is not included
    
    // end() finalizes with the actual number of fields serialized
    // Some serializers track this internally
    
    Ok(())
}

Structs can skip fields, and end() finalizes only serialized fields.

Map Length Uncertainty

use serde::ser::{Serializer, SerializeMap};
use std::collections::HashMap;
 
// Maps may not know their length upfront
 
fn uncertain_length() -> Result<(), Box<dyn std::error::Error>> {
    let mut map = HashMap::new();
    map.insert("a", 1);
    
    // serialize_map takes Option<usize> for length
    // None means length is unknown
    // Some(n) means exactly n entries will follow
    
    // end() may need to handle:
    // - Verifying actual count matches declared count
    // - Writing length suffix for streaming formats
    // - Finalizing buffer
    
    Ok(())
}
 
// For struct, the number of fields is always known
// serialize_struct(&self, name: &'static str, len: usize)
// len is the declared field count

Maps may have unknown length; structs always know field count upfront.

Streaming and Flushing

use serde::ser::{Serializer, SerializeMap};
 
// For streaming serializers, end() may flush buffers
 
struct StreamingMap<'a, W> {
    writer: &'a mut W,
    entries_written: usize,
}
 
impl<'a, W: std::io::Write> SerializeMap for StreamingMap<'a, W> {
    type Ok = ();
    type Error = std::io::Error;
    
    fn serialize_entry<K, V>(&mut self, key: &K, value: &V) -> Result<(), Self::Error>
    where
        K: serde::Serialize + ?Sized,
        V: serde::Serialize + ?Sized,
    {
        // Write entry to underlying writer
        // Might buffer internally
        self.entries_written += 1;
        Ok(())
    }
    
    fn end(self) -> Result<Self::Ok, Self::Error> {
        // Finalize: write closing delimiter
        // Flush any buffered data
        // Complete the stream
        self.writer.write_all(b"}")?;
        self.writer.flush()?;  // Ensure all data is written
        Ok(())
    }
}
 
// end() is the point where finalization happens
// This is crucial for streaming and buffered formats

end() may flush buffers for streaming serializers.

Comparison Table

use serde::ser::{SerializeMap, SerializeStruct};
 
fn comparison() {
    // | Aspect | SerializeMap | SerializeStruct |
    // |--------|--------------|-----------------|
    // | Created by | serialize_map() | serialize_struct() |
    // | Used for | HashMap, BTreeMap | Rust structs |
    // | Keys/Fields | Dynamic, unknown | Fixed, known |
    // | Length parameter | Option<usize> | usize (exact) |
    // | Key type | Runtime value | Static str |
    // | end() semantics | Close map | Close struct |
    // | Typical output | {k1:v1, k2:v2} | {f1:v1, f2:v2} |
    // | Format flexibility | Key-value encoding | Schema-aware encoding |
    
    // Both end() methods:
    // - Take self by value
    // - Return Result<Ok, Error>
    // - Finalize the serialization
    // - May flush/complete I/O
}

Synthesis

Quick reference:

use serde::ser::{Serializer, SerializeMap, SerializeStruct};
 
// SerializeMap::end - finalizes dynamic key-value collections
fn finish_map(mut map: impl SerializeMap) -> Result<(), ()> {
    // After adding all entries with serialize_entry()
    map.end().map_err(|_| ())?;
    Ok(())
}
 
// SerializeStruct::end - finalizes fixed-schema structures
fn finish_struct(mut s: impl SerializeStruct) -> Result<(), ()> {
    // After adding all fields with serialize_field()
    s.end().map_err(|_| ())?;
    Ok(())
}
 
// Key differences:
// - Maps: dynamic keys, unknown length possible
// - Structs: fixed fields, known count
// - end() consumes the serializer in both cases

Key insight: The separation between SerializeMap::end and SerializeStruct::end reflects serde's design principle of giving format implementations complete information about the shape of data being serialized. While JSON produces similar output for both maps and structs (curly braces with string keys and values), other formats can optimize differently: a binary protocol might encode struct fields as indices into a schema (avoiding field name overhead), while maps might require length prefixes for streaming deserialization. The end() method is the finalization point for both types, consuming self to prevent further modification and returning any errors from the underlying writer. For streaming formats, end() is where final delimiters are written and buffers are flushed. The two-argument difference in creation (serialize_map(Some(len)) vs serialize_struct("Name", len)) also signals the semantic distinction—structs carry their type name for schema-aware formats, while maps are just collections of key-value pairs. This separation enables serde to support both schemaless formats like JSON and schema-heavy formats like protocol buffers or database backends.