How does serde::ser::SerializeMap::end finalize map serialization compared to implicit drops?

SerializeMap::end explicitly finalizes map serialization, ensuring all buffered data is flushed and any validation or error checking completes, while an implicit drop may silently discard errors and leave serialized data incomplete or corrupted. The end method returns a Result that must be handled, giving the serializer a final opportunity to report errors—such as incomplete map structures or I/O failures—that would otherwise be lost if the SerializeMap is simply dropped. Custom serializers that buffer data or write to I/O streams depend on end to guarantee that all content is written; dropping without calling end may result in truncated output or missing closing delimiters.

Understanding SerializeMap

use serde::ser::{Serialize, Serializer, SerializeMap};
 
// SerializeMap is part of serde's serializer API
// It's used to serialize map-like data structures
 
// When implementing a custom Serializer, you return a SerializeMap
// for map serialization operations
 
// The SerializeMap trait has these key methods:
// - serialize_key(K) -> Result<(), Error>
// - serialize_value(V) -> Result<(), Error>
// - end() -> Result<(), Error>
 
// Example: Serializing a HashMap
use std::collections::HashMap;
 
fn serialize_map_example() {
    let mut map = HashMap::new();
    map.insert("name", "Alice");
    map.insert("age", "30");
    
    // When serde serializes this map, it:
    // 1. Calls serializer.serialize_map(Some(2))
    // 2. For each entry:
    //    a. Calls map.serialize_key(key)
    //    b. Calls map.serialize_value(value)
    // 3. Calls map.end()
}

SerializeMap is the interface serializers use to output map structures.

The end Method Signature

use serde::ser::{Error, SerializeMap};
 
// The end method signature:
pub trait SerializeMap {
    type Ok;
    type Error: Error;
    
    fn end(self) -> Result<Self::Ok, Self::Error>;
    
    // Other methods...
    fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<(), Self::Error>;
    fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error>;
}
 
// end() returns Result<Ok, Error>
// - Ok: Success result from the serializer (often ())
// - Error: Any error that occurred during finalization
 
// Key insight: end() MUST be called for proper finalization
// Dropping without calling end() loses the Result

The end method consumes self and returns a Result, ensuring finalization happens.

What end Does for JSON Serialization

use serde::ser::{SerializeMap, Serializer};
use serde_json::Serializer;
 
fn json_end_example() -> Result<(), serde_json::Error> {
    let mut output = Vec::new();
    let mut serializer = Serializer::new(&mut output);
    
    // Start serializing a map
    let mut map = serializer.serialize_map(Some(2))?;
    
    // Add entries
    map.serialize_key("name")?;
    map.serialize_value("Alice")?;
    
    map.serialize_key("age")?;
    map.serialize_value(&30)?;
    
    // end() writes the closing brace '}' and flushes
    map.end()?;
    
    // Output now contains: {"name":"Alice","age":30}
    println!("{}", String::from_utf8_lossy(&output));
    
    Ok(())
}
 
// For JSON, end() writes:
// - The closing '}' brace
// - Any final formatting
// - Ensures the output is complete

For JSON, end writes the closing delimiter and ensures the structure is complete.

What Happens on Implicit Drop

use serde::ser::{SerializeMap, Serializer};
use serde_json::Serializer;
 
fn implicit_drop_example() {
    let mut output = Vec::new();
    let serializer = Serializer::new(&mut output);
    
    {
        // Start serializing a map
        let mut map = serializer.serialize_map(Some(2)).unwrap();
        
        map.serialize_key("name").unwrap();
        map.serialize_value("Alice").unwrap();
        
        // Oh no! We didn't call end()
        // map goes out of scope here
    }
    // The map is dropped without end()
    
    // What happens depends on the serializer implementation
    // - JSON: May write closing brace in Drop
    // - Custom: May or may not finalize
    // - Errors: Definitely lost!
}

Dropping without end may or may not finalize properly, depending on the serializer.

Error Handling Differences

use serde::ser::{SerializeMap, Serializer, Error};
use std::io::Write;
 
// Custom serializer that validates map structure
struct ValidatingSerializer<'a, W: Write>(&'a mut W);
 
#[derive(Debug)]
enum SerializeError {
    IncompleteMap,
    Io(std::io::Error),
}
 
impl serde::ser::Error for SerializeError {
    fn custom<T: std::fmt::Display>(msg: T) -> Self {
        // In real code, you'd have a proper error variant
        SerializeError::IncompleteMap
    }
}
 
impl<'a, W: Write> Serializer for ValidatingSerializer<'a, W> {
    type Ok = ();
    type Error = SerializeError;
    type SerializeMap = ValidatingMap<'a, W>;
    
    // ... other methods ...
    
    fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
        write!(self.0, "{{").map_err(SerializeError::Io)?;
        Ok(ValidatingMap(self.0, 0))  // Track entry count
    }
}
 
struct ValidatingMap<'a, W: Write>(&'a mut W, usize);
 
impl<'a, W: Write> SerializeMap for ValidatingMap<'a, W> {
    type Ok = ();
    type Error = SerializeError;
    
    fn serialize_key<T: ?Sized + serde::Serialize>(&mut self, _key: &T) -> Result<(), Self::Error> {
        write!(self.0, "\"key\":").map_err(SerializeError::Io)?;
        self.1 += 1;
        Ok(())
    }
    
    fn serialize_value<T: ?Sized + serde::Serialize>(&mut self, _value: &T) -> Result<(), Self::Error> {
        write!(self.0, "\"value\"").map_err(SerializeError::Io)?;
        Ok(())
    }
    
    fn end(self) -> Result<Self::Ok, Self::Error> {
        // Validation: Check if map is complete
        if self.1 % 2 != 0 {
            return Err(SerializeError::IncompleteMap);
        }
        write!(self.0, "}}").map_err(SerializeError::Io)
    }
}
 
impl<'a, W: Write> Drop for ValidatingMap<'a, W> {
    fn drop(&mut self) {
        // Drop cannot return errors!
        // Best effort: write closing brace
        let _ = write!(self.0, "}}");
        // But we CAN'T report IncompleteMap error here
    }
}

end can return errors; Drop cannot, making error reporting impossible.

I/O Buffering and Flushing

use serde::ser::{SerializeMap, Serializer};
use std::io::Write;
 
// Serializer that buffers and flushes on end()
struct BufferedSerializer<W: Write> {
    writer: W,
    buffer: Vec<u8>,
}
 
impl<W: Write> BufferedSerializer<W> {
    fn flush(&mut self) -> Result<(), std::io::Error> {
        self.writer.write_all(&self.buffer)?;
        self.buffer.clear();
        Ok(())
    }
}
 
// In end():
// fn end(self) -> Result<(), Error> {
//     self.serialize_map_struct.close_delimiter()?;
//     self.flush()?;  // Flush buffered data
//     Ok(())
// }
 
// In Drop:
// fn drop(&mut self) {
//     // Best effort flush - can't return error
//     let _ = self.flush();
// }
 
// The difference:
// - end(): Errors propagate, caller knows write failed
// - Drop: Errors ignored, caller thinks write succeeded

end ensures I/O errors are visible; Drop silently swallows them.

The Drop Implementation for Safety

// serde_json's MapSerializer implements Drop defensively
 
// From serde_json source (simplified):
impl<'a, W> Drop for MapSerializer<'a, W> {
    fn drop(&mut self) {
        // Write closing brace if not already done
        // This is a fallback for safety
        
        // BUT: This cannot report errors!
        // If writing '}' fails, the error is lost
        
        if !self.ended {
            // Try to write closing brace
            // Error is silently ignored
            let _ = self.writer.write_all(b"}");
        }
    }
}
 
// The ended field tracks whether end() was called
// If Drop runs, it means end() wasn't called
// The serializer tries to recover, but errors are lost
 
// This is why end() should always be called explicitly

Drop implementations try to recover but cannot propagate errors.

Comparison: end vs Drop

use serde::ser::{SerializeMap, Serializer};
 
fn comparison_example() {
    // With end():
    {
        let mut map = serializer.serialize_map(Some(2))?;
        map.serialize_key("a")?;
        map.serialize_value(&1)?;
        map.serialize_key("b")?;
        map.serialize_value(&2)?;
        
        // Explicit end - all errors visible
        map.end()?;  // Must handle Result
    }
    
    // With implicit drop:
    {
        let mut map = serializer.serialize_map(Some(2))?;
        map.serialize_key("a")?;
        map.serialize_value(&1)?;
        map.serialize_key("b")?;
        map.serialize_value(&2)?;
        
        // No end() call
        // Drop runs when scope ends
        // Drop tries to close but swallows errors
    }  // <- Drop happens here
    
    // The difference:
    // 1. end() returns Result - caller must handle
    // 2. end() can report validation errors
    // 3. end() can report I/O errors
    // 4. Drop cannot return errors
}

The explicit end call ensures errors are handled; Drop cannot report them.

SerializeMap Implementation Pattern

use serde::ser::{Error, SerializeMap};
use std::io::Write;
 
// Proper SerializeMap implementation pattern
pub struct MyMapSerializer<'a, W: Write> {
    writer: &'a mut W,
    first: bool,      // Track if first entry for commas
    ended: bool,      // Track if end() was called
    entries: usize,   // Count entries for validation
}
 
impl<'a, W: Write> SerializeMap for MyMapSerializer<'a, W> {
    type Ok = ();
    type Error = MyError;
    
    fn serialize_key<T: ?Sized + serde::Serialize>(
        &mut self,
        key: &T
    ) -> Result<(), Self::Error> {
        // Add comma if not first entry
        if self.first {
            self.first = false;
        } else {
            write!(self.writer, ",").map_err(MyError::Io)?;
        }
        
        // Serialize key
        key.serialize(&mut *self.writer)?;
        write!(self.writer, ":").map_err(MyError::Io)?;
        
        self.entries += 1;
        Ok(())
    }
    
    fn serialize_value<T: ?Sized + serde::Serialize>(
        &mut self,
        value: &T
    ) -> Result<(), Self::Error> {
        // Serialize value
        value.serialize(&mut *self.writer)?;
        Ok(())
    }
    
    fn end(mut self) -> Result<Self::Ok, Self::Error> {
        // Mark as ended (so Drop knows)
        self.ended = true;
        
        // Write closing delimiter
        write!(self.writer, "}}").map_err(MyError::Io)?;
        
        // Can do validation here
        // e.g., check entries count, verify structure
        
        Ok(())
    }
}
 
impl<'a, W: Write> Drop for MyMapSerializer<'a, W> {
    fn drop(&mut self) {
        // Only finalize if not already ended
        if !self.ended {
            // Best effort close
            let _ = write!(self.writer, "}}");
        }
    }
}

A proper implementation tracks whether end was called and has a fallback Drop.

When You Must Call end

use serde::ser::{SerializeMap, Serializer};
 
fn must_call_end() {
    // You must call end() when:
    
    // 1. Serializing to I/O (files, network)
    //    - end() flushes buffers
    //    - end() returns I/O errors
    
    // 2. Custom serializers with validation
    //    - end() validates structure
    //    - end() returns validation errors
    
    // 3. Serializing with side effects
    //    - end() completes side effects
    //    - end() reports failures
    
    // 4. Any Result<Ok, Error> must be handled
    //    - Dropping silently ignores errors
    //    - end() forces error handling
}
 
// When serde calls end() for you:
// The standard Serialize implementations call end()
// You only need to call it when implementing custom serialization
 
impl serde::Serialize for MyMap {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(Some(self.len()))?;
        for (k, v) in &self.data {
            map.serialize_key(k)?;
            map.serialize_value(v)?;
        }
        map.end()  // serde implementations call end()
    }
}

Standard Serialize implementations call end; custom implementations must also call it.

The Compile-Time Safety net

use serde::ser::SerializeMap;
 
// Rust's ownership helps enforce end() usage:
 
// end() takes self by value, consuming the SerializeMap
// After calling end(), you can't use the map anymore
 
fn ownership_example() -> Result<(), Error> {
    let mut map = serializer.serialize_map(Some(2))?;
    
    map.serialize_key("a")?;
    map.serialize_value(&1)?;
    
    map.end()?;  // Consumes map
    
    // map.serialize_key("b");  // ERROR: map moved
    // Can't use map after end()
    
    Ok(())
}
 
// However, this doesn't prevent forgetting end():
fn forgot_end_example() -> Result<(), Error> {
    let mut map = serializer.serialize_map(Some(2))?;
    
    map.serialize_key("a")?;
    map.serialize_value(&1)?;
    
    // Forgot to call end()
    // Compiler allows this - map just drops
    
    Ok(())  // Returns Ok even if serialization is incomplete
}
 
// This is why Drop is needed as a safety net
// But Drop cannot return errors

end consumes self, preventing further use, but doesn't prevent forgetting to call it.

Practical Example: Custom Serializer

use serde::ser::{self, Serializer, SerializeMap, Error};
use std::io::Write;
 
// A serializer that writes to a file with validation
pub struct FileSerializer<W: Write> {
    writer: W,
}
 
impl<W: Write> FileSerializer<W> {
    pub fn new(writer: W) -> Self {
        FileSerializer { writer }
    }
}
 
impl<W: Write> Serializer for FileSerializer<W> {
    type Ok = ();
    type Error = FileError;
    type SerializeMap = FileMapSerializer<W>;
    
    fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
        write!(self.writer, "{{").map_err(FileError::Io)?;
        Ok(FileMapSerializer {
            writer: self.writer,
            first: true,
            ended: false,
            expected_len: len,
            actual_len: 0,
        })
    }
    
    // ... other methods ...
}
 
pub struct FileMapSerializer<W: Write> {
    writer: W,
    first: bool,
    ended: bool,
    expected_len: Option<usize>,
    actual_len: usize,
}
 
#[derive(Debug)]
pub enum FileError {
    Io(std::io::Error),
    IncompleteMap { expected: usize, actual: usize },
}
 
impl std::fmt::Display for FileError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            FileError::Io(e) => write!(f, "IO error: {}", e),
            FileError::IncompleteMap { expected, actual } => {
                write!(f, "Incomplete map: expected {} entries, got {}", expected, actual)
            }
        }
    }
}
 
impl std::error::Error for FileError {}
 
impl ser::Error for FileError {
    fn custom<T: std::fmt::Display>(msg: T) -> Self {
        FileError::Io(std::io::Error::new(std::io::ErrorKind::Other, msg.to_string()))
    }
}
 
impl<W: Write> SerializeMap for FileMapSerializer<W> {
    type Ok = ();
    type Error = FileError;
    
    fn serialize_key<T: ?Sized + ser::Serialize>(&mut self, key: &T) -> Result<(), Self::Error> {
        if !self.first {
            write!(self.writer, ",").map_err(FileError::Io)?;
        }
        self.first = false;
        
        // Serialize key (simplified)
        write!(self.writer, "\"key\"").map_err(FileError::Io)?;
        write!(self.writer, ":").map_err(FileError::Io)?;
        
        Ok(())
    }
    
    fn serialize_value<T: ?Sized + ser::Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
        // Serialize value (simplified)
        write!(self.writer, "\"value\"").map_err(FileError::Io)?;
        self.actual_len += 1;
        Ok(())
    }
    
    fn end(mut self) -> Result<Self::Ok, Self::Error> {
        self.ended = true;
        
        // Validation: check length matches
        if let Some(expected) = self.expected_len {
            if expected != self.actual_len {
                return Err(FileError::IncompleteMap {
                    expected: expected,
                    actual: self.actual_len,
                });
            }
        }
        
        write!(self.writer, "}}").map_err(FileError::Io)
    }
}
 
impl<W: Write> Drop for FileMapSerializer<W> {
    fn drop(&mut self) {
        if !self.ended {
            // Best effort: try to close the map
            // Cannot report validation error!
            let _ = write!(self.writer, "}}");
        }
    }
}

Custom serializers should track ended state and provide both end and Drop.

Synthesis

The end method purpose:

// end() does three critical things:
// 1. Completes the serialized structure (writes '}' for JSON)
// 2. Performs final validation
// 3. Returns Result with any errors
 
fn end(self) -> Result<Self::Ok, Self::Error> {
    // 1. Close structure
    self.write_closing_delimiter()?;
    
    // 2. Validate
    if !self.is_complete() {
        return Err(Error::Incomplete);
    }
    
    // 3. Flush if needed
    self.flush()?;
    
    Ok(())
}

The Drop fallback:

// Drop is a safety net, not a replacement
impl Drop for MapSerializer<'_> {
    fn drop(&mut self) {
        if !self.ended {
            // Try to close, but cannot report errors
            let _ = self.close();
        }
    }
}
 
// Drop limitations:
// 1. Cannot return errors (signature is fn drop(&mut self))
// 2. Cannot fail the operation
// 3. Called implicitly - no control over timing

Comparison:

// Calling end():
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_key("a")?;
map.serialize_value(&1)?;
map.end()?;  // <- Returns Result, must handle
// Guarantees:
// - Structure closed
// - Errors reported
// - Validation complete
 
// Implicit drop:
{
    let mut map = serializer.serialize_map(Some(2))?;
    map.serialize_key("a")?;
    map.serialize_value(&1)?;
    // No end() call
}  // <- Drop runs
// Guarantees:
// - Structure closed (if Drop implements it)
// - Errors LOST
// - Validation SKIPPED

When each is used:

// Standard serde Serialize implementations always call end()
impl Serialize for MyStruct {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where S: Serializer {
        let mut map = serializer.serialize_map(None)?;
        // ... serialize entries ...
        map.end()  // Always called
    }
}
 
// Drop is for safety when:
// 1. Code panics during serialization
// 2. Programmer forgets to call end()
// 3. Early return bypasses end()
 
// Never rely on Drop for:
// 1. Error reporting
// 2. Validation
// 3. I/O flushing

Key insight: SerializeMap::end is the proper way to finalize map serialization because it returns a Result that can propagate validation errors, I/O failures, and other issues that occur during finalization. The Drop implementation is a safety net that tries to close incomplete maps but cannot report errors—any failure in Drop is silently swallowed. This is why custom serializers should both track whether end was called (with an ended flag) and implement Drop as a fallback: explicit end calls guarantee proper finalization with error handling, while Drop provides best-effort recovery when end is accidentally omitted or when panics occur. The pattern is similar to std::io::Write::flush—explicit flushing ensures errors are visible, while Drop tries to flush on cleanup but cannot signal failures.