Loading page…
Rust walkthroughs
Loading page…
serde::ser::SerializeMap differ from SerializeStruct for serializing complex data structures?SerializeMap and SerializeStruct are both serializer output types that represent structured data, but they serve different conceptual models: SerializeMap serializes dynamic key-value collections where keys and values are determined at runtime, while SerializeStruct serializes fixed-schema structures where field names are known in advance. The serializer implementation chooses which to use based on whether the data has a known schema (struct) or arbitrary keys (map). In JSON, both produce objects, but in other formats like binary serializers, they may encode differently. The key distinction is that SerializeStruct::serialize_field takes a &str for the field name while SerializeMap::serialize_entry takes a serializable key, allowing the map to encode any key type.
use serde::ser::{Serialize, Serializer, SerializeStruct};
struct User {
id: u64,
name: String,
email: String,
}
impl Serialize for User {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// For structs with known fields, use SerializeStruct
// Field count (3) helps serializers pre-allocate
let mut s = serializer.serialize_struct("User", 3)?;
// Field names are &str - known at compile time
s.serialize_field("id", &self.id)?;
s.serialize_field("name", &self.name)?;
s.serialize_field("email", &self.email)?;
s.end()
}
}
fn main() {
let user = User { id: 1, name: "Alice".into(), email: "alice@example.com".into() };
let json = serde_json::to_string(&user).unwrap();
println!("{}", json); // {"id":1,"name":"Alice","email":"alice@example.com"}
}SerializeStruct requires field names to be string literals known at call time.
use serde::ser::{Serialize, Serializer, SerializeMap};
use std::collections::HashMap;
struct DynamicConfig {
settings: HashMap<String, String>,
}
impl Serialize for DynamicConfig {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// For dynamic key-value pairs, use SerializeMap
let mut map = serializer.serialize_map(Some(self.settings.len()))?;
// Keys are serialized values - can be any type
for (key, value) in &self.settings {
map.serialize_entry(key, value)?;
}
map.end()
}
}
fn main() {
let mut settings = HashMap::new();
settings.insert("theme".to_string(), "dark".to_string());
settings.insert("language".to_string(), "en".to_string());
let config = DynamicConfig { settings };
let json = serde_json::to_string(&config).unwrap();
println!("{}", json);
}SerializeMap allows any serializable key type, determined at runtime.
use serde::ser::{Serializer, SerializeMap, SerializeStruct};
use serde_json::Value;
fn serialize_as_map<S: Serializer>(data: &[(String, Value)], serializer: S) -> Result<S::Ok, S::Error> {
// Map: keys are serialized values
let mut map = serializer.serialize_map(Some(data.len()))?;
for (key, value) in data {
// Key is serializable - can be any type that implements Serialize
map.serialize_entry(key, value)?;
}
map.end()
}
fn serialize_as_struct<S: Serializer>(serializer: S) -> Result<S::Ok, S::Error> {
// Struct: field names are &str literals
let mut s = serializer.serialize_struct("MyStruct", 3)?;
// Field names must be string slices known at call time
s.serialize_field("fixed_field_1", &42)?;
s.serialize_field("fixed_field_2", &"hello")?;
s.serialize_field("fixed_field_3", &true)?;
s.end()
}
fn main() {
// Map: arbitrary string keys
let data = vec![
("key1".to_string(), Value::Number(1.into())),
("key2".to_string(), Value::String("value".into())),
];
// Can use dynamic keys
let map_result = serialize_as_map(&data, serde_json::serializer());
}The key difference: serialize_field takes &str, serialize_entry takes a serializable key.
use serde::ser::{Serialize, Serializer, SerializeMap};
use std::collections::BTreeMap;
struct LookupTable {
// Numeric keys are valid for maps
entries: BTreeMap<u32, String>,
}
impl Serialize for LookupTable {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(self.entries.len()))?;
for (key, value) in &self.entries {
// Numeric key - serialize_entry accepts any Serialize
map.serialize_entry(key, value)?;
}
map.end()
}
}
fn main() {
let mut entries = BTreeMap::new();
entries.insert(100, "first");
entries.insert(200, "second");
entries.insert(300, "third");
let table = LookupTable { entries };
let json = serde_json::to_string(&table).unwrap();
println!("{}", json); // {"100":"first","200":"second","300":"third"}
}SerializeMap can use numeric or any serializable key; SerializeStruct fields are always string names.
use serde::ser::{Serialize, Serializer, SerializeStruct, SerializeMap};
use serde_json::Value;
// Struct: homogeneous field types after serialization
struct ApiResponse {
status: u16,
message: String,
data: Value, // Generic, but fields are fixed
}
impl Serialize for ApiResponse {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut s = serializer.serialize_struct("ApiResponse", 3)?;
s.serialize_field("status", &self.status)?;
s.serialize_field("message", &self.message)?;
s.serialize_field("data", &self.data)?;
s.end()
}
}
// Map: fully dynamic structure
struct DynamicResponse {
fields: Vec<(String, Value)>,
}
impl Serialize for DynamicResponse {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(self.fields.len()))?;
for (key, value) in &self.fields {
map.serialize_entry(key, value)?;
}
map.end()
}
}Structs work well for fixed schemas; maps for dynamic structures.
use serde::ser::{Serialize, Serializer, SerializeStruct};
struct User {
id: u64,
name: String,
email: Option<String>,
}
impl Serialize for User {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Count only non-skipped fields for accurate length hint
let mut field_count = 2; // id, name always present
if self.email.is_some() {
field_count += 1;
}
let mut s = serializer.serialize_struct("User", field_count)?;
s.serialize_field("id", &self.id)?;
s.serialize_field("name", &self.name)?;
// Skip serializing None values
if let Some(ref email) = self.email {
s.serialize_field("email", email)?;
}
s.end()
}
}
fn main() {
let user = User { id: 1, name: "Alice".into(), email: None };
let json = serde_json::to_string(&user).unwrap();
println!("{}", json); // {"id":1,"name":"Alice"} - email skipped
}SerializeStruct allows conditional field inclusion; SerializeMap includes all entries.
use serde::ser::{Serialize, Serializer, SerializeStruct};
struct Config {
name: String,
version: String,
#[serde(skip)]
secret_key: String, // Internal field, not serialized
}
impl Serialize for Config {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Only serialize the fields we want
let mut s = serializer.serialize_struct("Config", 2)?;
s.serialize_field("name", &self.name)?;
s.serialize_field("version", &self.version)?;
// secret_key is intentionally skipped
s.end()
}
}
// With SerializeMap, you'd filter entries during iterationSerializeStruct gives explicit control over which fields to include.
use serde::ser::{Serialize, Serializer, SerializeStruct};
use std::collections::HashMap;
struct NestedConfig {
base: BaseConfig,
overrides: HashMap<String, String>,
}
struct BaseConfig {
timeout: u64,
retries: u32,
}
impl Serialize for NestedConfig {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Flatten base fields into parent
let mut s = serializer.serialize_struct("NestedConfig", 4)?;
// Flatten base config fields
s.serialize_field("timeout", &self.base.timeout)?;
s.serialize_field("retries", &self.base.retries)?;
// Flatten map entries
for (key, value) in &self.overrides {
s.serialize_field(key, value)?;
}
s.end()
}
}
// Note: Flattening maps into structs requires careful handling
// as struct field names are staticFlattening requires SerializeStruct when mixing fixed and dynamic fields.
use serde::ser::{Serialize, Serializer, SerializeStruct};
// A newtype struct wraps a single value
struct UserId(u64);
impl Serialize for UserId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Newtype: use serialize_newtype_struct
serializer.serialize_newtype_struct("UserId", &self.0)
}
}
// Compare to a struct with one field
struct UserIdStruct {
id: u64,
}
impl Serialize for UserIdStruct {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Regular struct: use serialize_struct
let mut s = serializer.serialize_struct("UserIdStruct", 1)?;
s.serialize_field("id", &self.id)?;
s.end()
}
}
fn main() {
let newtype = UserId(42);
let regular = UserIdStruct { id: 42 };
// Newtype serializes as the inner value directly
// Regular struct serializes as {"id":42}
}serialize_newtype_struct is a special case for transparent wrapper types.
use serde::ser::{Serialize, Serializer, SerializeStruct, SerializeMap};
use serde_json::Value;
enum Data {
Object { id: u64, name: String },
Dynamic { entries: Vec<(String, Value)> },
}
impl Serialize for Data {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Data::Object { id, name } => {
// Fixed schema: use SerializeStruct
let mut s = serializer.serialize_struct("Object", 2)?;
s.serialize_field("id", id)?;
s.serialize_field("name", name)?;
s.end()
}
Data::Dynamic { entries } => {
// Dynamic keys: use SerializeMap
let mut map = serializer.serialize_map(Some(entries.len()))?;
for (key, value) in entries {
map.serialize_entry(key, value)?;
}
map.end()
}
}
}
}
fn main() {
let object = Data::Object { id: 1, name: "test".into() };
let dynamic = Data::Dynamic {
entries: vec![
("key1".into(), Value::Number(1.into())),
("key2".into(), Value::String("value".into())),
]
};
}Choose SerializeStruct or SerializeMap based on whether the schema is known.
use serde::ser::{Serialize, Serializer, SerializeStruct, SerializeMap};
// Both accept a length hint for optimization
impl Serialize for MyData {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Struct: hint is number of fields
let mut s = serializer.serialize_struct("MyData", 3)?;
s.serialize_field("a", &1)?;
s.serialize_field("b", &2)?;
s.serialize_field("c", &3)?;
s.end()
}
}
impl Serialize for MyMap {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Map: hint is number of entries
// None means unknown size (like Iterator::size_hint)
let mut map = serializer.serialize_map(Some(self.entries.len()))?;
for (k, v) in &self.entries {
map.serialize_entry(k, v)?;
}
map.end()
}
}
// Both allow None if size is unknown
let mut map = serializer.serialize_map(None)?; // Unknown sizeLength hints help serializers pre-allocate buffers.
use serde::ser::{Serializer, SerializeStruct, SerializeMap, Ok, Error};
use std::collections::HashMap;
// Hypothetical custom serializer showing the distinction
struct MySerializer;
impl Serializer for MySerializer {
type Ok = String;
type Error = String;
type SerializeStruct = StructSerializer;
type SerializeMap = MapSerializer;
fn serialize_struct(self, name: &'static str, len: usize) -> Result<Self::SerializeStruct, Self::Error> {
// Struct serializer knows the type name
// Can output type information for formats that support it
println!("Starting struct '{}' with {} fields", name, len);
Ok(StructSerializer { fields: Vec::new() })
}
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
// Map serializer has no type name
// Keys are generic, not tied to schema
println!("Starting map with {:?} entries", len);
Ok(MapSerializer { entries: Vec::new() })
}
}
struct StructSerializer {
fields: Vec<String>,
}
impl SerializeStruct for StructSerializer {
type Ok = String;
type Error = String;
fn serialize_field(&mut self, key: &'static str, value: &dyn Serialize) -> Result<(), Self::Error> {
// Field names are static strings
self.fields.push(format!("{}: {:?}", key, value));
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(format!("struct({})", self.fields.join(", ")))
}
}
struct MapSerializer {
entries: Vec<String>,
}
impl SerializeMap for MapSerializer {
type Ok = String;
type Error = String;
fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<(), Self::Error> {
// Key can be any serializable type
// Serialize key, store for when value comes
Ok(())
}
fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
// Value for the preceding key
Ok(())
}
fn serialize_entry<K: ?Sized + Serialize, V: ?Sized + Serialize>(
&mut self, key: &K, value: &V
) -> Result<(), Self::Error> {
// Combined key-value entry
self.entries.push(format!("{:?}: {:?}", key, value));
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(format!("map({})", self.entries.join(", ")))
}
}Implementing a serializer requires handling both types differently.
// In JSON, both produce objects:
// SerializeStruct {"id": 1, "name": "Alice"}
// SerializeMap {"key1": "value1", "key2": "value2"}
// In binary formats, they may encode differently:
// - Struct: type tag + known field order, can skip field names
// - Map: key-value pairs with encoded key type
// Example: Bincode encodes struct fields in order without names
// Map entries encode the key before each value
// This is why SerializeStruct has a type name but SerializeMap doesn't
// Structs are typed, Maps are anonymous collectionsFormat implementations can optimize structs based on known field order.
// Use SerializeStruct when:
// 1. You have a Rust struct with named fields
// 2. Field names are known at compile time
// 3. You want to skip certain fields
// 4. You need to flatten structures
// 5. The type has semantic meaning (type name matters)
// Use SerializeMap when:
// 1. Keys are dynamic/runtime-determined
// 2. Keys are not string literals
// 3. Serializing HashMap, BTreeMap, or similar
// 4. Keys have types other than string
// 5. The structure is anonymous/untypedChoose based on whether your data has a fixed schema or dynamic keys.
SerializeStruct characteristics:
&'static str literalsSerializeMap characteristics:
None)API differences:
SerializeStruct::serialize_field(&mut self, key: &'static str, value: &T)SerializeMap::serialize_entry(&mut self, key: &K, value: &V)SerializeMap also has serialize_key and serialize_value for two-step encodingFormat implications:
Derived behavior:
#[derive(Serialize)] on structs uses SerializeStruct#[derive(Serialize)] on HashMap/BTreeMap uses SerializeMap#[serde(flatten)] uses SerializeMap internally for dynamic portionKey insight: SerializeStruct and SerializeMap represent two conceptual models of structured data that happen to produce similar output in JSON but encode differently in formats that care about schemas versus collections. The serializer chooses based on whether you're serializing a typed Rust struct with fixed fields or a dynamic collection of key-value pairs. When implementing Serialize manually, use SerializeStruct for any fixed schema where field names are known at compile time and SerializeMap when keys are determined at runtime or are non-string types. The distinction matters most for binary formats that can optimize struct encoding by assuming field order, while map encoding must include each key explicitly.