How does serde_json::Map::retain filter key-value pairs in place compared to iteration-based filtering?
serde_json::Map::retain removes key-value pairs that don't satisfy a predicate in a single in-place operation, avoiding the allocation and copying overhead of iteration-based filtering which requires collecting matching elements into a new map. The retain method modifies the map directly, preserving the original allocation and only shifting remaining elements, while iteration-based approaches create a new map and move or clone the retained elements.
The retain Method Signature
use serde_json::Map;
use serde_json::Value;
fn method_signature() {
let mut map: Map<String, Value> = Map::new();
// retain removes entries where predicate returns false
map.retain(|key, value| {
// Return true to keep, false to remove
value.is_string()
});
// The signature is:
// pub fn retain<F>(&mut self, f: F)
// where
// F: FnMut(&String, &mut Value) -> bool
}retain takes a closure that receives a reference to the key and a mutable reference to the value.
Basic retain Usage
use serde_json::{Map, Value, json};
fn basic_retain() {
let mut map: Map<String, Value> = Map::new();
map.insert("name".to_string(), json!("Alice"));
map.insert("age".to_string(), json!(30));
map.insert("active".to_string(), json!(true));
map.insert("score".to_string(), json!(null));
// Keep only string values
map.retain(|_key, value| value.is_string());
assert_eq!(map.len(), 1);
assert!(map.contains_key("name"));
assert!(!map.contains_key("age"));
// Map now only contains: {"name": "Alice"}
}retain modifies the map in place, keeping only entries matching the predicate.
Iteration-Based Filtering Approach
use serde_json::{Map, Value, json};
fn iteration_filtering() {
let mut map: Map<String, Value> = Map::new();
map.insert("name".to_string(), json!("Alice"));
map.insert("age".to_string(), json!(30));
map.insert("active".to_string(), json!(true));
map.insert("score".to_string(), json!(null));
// Iteration-based: create new map with matching entries
let mut filtered: Map<String, Value> = Map::new();
for (key, value) in map.iter() {
if value.is_string() {
filtered.insert(key.clone(), value.clone());
}
}
// Or using collect:
let filtered2: Map<String, Value> = map.iter()
.filter(|(_, value)| value.is_string())
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
// Original map unchanged
assert_eq!(map.len(), 4);
// Filtered has 1 entry
assert_eq!(filtered.len(), 1);
}Iteration-based filtering creates a new map, cloning retained elements.
Memory and Allocation Differences
use serde_json::{Map, Value, json};
fn memory_comparison() {
// Create a large map
let mut map: Map<String, Value> = (0..1000)
.map(|i| (format!("key{}", i), json!(i)))
.collect();
// retain approach:
// - Operates in place on existing allocation
// - No new allocation for the map itself
// - Elements that stay don't need to be moved/cloned
// - Elements that are removed are dropped in place
map.retain(|key, _| key.starts_with("key1"));
// Memory: original allocation, but some entries removed
// Iteration approach:
// - Creates new Map allocation
// - Clones each retained key-value pair
// - Old map stays in memory until dropped
// - More memory pressure during operation
// For large maps with few removals:
// retain is much more efficient - no cloning of kept entries
// For large maps with many removals:
// retain still wins - operates on single allocation
// Trade-off: iteration can release old memory by dropping original
}retain avoids allocation and cloning; iteration creates copies.
Access to Mutable Values in retain
use serde_json::{Map, Value, json};
fn mutable_access() {
let mut map: Map<String, Value> = Map::new();
map.insert("name".to_string(), json!(" alice "));
map.insert("age".to_string(), json!("30"));
map.insert("notes".to_string(), json!(" some notes "));
// retain gives mutable access to values
map.retain(|key, value| {
// Can mutate values while filtering
if value.is_string() {
if let Some(s) = value.as_str() {
*value = json!(s.trim());
}
}
// Return true to keep all entries
true
});
assert_eq!(map.get("name").unwrap(), "alice");
assert_eq!(map.get("notes").unwrap(), "some notes");
}retain provides mutable access to values, enabling transformation during filtering.
Combining Filtering and Mutation
use serde_json::{Map, Value, json};
fn filter_and_mutate() {
let mut map: Map<String, Value> = Map::new();
map.insert("name".to_string(), json!("alice"));
map.insert("age".to_string(), json!(30));
map.insert("email".to_string(), json!("alice@example.com"));
map.insert("temp".to_string(), json!(true));
// Remove certain keys AND normalize remaining string values
map.retain(|key, value| {
// Remove temporary fields
if key == "temp" {
return false;
}
// Normalize string values (lowercase)
if let Some(s) = value.as_str() {
*value = json!(s.to_lowercase());
}
true
});
assert_eq!(map.get("name").unwrap(), "alice");
assert_eq!(map.get("email").unwrap(), "alice@example.com");
assert!(!map.contains_key("temp"));
}Combine filtering logic with value transformation in one pass.
Performance Characteristics
use serde_json::{Map, Value, json};
use std::time::Instant;
fn performance_demo() {
// Create a map with 10,000 entries
let mut map: Map<String, Value> = (0..10000)
.map(|i| {
let key = format!("key{}", i);
let value = if i % 3 == 0 {
json!(null)
} else {
json!(i)
};
(key, value)
})
.collect();
// retain: O(n) time, O(1) extra space
let start = Instant::now();
map.retain(|_, value| !value.is_null());
let retain_time = start.elapsed();
// Reset map
let mut map: Map<String, Value> = (0..10000)
.map(|i| (format!("key{}", i), if i % 3 == 0 { json!(null) } else { json!(i) }))
.collect();
// iteration + collect: O(n) time, O(n) space for new map
let start = Instant::now();
let filtered: Map<String, Value> = map.iter()
.filter(|(_, v)| !v.is_null())
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let collect_time = start.elapsed();
// retain is typically faster because:
// 1. No cloning of retained values
// 2. No allocation of new map structure
// 3. Single pass through the data
println!("retain: {:?}", retain_time);
println!("collect: {:?}", collect_time);
}retain is faster due to no cloning or new allocation.
Iteration with Removal Pitfalls
use serde_json::{Map, Value, json};
fn removal_pitfalls() {
let mut map: Map<String, Value> = Map::new();
map.insert("a".to_string(), json!(1));
map.insert("b".to_string(), json!(2));
map.insert("c".to_string(), json!(3));
// WRONG: Cannot remove during iteration
// for (key, _) in map.iter() {
// if key == "b" {
// map.remove(key); // Compile error!
// }
// }
// WRONG: Using iter_mut to remove
// for (key, _) in map.iter_mut() {
// // Cannot remove from map during mutable iteration
// }
// CORRECT: Use retain for safe in-place filtering
map.retain(|key, _| key != "b");
assert_eq!(map.len(), 2);
assert!(!map.contains_key("b"));
// Alternative: drain + collect (still creates new collection)
// Or: collect keys to remove, then remove one by one
}retain is the safe way to remove during iteration; direct removal would cause iterator invalidation.
Key-Based Filtering
use serde_json::{Map, Value, json};
fn key_filtering() {
let mut map: Map<String, Value> = Map::new();
map.insert("user_id".to_string(), json!(123));
map.insert("user_name".to_string(), json!("alice"));
map.insert("admin_id".to_string(), json!(456));
map.insert("admin_name".to_string(), json!("bob"));
map.insert("temp_data".to_string(), json!([]));
// Keep only user_* keys
map.retain(|key, _| key.starts_with("user_"));
assert!(map.contains_key("user_id"));
assert!(map.contains_key("user_name"));
assert!(!map.contains_key("admin_id"));
// Or using a set of allowed keys
let allowed = std::collections::HashSet::from([
"user_id".to_string(),
"user_name".to_string(),
]);
map.retain(|key, _| allowed.contains(key));
}Filter by key patterns or whitelists using the key parameter.
Value-Based Filtering
use serde_json::{Map, Value, json};
fn value_filtering() {
let mut map: Map<String, Value> = Map::new();
map.insert("count".to_string(), json!(42));
map.insert("name".to_string(), json!("test"));
map.insert("active".to_string(), json!(true));
map.insert("data".to_string(), json!(null));
map.insert("items".to_string(), json!([1, 2, 3]));
// Keep only non-null values
map.retain(|_, value| !value.is_null());
assert!(!map.contains_key("data"));
// Keep only numbers
let mut map2: Map<String, Value> = map.clone();
map2.retain(|_, value| value.is_number());
assert!(map2.contains_key("count"));
assert!(!map2.contains_key("name"));
// Keep only arrays
let mut map3: Map<String, Value> = map.clone();
map3.retain(|_, value| value.is_array());
assert!(map3.contains_key("items"));
// Keep only truthy values
let mut map4: Map<String, Value> = map.clone();
map4.retain(|_, value| {
match value {
Value::Null => false,
Value::Bool(b) => *b,
Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
Value::String(s) => !s.is_empty(),
Value::Array(a) => !a.is_empty(),
Value::Object(o) => !o.is_empty(),
}
});
}Filter by value type, content, or computed properties.
Complex Filtering Logic
use serde_json::{Map, Value, json};
fn complex_filtering() {
let mut map: Map<String, Value> = Map::new();
map.insert("users".to_string(), json!([{"id": 1}, {"id": 2}]));
map.insert("config".to_string(), json!({"debug": true}));
map.insert("count".to_string(), json!(100));
map.insert("empty_arr".to_string(), json!([]));
map.insert("metadata".to_string(), json!({"version": "1.0"}));
// Complex: keep non-empty objects and arrays, all other truthy values
map.retain(|key, value| {
match value {
Value::Array(arr) => {
// Keep non-empty arrays, unless key suggests temporary
!arr.is_empty() && !key.starts_with("temp")
}
Value::Object(obj) => {
// Keep non-empty objects
!obj.is_empty()
}
Value::Null => false,
Value::Bool(b) => *b,
Value::Number(_) => true,
Value::String(s) => !s.is_empty(),
}
});
assert!(map.contains_key("users")); // non-empty array
assert!(map.contains_key("config")); // non-empty object
assert!(map.contains_key("count")); // number
assert!(!map.contains_key("empty_arr")); // empty array removed
}Combine multiple conditions for sophisticated filtering.
Comparison: retain vs Iteration Patterns
use serde_json::{Map, Value, json};
fn pattern_comparison() {
let mut original: Map<String, Value> = Map::new();
original.insert("a".to_string(), json!(1));
original.insert("b".to_string(), json!(2));
original.insert("c".to_string(), json!(3));
// Pattern 1: retain (in-place)
let mut map1 = original.clone();
map1.retain(|key, _| key != "b");
// Pros:
// - No allocation
// - No cloning
// - Single pass
// - Original map modified
// Cons:
// - Original map modified (might need clone first)
// Pattern 2: clone + retain
let mut map2 = original.clone();
map2.retain(|key, _| key != "b");
// Pros: same as retain
// Cons: requires clone if you need to preserve original
// Pattern 3: iteration + collect
let map3: Map<String, Value> = original.iter()
.filter(|(key, _)| key != &"b")
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
// Pros:
// - Original preserved
// - Can transform during collection
// Cons:
// - Allocates new map
// - Clones retained values
// Pattern 4: drain + filter + collect
let mut map4 = original.clone();
let drained: Map<String, Value> = map4.drain()
.filter(|(key, _)| key != "b")
.collect();
// Pros:
// - Takes ownership, no cloning needed
// - Original emptied (memory released)
// Cons:
// - Original destroyed
// - Creates new map allocation
}Choose based on whether you need the original and want to avoid allocation.
When to Use retain
use serde_json::{Map, Value, json};
fn when_to_use_retain() {
// BEST for retain:
// 1. Large maps where allocation cost matters
let mut large_map: Map<String, Value> = (0..100000)
.map(|i| (format!("key{}", i), json!(i)))
.collect();
large_map.retain(|key, _| key.ends_with('7'));
// No allocation, just removal
// 2. When you don't need the original
fn process_request_data(data: &mut Map<String, Value>) {
// Remove internal fields before returning
data.retain(|key, _| !key.starts_with('_'));
}
// 3. Single-pass filtering + mutation
let mut map: Map<String, Value> = Map::new();
map.insert("name".to_string(), json!(" alice "));
map.insert("temp".to_string(), json!(null));
map.retain(|key, value| {
if key == "temp" {
return false;
}
if let Some(s) = value.as_str() {
*value = json!(s.trim());
}
true
});
// One pass for both filtering and transformation
// 4. Chained with other in-place operations
let mut config: Map<String, Value> = Map::new();
// ... populate config ...
config.retain(|_, v| !v.is_null()); // Remove nulls
config.retain(|k, _| k.starts_with("valid_")); // Then filter keys
}Use retain for large maps, in-place modifications, and combined filtering/mutation.
When to Prefer Iteration-Based Filtering
use serde_json::{Map, Value, json};
fn when_to_use_iteration() {
// BEST for iteration-based:
// 1. When you need both original and filtered
let original: Map<String, Value> = Map::new();
// ... populate ...
let filtered: Map<String, Value> = original.iter()
.filter(|(_, v)| v.is_string())
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
// original still available
// 2. When transforming during filter
let transformed: Map<String, Value> = original.iter()
.filter(|(k, _)| k.starts_with("user_"))
.map(|(k, v)| {
let transformed_value = match v {
Value::String(s) => json!(s.to_uppercase()),
other => other.clone(),
};
(k.clone(), transformed_value)
})
.collect();
// 3. When collecting into a different type
let as_vec: Vec<(String, Value)> = original.iter()
.filter(|(_, v)| v.is_number())
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
// 4. Small maps where allocation cost is negligible
let mut small_map: Map<String, Value> = Map::new();
small_map.insert("a".to_string(), json!(1));
small_map.insert("b".to_string(), json!(2));
// Cloning small map is trivial
let filtered = small_map.iter()
.filter(|(_, v)| v.is_number())
.map(|(k, v)| (k.clone(), v.clone()))
.collect::<Map<String, Value>>();
}Use iteration when you need the original, want transformations, or map is small.
Drain as Alternative
use serde_json::{Map, Value, json};
fn drain_alternative() {
let mut map: Map<String, Value> = Map::new();
map.insert("a".to_string(), json!(1));
map.insert("b".to_string(), json!(2));
map.insert("c".to_string(), json!(3));
// drain takes ownership, no cloning needed
let retained: Map<String, Value> = map.drain()
.filter(|(key, _)| key != "b")
.collect();
// Original map is now empty
assert!(map.is_empty());
// Retained has filtered entries (no cloning occurred)
assert_eq!(retained.len(), 2);
assert!(!retained.contains_key("b"));
// Compare to retain:
// - retain: modifies in place, keeps original allocation
// - drain + collect: empties original, creates new allocation
// - Both avoid cloning, but drain releases original memory
}drain takes ownership, avoiding clones while creating a new collection.
Summary Table
fn summary() {
// | Method | Allocation | Cloning | Original | Use Case |
// |-----------------|------------|---------|-----------|-------------------|
// | retain | None | None | Modified | Large maps, inplace|
// | iter+collect | New map | Yes | Preserved | Need original |
// | drain+collect | New map | No | Emptied | Release memory |
// | clone+retain | Clone | Yes | Preserved | Need original |
// | Operation | Time | Extra Space | Mutates Original |
// |------------------|---------|-------------|------------------|
// | retain | O(n) | O(1) | Yes |
// | iter+collect | O(n) | O(n) | No |
// | drain+collect | O(n) | O(n) | Yes (empties) |
// | Scenario | Best Method |
// |-----------------------------|--------------------|
// | Large map, don't need orig | retain |
// | Need original unchanged | iter+collect |
// | Want to release orig memory | drain+collect |
// | Filter + transform | iter+map+collect |
// | Filter + mutate | retain |
}Synthesis
Quick reference:
use serde_json::{Map, Value, json};
let mut map: Map<String, Value> = /* ... */;
// retain: in-place, no allocation, no cloning
map.retain(|key, value| {
// Return true to keep, false to remove
// Can mutate value through mutable reference
!value.is_null() && key.starts_with("valid_")
});
// Alternative: iteration-based (creates new map, clones values)
let filtered: Map<String, Value> = original.iter()
.filter(|(k, v)| !v.is_null() && k.starts_with("valid_"))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();Key insight: Map::retain is the idiomatic and efficient way to filter a JSON map in place. It operates in a single pass, modifying the map directly without allocating a new map or cloning retained valuesâO(n) time and O(1) extra space. The predicate receives (&String, &mut Value), allowing both key inspection and value mutation during filtering. Compare this to iteration-based approaches: collecting into a new map requires O(n) allocation and cloning each retained value. The drain method offers a middle groundâit takes ownership of entries so no cloning occurs, but still requires allocating a new map for the result. Use retain when you want to modify the original map and care about allocation overhead (large maps, hot paths). Use iteration-based collection when you need to preserve the original map or want to transform values during filtering. The mutable value reference in retain's predicate enables powerful patterns like normalizing string values while simultaneously removing unwanted entries, all in a single efficient pass. Unlike attempting to remove during iteration (which would invalidate the iterator), retain handles the complexity of in-place modification safely.
