When using 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.

Default Derive Bounds Behavior

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 works

By default, serde requires all field types to implement the derived trait.

Default Bounds on Generic Types

use serde::{Serialize, Deserialize};
 
#[derive(Serialize, Deserialize)]
struct Point<T> {
    x: T,
    y: T,
}
 
// Generated bound: Point<T>: Serialize requires T: Serialize
// Same for Deserialize

For generic types, default bounds include constraints on generic parameters.

Bounds Fail with Associated Types

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 bounds

Associated types require explicit bounds.

Using bound to Fix Associated Types

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>: Serialize

The bound attribute specifies what bounds to use for serialization.

Empty Bound with PhantomData

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 T

Use empty bounds when the type parameter isn't actually serialized.

Bounds for Serialization Only

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 requirements

bound(serialize = "...") sets bounds only for the Serialize impl.

Bounds for Deserialization Only

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.

Different Bounds for Serialize and Deserialize

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 bounds

Use separate bounds when serialization and deserialization have different requirements.

Default Bounds Can Be Too Restrictive

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 this

Sometimes default bounds are stricter than necessary.

Overriding Bounds on Container

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 T

Set empty bounds at container level for types that don't serialize their generic parameters.

Multiple Bounds with bound

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 attribute

Combine multiple bounds with + syntax.

Bounds with Where Clause 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 supported

Use where-clause syntax for complex bounds.

Lifetime Bounds in bound Attribute

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 'a

Lifetime bounds are sometimes needed for borrowed data.

Deserialization with Explicit Deserialize<'de>

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.

Comparing Default vs Custom Bounds

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 serialization

Default bounds require all fields to implement the trait; custom bounds give you control.

Practical Example: Generic Wrapper

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 here

When using custom serialization, bounds must match the actual requirements.

Bounds on Fields vs Container

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 impl

Field-level and container-level bounds can both be used.

Skip Serializing with Bound

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 serializable

Use empty bounds with skip to avoid unnecessary constraints.

Default Bounds with Complex Generics

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 serialized

When type parameters have non-Serialize bounds, use custom bounds.

When Default Bounds Work Well

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 perfectly

For simple types with standard fields, default bounds are ideal.

When Default Bounds Fail

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.

Debugging Generated Bounds

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 errors

cargo expand shows the generated impl blocks with their bounds.

Common Patterns Summary

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.

Synthesis

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:

  • Generic parameters aren't actually serialized (PhantomData, skipped fields)
  • Field types have associated types or complex bounds
  • Custom serialization requires different bounds than the field type's impl

Custom bounds: #[serde(bound = "...")] lets you specify exactly what the impl requires. Use it when:

  • The type contains PhantomData<T> and T doesn't need to implement Serialize
  • Fields are skipped and don't contribute to bounds
  • Custom serialization/deserialization requires different bounds
  • Default bounds are stricter than what's actually needed

Key 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.