Loading pageā¦
Rust walkthroughs
Loading pageā¦
serde's #[serde(bound = "...")] attribute help with generic type serialization?The #[serde(bound = "...")] attribute controls the trait bounds that serde generates for Serialize and Deserialize implementations on generic types. By default, serde automatically derives bounds requiring all generic type parameters to implement Serialize or Deserialize, which can be overly restrictive or incorrect for types that don't directly serialize their generic parameters. The bound attribute lets you specify exactly which bounds are needed, enabling serialization of types with generic parameters that are used in ways that don't require serialization themselves, or types where bounds should only apply to certain generic parameters.
use serde::{Serialize, Deserialize};
// Default behavior: serde adds bounds for ALL generic parameters
#[derive(Serialize)]
struct Container<T> {
value: T,
}
// Serde generates:
// impl<T: Serialize> Serialize for Container<T>
// This is correct - T is serialized
#[derive(Serialize)]
struct PhantomContainer<T> {
data: i32,
_marker: std::marker::PhantomData<T>,
}
// Serde generates:
// impl<T: Serialize> Serialize for PhantomContainer<T>
// This is PROBLEMATIC - T is never serialized!
// We can't serialize PhantomContainer<()> if () doesn't implement SerializeBy default, serde requires all generic parameters to implement Serialize or Deserialize, even when they're not actually serialized.
use serde::{Serialize, Deserialize};
use std::marker::PhantomData;
#[derive(Serialize)]
#[serde(bound = "")] // Empty bounds - no requirements on T
struct PhantomContainer<T> {
data: i32,
_marker: PhantomData<T>,
}
fn main() {
// Now this works even though NotSerializable doesn't implement Serialize
struct NotSerializable;
let container = PhantomContainer {
data: 42,
_marker: PhantomData::<NotSerializable>,
};
let json = serde_json::to_string(&container).unwrap();
println!("{}", json); // {"data":42}
}The bound = "" attribute removes all automatically generated bounds.
use serde::{Serialize, Deserialize};
#[derive(Serialize)]
#[serde(bound = "T: Serialize")] // Explicitly specify bounds
struct Wrapper<T> {
inner: T,
}
#[derive(Serialize)]
#[serde(bound = "T: Serialize, U: Serialize")] // Multiple bounds
struct Pair<T, U> {
first: T,
second: U,
}
#[derive(Serialize)]
#[serde(bound = "T: Clone + Serialize")] // Additional trait requirements
struct CloningWrapper<T: Clone> {
inner: T,
}Custom bounds replace the auto-generated ones entirely.
use serde::{Serialize, Deserialize};
#[derive(Deserialize)]
#[serde(bound = "T: Deserialize<'de>")] // Note the lifetime
struct DeserializableWrapper<T> {
value: T,
}
// The 'de lifetime comes from the Deserialize trait
// serde provides it in the bound context
#[derive(Deserialize)]
#[serde(bound = "")] // No bounds needed
struct TypeIdMarker<T: 'static> {
id: u64,
_marker: std::marker::PhantomData<T>,
}
fn example() {
// Can deserialize even with non-Deserialize type parameter
#[derive(Debug)]
struct NonDeserializable;
let marker: TypeIdMarker<NonDeserializable> =
serde_json::from_str(r#"{"id":123}"#).unwrap();
println!("ID: {}", marker.id);
}Deserialization bounds use the special 'de lifetime from the Deserialize trait.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(
bound(
serialize = "T: Serialize",
deserialize = "T: Deserialize<'de>",
)
)]
struct GenericValue<T> {
value: T,
}
// Or use separate attributes
#[derive(Serialize, Deserialize)]
struct AnotherWrapper<T> {
#[serde(bound(
serialize = "T: Serialize",
deserialize = "T: Deserialize<'de>",
))]
value: T,
}Separate bounds for serialization and deserialization allow different requirements.
use serde::{Serialize, Deserialize};
use std::marker::PhantomData;
#[derive(Serialize, Deserialize)]
#[serde(bound = "")] // No bounds on T
struct Processor<T: Process> {
name: String,
config: Config,
#[serde(skip)]
_processor: PhantomData<T>,
}
trait Process {
fn process(&self, input: &str) -> String;
}
#[derive(Serialize, Deserialize)]
struct Config {
timeout: u64,
retries: u32,
}
// T is used for behavior, not data
// We don't need T to be serializable
struct UppercaseProcessor;
impl Process for UppercaseProcessor {
fn process(&self, input: &str) -> String {
input.to_uppercase()
}
}
fn example() {
let processor: Processor<UppercaseProcessor> = Processor {
name: "upper".to_string(),
config: Config { timeout: 30, retries: 3 },
_processor: PhantomData,
};
// This works even though UppercaseProcessor doesn't implement Serialize
let json = serde_json::to_string(&processor).unwrap();
}Types that use generic parameters for behavior, not data, can serialize without requiring bounds on those parameters.
use serde::{Serialize, Deserialize};
trait Backend {
type Connection;
}
#[derive(Serialize, Deserialize)]
#[serde(bound = "B::Connection: Serialize")]
struct ConnectionPool<B: Backend> {
pool_id: u64,
connection: B::Connection,
}
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct BackendConfig<B: Backend> {
name: String,
_backend: std::marker::PhantomData<B>,
}
// When associated types aren't serialized, no bounds neededBounds can reference associated types when those types are actually serialized.
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize)]
#[serde(bound(
serialize = "K: Serialize, V: Serialize",
deserialize = "K: Deserialize<'de> + std::hash::Hash + Eq, V: Deserialize<'de>",
))]
struct LookupTable<K, V> {
name: String,
table: HashMap<K, V>,
}
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct Metadata<T> {
version: u32,
created_at: u64,
// T is purely metadata for the type system
#[serde(skip)]
_type: std::marker::PhantomData<T>,
}
#[derive(Serialize, Deserialize)]
#[serde(bound = "T: Into<String> + Clone + Serialize")]
struct StringifyWrapper<T>
where
T: Into<String> + Clone,
{
#[serde(
serialize_with = "serialize_as_string",
deserialize_with = "deserialize_from_string",
)]
value: T,
}
fn serialize_as_string<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: Into<String> + Clone,
{
serializer.serialize_str(&value.clone().into())
}
fn deserialize_from_string<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: serde::Deserializer<'de>,
T: From<String>,
{
let s = String::deserialize(deserializer)?;
Ok(T::from(s))
}Complex bounds can include multiple traits and custom serialization logic.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct Container<T = i32> {
value: i32, // Doesn't use T
#[serde(skip)]
_marker: std::marker::PhantomData<T>,
}
fn example() {
// Works with default type parameter
let c1: Container = Container { value: 42, _marker: std::marker::PhantomData };
let json = serde_json::to_string(&c1).unwrap();
// Works with explicit type parameter
let c2: Container<String> = Container { value: 42, _marker: std::marker::PhantomData };
let json = serde_json::to_string(&c2).unwrap();
}Default type parameters don't change the bound requirements.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct PluginRegistry {
name: String,
plugins: Vec<String>, // Just names, not the actual trait objects
}
trait Plugin {
fn name(&self) -> &str;
fn execute(&self);
}
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct PluginConfig<P: Plugin> {
enabled: bool,
priority: u32,
#[serde(skip)]
_plugin: std::marker::PhantomData<P>,
}When trait objects or trait bounds exist for behavior, serialization may not need them.
use serde::{Serialize, Deserialize};
// Without proper bounds, you might see errors like:
// "the trait `Serialize` is not implemented for `T`"
// "the trait bound `T: Serialize` is not satisfied"
#[derive(Serialize)]
struct BadContainer<T> {
data: Vec<T>, // T needs to be Serialize
}
#[derive(Serialize)]
#[serde(bound = "T: Serialize")]
struct GoodContainer<T> {
data: Vec<T>,
}
#[derive(Serialize)]
#[serde(bound = "")]
struct NoSerializeNeeded<T> {
count: usize, // T not used in serializable data
#[serde(skip)]
_marker: std::marker::PhantomData<T>,
}
// The error message tells you what bounds are missing:
// error[E0277]: the trait bound `T: Serialize` is not satisfied
// --> src/lib.rs:4:10
// |
//4 | #[derive(Serialize)]
// | ^^^^^^^^^ the trait `Serialize` is not implemented for `T`Compiler errors indicate when bounds are missing or incorrect.
use serde::{Serialize, Deserialize};
use std::marker::PhantomData;
// Type-safe ID wrapper - doesn't need the entity type to be serializable
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct Id<T> {
value: u64,
#[serde(skip)]
_entity: PhantomData<T>,
}
// Entity types - don't need to be serializable for Id serialization
struct User {
id: Id<User>,
name: String,
}
struct Post {
id: Id<Post>,
title: String,
author_id: Id<User>,
}
// Id can be serialized without User or Post implementing Serialize
impl<T> Id<T> {
fn new(value: u64) -> Self {
Id {
value,
_entity: PhantomData,
}
}
}
fn example() {
let user_id: Id<User> = Id::new(42);
let json = serde_json::to_string(&user_id).unwrap();
println!("{}", json); // {"value":42}
let post_id: Id<Post> = Id::new(123);
let json = serde_json::to_string(&post_id).unwrap();
println!("{}", json); // {"value":123}
}Type-safe wrappers with phantom types often don't need bounds on the phantom parameter.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct MixedBounds<T, U> {
// This field requires T: Serialize
#[serde(bound = "T: Serialize")]
serialized: T,
// This field doesn't need bounds
#[serde(bound = "")]
count: usize,
// This field requires U: Serialize
#[serde(bound = "U: Serialize")]
optional: Option<U>,
// PhantomData needs no bounds
#[serde(skip)]
_marker: std::marker::PhantomData<(T, U)>,
}
// Equivalent struct-level bounds:
#[derive(Serialize, Deserialize)]
#[serde(bound(serialize = "T: Serialize, U: Serialize"))]
#[serde(bound(deserialize = "T: Deserialize<'de>, U: Deserialize<'de>"))]
struct AllFieldsBound<T, U> {
serialized: T,
optional: Option<U>,
}Field-level bounds provide granular control over each field's requirements.
use serde::{Serialize, Deserialize};
// Struct with where clause
#[derive(Serialize, Deserialize)]
#[serde(bound = "T: Serialize + Clone")]
struct WhereClauseExample<T>
where
T: Clone,
{
value: T,
}
// Serde's bounds are independent of where clauses
// You must specify bounds that match the serialization needs
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct PhantomWhere<T>
where
T: Send + Sync,
{
name: String,
#[serde(skip)]
_marker: std::marker::PhantomData<T>,
}
// T: Send + Sync doesn't need to be in serde bounds - not serializedStruct where clauses express type constraints; serde bounds express serialization requirements.
use serde::{Serialize, Deserialize};
// WITHOUT bound attribute
#[derive(Serialize)]
struct WithoutBound<T> {
value: T,
}
// Generated impl:
// impl<T: Serialize> Serialize for WithoutBound<T>
// This fails:
// struct NotSerializable;
// let w = WithoutBound { value: NotSerializable };
// serde_json::to_string(&w); // Error: NotSerializable doesn't implement Serialize
// WITH bound attribute
#[derive(Serialize)]
#[serde(bound = "")] // No bounds
struct WithBound<T> {
#[serde(skip)]
value: T,
name: String,
}
// This works:
struct NotSerializable;
let w = WithBound { value: NotSerializable, name: "test".to_string() };
let json = serde_json::to_string(&w).unwrap(); // {"name":"test"}The bound attribute controls exactly what the generated implementation requires.
use serde::{Serialize, Deserialize};
use std::marker::PhantomData;
// Case 1: PhantomData<T> - T not serialized
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct PhantomUse<T> {
data: i32,
_marker: PhantomData<T>,
}
// Case 2: T used only in methods, not fields
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct StrategyPattern<T: Strategy> {
config: Config,
#[serde(skip)]
_strategy: PhantomData<T>,
}
trait Strategy {
fn execute(&self);
}
#[derive(Serialize, Deserialize)]
struct Config {
value: i32,
}
// Case 3: Conditional serialization based on T
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct ConditionalFields<T> {
always_present: String,
#[serde(skip)]
type_specific: PhantomData<T>,
}
// Case 4: Type-erased serialization
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct TypeErased<T> {
type_id: u64,
data: Vec<u8>, // Serialized bytes, not T directly
#[serde(skip)]
_original: PhantomData<T>,
}Use bound when generic parameters don't directly participate in serialization.
The #[serde(bound = "...")] attribute solves a fundamental tension between Rust's type system and derive-based serialization:
Default behavior: Serde conservatively requires all generic parameters to implement Serialize/Deserialize. This is correct for simple cases but overly restrictive when generics are used for:
The bound attribute gives you control:
bound = "" removes all bounds when generic parameters aren't serializedbound = "T: Serialize" specifies exactly what's neededbound(serialize = "...", deserialize = "...") handles different requirements#[serde(bound = "...")] provides per-field controlKey insight: The bound attribute is essential when there's a mismatch between your type's generic parameters and what data is actually serialized. Phantom types are the most common caseāthe type parameter exists for compile-time type safety but holds no runtime data that needs serialization. Without the bound attribute, you'd be forced to implement Serialize for types that have no meaningful serialized representation, or abandon type-safe generics entirely.