Loading page…
Rust walkthroughs
Loading page…
serde, what is the difference between #[serde(bound = "...")] and the default derive bounds?When deriving Serialize and Deserialize, serde automatically generates trait bounds based on the types of a struct's fields. #[serde(bound = "...")] replaces these automatically inferred bounds with custom bounds you specify. The default derive bounds require that every field type implements the trait being derived, but this is sometimes too restrictive—for types with generic parameters, associated types, or complex relationships, the automatically generated bounds may not compile or may be overly restrictive. The bound attribute lets you specify exactly what bounds are needed, which is essential for advanced generic programming patterns where the compiler cannot infer the correct bounds automatically.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct User {
name: String,
age: u32,
}
// Generated bound: User: Serialize requires String: Serialize + u32: Serialize
// Both String and u32 implement Serialize, so this worksBy default, serde requires all field types to implement the derived trait.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Point<T> {
x: T,
y: T,
}
// Generated bound: Point<T>: Serialize requires T: Serialize
// Same for DeserializeFor generic types, default bounds include constraints on generic parameters.
use serde::{Serialize, Deserialize};
// This won't compile with default bounds:
// #[derive(Serialize)]
// struct Container<T: Iterator> {
// items: Vec<T::Item>,
// }
//
// Error: T::Item doesn't implement Serialize by default
// serde generates: impl<T> Serialize for Container<T> where T::Item: Serialize
// But T::Item is not known to implement Serialize
// The problem is that associated types don't automatically inherit boundsAssociated types require explicit bounds.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Container<T>
where
T: Iterator,
{
#[serde(bound = "")]
items: Vec<T::Item>,
}
// Wait, this still won't work. Let me show the correct approach:
#[derive(Serialize, Deserialize)]
struct Container<T>
where
T: Iterator,
T::Item: Serialize, // Add bound at type level
{
items: Vec<T::Item>,
}
// Or use bound attribute:
#[derive(Serialize, Deserialize)]
struct Container<T> {
#[serde(bound = "T::Item: Serialize")]
items: Vec<T::Item>,
}
// This tells serde to use T::Item: Serialize instead of Vec<T::Item>: SerializeThe bound attribute specifies what bounds to use for serialization.
use serde::{Serialize, Deserialize};
use std::marker::PhantomData;
#[derive(Serialize, Deserialize)]
struct Wrapper<T> {
#[serde(skip)]
_phantom: PhantomData<T>,
}
// Default bounds: T: Serialize + T: Deserialize<'_>
// But PhantomData doesn't actually contain a T, so we don't need that bound
#[derive(Serialize, Deserialize)]
struct Wrapper<T> {
#[serde(skip)]
#[serde(bound = "")] // No bounds needed for PhantomData
_phantom: PhantomData<T>,
}
// Now Wrapper<T> implements Serialize/Deserialize for any TUse empty bounds when the type parameter isn't actually serialized.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(bound(serialize = "T: Serialize"))]
struct Item<T> {
value: T,
}
// For Serialize: T: Serialize
// For Deserialize: use default bounds (T: Deserialize<'_>)
// This is useful when Serialize and Deserialize have different requirementsbound(serialize = "...") sets bounds only for the Serialize impl.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(bound(deserialize = "T: Deserialize<'de>"))]
struct Data<T> {
values: Vec<T>,
}
// For Deserialize: T: Deserialize<'de>
// For Serialize: use default bounds (T: Serialize)bound(deserialize = "...") sets bounds only for the Deserialize impl.
use serde::{Serialize, Deserialize};
use std::fmt::Display;
#[derive(Serialize, Deserialize)]
#[serde(
bound(serialize = "T: Serialize + Display"),
bound(deserialize = "T: Deserialize<'de> + Default")
)]
struct Complex<T> {
value: T,
}
impl<T: Display> Serialize for Complex<T>
where
T: Serialize,
{
// Serialize needs Display for custom serialization
}
// The impl would be generated with the specified boundsUse separate bounds when serialization and deserialization have different requirements.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Inner<T> {
value: T,
}
#[derive(Serialize, Deserialize)]
struct Outer<T> {
inner: Inner<T>,
}
// Default bounds for Outer<T>: T: Serialize
// This works because Inner<T>: Serialize requires T: Serialize
// But what if Inner doesn't require T: Serialize?
#[derive(Serialize, Deserialize)]
#[serde(bound = "")] // Inner doesn't actually need bounds on T
struct SkipInner<T> {
#[serde(skip)]
value: T,
}
#[derive(Serialize, Deserialize)]
struct Outer2<T> {
inner: SkipInner<T>,
}
// Default bounds: T: Serialize (inherited from Outer2's inner field)
// But SkipInner<T> doesn't need T: Serialize!
// We need to override thisSometimes default bounds are stricter than necessary.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct NoBounds<T> {
#[serde(skip)]
_marker: PhantomData<T>,
}
// This struct can be serialized/deserialized for any T
// No bounds required because no fields are serialized
let no_bounds: NoBounds<String> = NoBounds { _marker: PhantomData };
// This serializes to {} regardless of TSet empty bounds at container level for types that don't serialize their generic parameters.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(bound = "T: Serialize + Clone, U: Serialize")]
struct Multi<T, U> {
first: T,
second: U,
}
// Specifies multiple bounds in one attributeCombine multiple bounds with + syntax.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(bound = "T: Serialize, U: Serialize + Clone")]
struct Pair<T, U> {
key: T,
value: U,
}
// Full where-clause syntax is supportedUse where-clause syntax for complex bounds.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(bound(deserialize = "'de: 'a"))]
struct Borrowed<'a, T> {
reference: &'a T,
}
// Lifetime bounds can be specified
// 'de: 'a means the deserialization lifetime outlives 'aLifetime bounds are sometimes needed for borrowed data.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(bound(deserialize = "T: Deserialize<'static>"))]
struct StaticData<T> {
value: T,
}
// Restrict to types that deserialize from 'static data
// More restrictive than the default T: Deserialize<'de>You can tighten deserialization bounds with 'static.
use serde::{Serialize, Deserialize};
// Default bounds: every field type must implement the trait
#[derive(Serialize, Deserialize)]
struct DefaultBounds<T> {
value: T,
}
// Generated: impl<T: Serialize> Serialize for DefaultBounds<T>
// Generated: impl<'de, T: Deserialize<'de>> Deserialize<'de> for DefaultBounds<T>
// Custom bounds: override what's required
#[derive(Serialize, Deserialize)]
#[serde(bound(serialize = ""))]
struct CustomBounds<T> {
#[serde(skip)]
value: T,
}
// Generated: impl<T> Serialize for CustomBounds<T>
// No bounds on T for serializationDefault bounds require all fields to implement the trait; custom bounds give you control.
use serde::{Serialize, Deserialize};
use std::fmt::Display;
#[derive(Serialize, Deserialize)]
#[serde(bound(serialize = "T: Display"))]
struct DisplayWrapper<T> {
#[serde(serialize_with = "serialize_display")]
value: T,
}
fn serialize_display<T: Display, S: serde::Serializer>(
value: &T,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&value.to_string())
}
// Serialize requires T: Display, not T: Serialize
// Default bound would require T: Serialize which is wrong hereWhen using custom serialization, bounds must match the actual requirements.
use serde::{Serialize, Deserialize};
// Bounds on individual fields
#[derive(Serialize, Deserialize)]
struct FieldBounds<T, U> {
#[serde(bound = "T: Serialize")]
first: T,
#[serde(bound = "U: Clone + Serialize")]
second: U,
}
// Bounds on the container (affects the whole impl)
#[derive(Serialize, Deserialize)]
#[serde(bound = "T: Serialize")]
struct ContainerBounds<T, U> {
first: T,
second: U,
}
// Container bound applies to all fields
// Field-level bounds are combined for the implField-level and container-level bounds can both be used.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Cache<T> {
#[serde(skip)]
#[serde(bound = "")] // No bounds needed
cached_value: Option<T>,
#[serde(bound = "String: Serialize")] // Always true, but explicit
key: String,
}
// cached_value is skipped, so no bounds on T needed
// key is always serializableUse empty bounds with skip to avoid unnecessary constraints.
use serde::{Serialize, Deserialize};
// Problematic default bounds:
#[derive(Serialize, Deserialize)]
struct ComplexGeneric<T>
where
T: Iterator,
{
count: usize,
#[serde(skip)]
iterator: Option<T>,
}
// Default bound: T: Serialize
// But T: Iterator might not implement Serialize!
// We need custom bounds:
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct ComplexGenericFixed<T>
where
T: Iterator,
{
count: usize,
#[serde(skip)]
iterator: Option<T>,
}
// Now no bounds on T for Serialize/Deserialize
// Only the count is serializedWhen type parameters have non-Serialize bounds, use custom bounds.
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct SimpleStruct {
name: String,
count: u32,
values: Vec<i32>,
}
// Default bounds: String: Serialize, u32: Serialize, Vec<i32>: Serialize
// All these are true, so default bounds work perfectlyFor simple types with standard fields, default bounds are ideal.
use serde::{Serialize, Deserialize};
// Case 1: PhantomData
#[derive(Serialize, Deserialize)]
struct PhantomExample<T> {
#[serde(skip)]
_marker: PhantomData<T>,
}
// Error: T doesn't implement Serialize/Deserialize
// Fix: #[serde(bound = "")]
// Case 2: Associated types
// (shown earlier)
// Case 3: Custom serialization
#[derive(Serialize, Deserialize)]
#[serde(bound(serialize = "T: AsRef<str>"))]
struct AsRefWrapper<T: AsRef<str>> {
#[serde(serialize_with = "serialize_as_str")]
value: T,
}
// Default would require T: Serialize, but we only need T: AsRef<str>Default bounds fail when the field type relationship doesn't match the serialized form.
use serde::{Serialize, Deserialize};
// Use cargo expand to see generated code:
// cargo expand --lib
// The generated impl will show:
// impl<T: Serialize> Serialize for MyStruct<T> { ... }
// or with bound attribute:
// impl<T> Serialize for MyStruct<T> { ... }
// Understanding what bounds are generated helps diagnose compilation errorscargo expand shows the generated impl blocks with their bounds.
use serde::{Serialize, Deserialize};
use std::marker::PhantomData;
// Pattern 1: PhantomData - no bounds needed
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
struct Phantom<T> {
#[serde(skip)]
_marker: PhantomData<T>,
}
// Pattern 2: Some fields need bounds, others don't
#[derive(Serialize, Deserialize)]
struct Mixed<T, U> {
#[serde(bound = "T: Serialize")]
value: T,
#[serde(skip)]
#[serde(bound = "")]
_marker: PhantomData<U>,
}
// Pattern 3: Different serialize/deserialize bounds
#[derive(Serialize, Deserialize)]
#[serde(
bound(serialize = "T: Display"),
bound(deserialize = "T: FromStr")
)]
struct StringSerialize<T> {
#[serde(serialize_with = "to_string", deserialize_with = "from_string")]
value: T,
}These patterns cover most use cases for custom bounds.
The #[serde(bound = "...")] attribute exists because the default derive bounds cannot always express what your type actually requires:
Default bounds: Serde looks at each field and requires that field's type to implement the trait. For struct Foo<T> { x: T }, the generated impl is impl<T: Serialize> Serialize for Foo<T>. This works for most cases but fails when:
Custom bounds: #[serde(bound = "...")] lets you specify exactly what the impl requires. Use it when:
PhantomData<T> and T doesn't need to implement SerializeKey insight: Default bounds are a reasonable approximation—require every field type to implement the trait—but they're a conservative approximation. Custom bounds let you express the minimal requirements, enabling generic types that couldn't compile with default bounds, or relaxing constraints when full bounds aren't necessary. The bound(serialize = "...") and bound(deserialize = "...") forms give you separate control when serialization and deserialization have different requirements.