How does serde::Serializer::collect_seq efficiently serialize iterators without intermediate allocation?

serde::Serializer::collect_seq serializes iterator elements directly into the output format without collecting them into an intermediate container like Vec, eliminating the allocation overhead that would result from .collect::<Vec<_>>() before serialization. The method takes any type implementing IntoIterator and calls the serializer's sequence serialization methods for each element, treating the iterator as a lazy sequence. This approach is particularly valuable for large datasets or expensive-to-clone items, where allocating a temporary collection would waste memory or CPU cycles. The serialization happens element-by-element through the SerializeSeq trait, allowing serializers to write directly to their output stream without buffering the entire sequence.

The Allocation Problem Without collect_seq

use serde::{Serialize, Serializer};
 
// Without collect_seq, you might write this:
fn serialize_items_bad<S>(items: &[i32], serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    // This allocates a Vec just for serialization
    let collected: Vec<&i32> = items.iter().collect();
    collected.serialize(serializer)
}
 
// With collect_seq, no intermediate allocation:
fn serialize_items_good<S>(items: &[i32], serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    serializer.collect_seq(items.iter())
}

collect_seq streams elements directly to the serializer without the intermediate Vec.

Basic collect_seq Usage

use serde::{Serialize, Serializer};
 
struct Data {
    values: Vec<i32>,
}
 
impl Serialize for Data {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Use collect_seq to serialize an iterator
        serializer.collect_seq(self.values.iter())
    }
}
 
fn main() {
    let data = Data { values: vec![1, 2, 3, 4, 5] };
    let json = serde_json::to_string(&data).unwrap();
    println!("{}", json);
    // [1, 2, 3, 4, 5]
}

collect_seq takes any iterator and serializes it as a sequence.

Serializing Transformed Iterators

use serde::{Serialize, Serializer};
 
struct Numbers {
    values: Vec<i32>,
}
 
impl Serialize for Numbers {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Transform and serialize without intermediate allocation
        serializer.collect_seq(self.values.iter().map(|n| n * 2))
    }
}
 
fn main() {
    let nums = Numbers { values: vec![1, 2, 3] };
    let json = serde_json::to_string(&nums).unwrap();
    println!("{}", json);
    // [2, 4, 6]
}
 
// Without collect_seq, you would need:
impl Serialize for Numbers {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Allocates temporary Vec
        let doubled: Vec<i32> = self.values.iter().map(|n| n * 2).collect();
        doubled.serialize(serializer)
    }
}

Transformations happen lazily during serialization, avoiding intermediate collections.

Serializing Filtered Sequences

use serde::{Serialize, Serializer};
 
struct Config {
    entries: Vec<(String, i32)>,
}
 
impl Serialize for Config {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Serialize only positive values, no intermediate Vec
        serializer.collect_seq(
            self.entries.iter().filter(|(_, v)| *v > 0)
        )
    }
}
 
fn main() {
    let config = Config {
        entries: vec![
            ("a".to_string(), 1),
            ("b".to_string(), -1),
            ("c".to_string(), 5),
            ("d".to_string(), 0),
        ],
    };
    
    let json = serde_json::to_string(&config).unwrap();
    println!("{}", json);
    // [["a",1],["c",5]]
}

Filtering happens lazily during serialization without collecting filtered results.

How collect_seq Works Internally

use serde::{Serialize, Serializer, ser::SerializeSeq};
 
// The implementation pattern (simplified):
fn collect_seq<I, S>(iter: I, serializer: S) -> Result<S::Ok, S::Error>
where
    I: IntoIterator,
    I::Item: Serialize,
    S: Serializer,
{
    // Create a sequence serializer
    let mut seq = serializer.serialize_seq(None)?;
    
    // Serialize each item directly
    for item in iter.into_iter() {
        seq.serialize_element(&item)?;
    }
    
    // Finish the sequence
    seq.end()
}
 
// This is equivalent to what collect_seq does internally
// The key is: no intermediate Vec allocation

collect_seq uses serialize_seq to stream elements directly to output.

Serializing Large Datasets

use serde::{Serialize, Serializer};
 
struct LargeDataset {
    count: usize,
}
 
impl Serialize for LargeDataset {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Generate values on-the-fly without storing them
        serializer.collect_seq((0..self.count).map(|i| format!("item_{}", i)))
    }
}
 
fn main() {
    let dataset = LargeDataset { count: 1_000_000 };
    
    // This streams directly to the writer
    // No Vec of 1 million strings is ever allocated
    let mut writer = Vec::new();
    serde_json::to_writer(&mut writer, &dataset).unwrap();
    
    println!("Serialized {} bytes", writer.len());
}
 
// Without collect_seq, you would need to allocate all strings first:
impl Serialize for LargeDataset {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // BAD: Allocates 1 million strings in memory
        let items: Vec<String> = (0..self.count).map(|i| format!("item_{}", i)).collect();
        items.serialize(serializer)
    }
}

For large datasets, avoiding intermediate allocation significantly reduces memory usage.

Serializing Expensive-to-Clone Types

use serde::{Serialize, Serializer};
 
struct ExpensiveItems {
    items: Vec<String>,
}
 
impl Serialize for ExpensiveItems {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Serialize references without cloning
        serializer.collect_seq(self.items.iter())
    }
}
 
// Without collect_seq, clone would be needed:
impl Serialize for ExpensiveItems {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // BAD: Clones all strings
        let cloned: Vec<&String> = self.items.iter().collect();
        cloned.serialize(serializer)
    }
}

Serializing by reference avoids cloning expensive types.

collect_seq with References

use serde::{Serialize, Serializer};
 
struct Container {
    items: Vec<String>,
}
 
impl Serialize for Container {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Works with references
        serializer.collect_seq(self.items.iter())
    }
}
 
// Also works with references to values
struct RefContainer<'a> {
    items: &'a [String],
}
 
impl<'a> Serialize for RefContainer<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.collect_seq(self.items.iter())
    }
}
 
// And with reference transformations
struct Transformed<'a> {
    items: &'a [i32],
}
 
impl<'a> Serialize for Transformed<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Serialize transformed references
        serializer.collect_seq(self.items.iter().map(|&n| n * 2))
    }
}

collect_seq handles references and transformations without collecting.

Custom Iterator Serialization

use serde::{Serialize, Serializer};
use std::collections::HashMap;
 
struct MapEntries<'a>(&'a HashMap<String, i32>);
 
impl<'a> Serialize for MapEntries<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Serialize map entries as a sequence
        serializer.collect_seq(self.0.iter().map(|(k, v)| (k, v)))
    }
}
 
fn main() {
    let mut map = HashMap::new();
    map.insert("a".to_string(), 1);
    map.insert("b".to_string(), 2);
    
    let entries = MapEntries(&map);
    let json = serde_json::to_string(&entries).unwrap();
    println!("{}", json);
    // [["a",1],["b",2]] or similar
}

Custom serializers can use collect_seq for iterator-based serialization.

Serializing Option Containers

use serde::{Serialize, Serializer};
 
struct OptionalItems {
    items: Option<Vec<i32>>,
}
 
impl Serialize for OptionalItems {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        match &self.items {
            Some(items) => serializer.collect_seq(items.iter()),
            None => serializer.serialize_none(),
        }
    }
}
 
// Or flatten into sequence:
struct FlattenedItems {
    items: Vec<Option<i32>>,
}
 
impl Serialize for FlattenedItems {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Filter out None values
        serializer.collect_seq(
            self.items.iter().filter_map(|opt| opt.as_ref())
        )
    }
}

collect_seq integrates naturally with Option handling in serializers.

Comparison with collect_map

use serde::{Serialize, Serializer};
use std::collections::HashMap;
 
struct Data {
    map: HashMap<String, i32>,
}
 
impl Serialize for Data {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // For maps, use collect_map instead
        serializer.collect_map(self.map.iter())
    }
}
 
// collect_seq for sequences
impl Serialize for Data {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // This would serialize as a sequence of pairs, not a map
        serializer.collect_seq(self.map.iter())
    }
}

Use collect_map for map-like structures; collect_seq for sequences.

Nested Collections

use serde::{Serialize, Serializer};
 
struct Matrix {
    rows: Vec<Vec<i32>>,
}
 
impl Serialize for Matrix {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Each row is serialized without intermediate allocation
        serializer.collect_seq(
            self.rows.iter().map(|row| row.iter())
        )
    }
}
 
fn main() {
    let matrix = Matrix {
        rows: vec![
            vec![1, 2, 3],
            vec![4, 5, 6],
        ],
    };
    
    let json = serde_json::to_string(&matrix).unwrap();
    println!("{}", json);
    // [[1,2,3],[4,5,6]]
}

Nested iterators serialize efficiently without intermediate allocations.

Combining with SerializeSeq

use serde::{Serialize, Serializer, ser::SerializeSeq};
 
struct ComplexData {
    metadata: String,
    items: Vec<i32>,
}
 
impl Serialize for ComplexData {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Use collect_seq within a custom structure
        let mut seq = serializer.serialize_seq(Some(2))?;
        seq.serialize_element(&self.metadata)?;
        seq.serialize_element(&self.items.iter().collect::<Vec<_>>())?;
        seq.end()
    }
}
 
// Or more efficiently:
impl Serialize for ComplexData {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        use serde::ser::SerializeTuple;
        let mut tuple = serializer.serialize_tuple(2)?;
        tuple.serialize_element(&self.metadata)?;
        // Use collect_seq for the items
        tuple.serialize_element(&serde::private::ser::IteratorAsSeq(&self.items.iter()))?;
        tuple.end()
    }
}

collect_seq can be combined with manual sequence serialization for complex structures.

Performance Comparison

use serde::{Serialize, Serializer};
 
struct Items {
    values: Vec<i32>,
}
 
// Approach 1: Collect then serialize (allocation)
impl Serialize for Items {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Allocates Vec
        let collected: Vec<&i32> = self.values.iter().collect();
        collected.serialize(serializer)
    }
}
 
// Approach 2: collect_seq (no allocation)
impl Serialize for Items {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // No intermediate allocation
        serializer.collect_seq(self.values.iter())
    }
}
 
// For large datasets, approach 2 uses significantly less memory:
fn benchmark() {
    let large: Vec<i32> = (0..1_000_000).collect();
    
    // Approach 1: Allocates additional Vec of 1 million references
    // Memory: ~8MB for references (on 64-bit)
    
    // Approach 2: No additional allocation
    // Memory: Only the serialized output buffer
}

The performance benefit grows with dataset size.

collect_seq with Custom Types

use serde::{Serialize, Serializer};
 
struct Person {
    name: String,
    age: u32,
}
 
struct People<'a> {
    people: &'a [Person],
}
 
impl<'a> Serialize for People<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.collect_seq(self.people.iter())
    }
}
 
// With transformation:
struct Names<'a> {
    people: &'a [Person],
}
 
impl<'a> Serialize for Names<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Serialize just names
        serializer.collect_seq(self.people.iter().map(|p| &p.name))
    }
}
 
fn main() {
    let people = vec![
        Person { name: "Alice".to_string(), age: 30 },
        Person { name: "Bob".to_string(), age: 25 },
    ];
    
    let names = Names { people: &people };
    let json = serde_json::to_string(&names).unwrap();
    println!("{}", json);
    // ["Alice","Bob"]
}

Transform complex types to simpler serializations without intermediate collections.

Infinite Iterators and Laziness

use serde::{Serialize, Serializer};
 
// Be careful: collect_seq consumes the iterator
// Infinite iterators will hang or overflow
 
struct Fibonacci {
    count: usize,
}
 
impl Serialize for Fibonacci {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Finite: take() limits the iterator
        serializer.collect_seq(
            std::iter::successors(Some((0, 1)), |&(a, b)| Some((b, a + b)))
                .map(|(a, _)| a)
                .take(self.count)
        )
    }
}
 
fn main() {
    let fib = Fibonacci { count: 10 };
    let json = serde_json::to_string(&fib).unwrap();
    println!("{}", json);
    // [0,1,1,2,3,5,8,13,21,34]
}

Ensure iterators are finite; use take() to limit infinite sequences.

Alternative: serialize_seq Manual Implementation

use serde::{Serialize, Serializer, ser::SerializeSeq};
 
struct ManualSeq<'a, T>(&'a [T]);
 
impl<'a, T: Serialize> Serialize for ManualSeq<'a, T> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        // Manual implementation (what collect_seq does internally)
        let mut seq = serializer.serialize_seq(None)?;
        for item in self.0 {
            seq.serialize_element(item)?;
        }
        seq.end()
    }
}
 
// collect_seq is equivalent but more concise:
impl<'a, T: Serialize> Serialize for ManualSeq<'a, T> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.collect_seq(self.0.iter())
    }
}

collect_seq provides the manual pattern as a convenient method.

collect_seq in Derive Macros

use serde::Serialize;
 
// When using #[derive(Serialize)], serde uses collect_seq
// for sequence types when possible
 
#[derive(Serialize)]
struct Container {
    // Vec serializes using collect_seq internally
    items: Vec<i32>,
    
    // Slices also use collect_seq
    refs: &'static [String],
    
    // Iterators don't derive Serialize, but can be used manually
}
 
// Custom derive that uses collect_seq:
impl Serialize for Container {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        use serde::ser::SerializeStruct;
        let mut s = serializer.serialize_struct("Container", 2)?;
        s.serialize_field("items", &self.items)?;
        s.serialize_field("refs", &self.refs)?;
        s.end()
    }
}

The derive macro uses collect_seq internally for efficient sequence serialization.

API Summary

use serde::Serializer;
 
// Basic usage
fn example1<S: Serializer>(items: &[i32], serializer: S) -> Result<S::Ok, S::Error> {
    serializer.collect_seq(items.iter())
}
 
// With transformation
fn example2<S: Serializer>(items: &[i32], serializer: S) -> Result<S::Ok, S::Error> {
    serializer.collect_seq(items.iter().map(|&n| n * 2))
}
 
// With filtering
fn example3<S: Serializer>(items: &[i32], serializer: S) -> Result<S::Ok, S::Error> {
    serializer.collect_seq(items.iter().filter(|&&n| n > 0))
}
 
// With references
fn example4<S: Serializer>(items: &Vec<String>, serializer: S) -> Result<S::Ok, S::Error> {
    serializer.collect_seq(items.iter())
}
 
// Complex transformations
fn example5<S: Serializer>(items: &[(String, i32)], serializer: S) -> Result<S::Ok, S::Error> {
    serializer.collect_seq(
        items.iter()
            .filter(|(_, v)| *v > 0)
            .map(|(k, v)| format!("{}={}", k, v))
    )
}

Synthesis

Key differences between approaches:

Approach Allocation Memory Use Case
.collect::<Vec<_>>().serialize() Yes O(n) When collection is needed elsewhere
collect_seq(iter) No O(1) extra Streaming serialization

collect_seq benefits:

Benefit Explanation
No intermediate allocation Streams directly to output
Works with any iterator Maps, filters, chains
Lazy evaluation Transforms happen during serialization
Lower memory footprint No temporary Vec storage

When collect_seq is essential:

Scenario Why it matters
Large datasets Avoiding Vec allocation saves memory
Expensive clones Reference serialization avoids cloning
Streaming output Direct serialization to writer
Computed sequences No need to materialize values

Comparison with collect_map:

Method Output Input
collect_seq JSON array IntoIterator<Item>
collect_map JSON object IntoIterator<Item = (K, V)>

Key insight: collect_seq eliminates the common pattern of collecting an iterator into a Vec before serialization, which wastes memory for the temporary container and CPU cycles for allocation. Instead, it uses the SerializeSeq trait to stream each element directly to the serializer's output, with the iterator acting as a lazy producer of values. This is particularly valuable for large datasets, expensive-to-clone items, transformed sequences, and computed values that don't need to exist outside serialization. The method integrates seamlessly with iterator combinators like map, filter, and filter_map, enabling complex transformations without intermediate collections. For map-like structures, collect_map provides similar functionality for key-value pairs. The efficiency gain comes from serde's design: SerializeSeq::serialize_element can write directly to the output (e.g., a JSON writer's buffer) without requiring the entire sequence to be buffered first.