Loading page…
Rust walkthroughs
Loading page…
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.
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.
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.
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.
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.
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 allocationcollect_seq uses serialize_seq to stream elements directly to output.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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))
)
}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.