serde_with

repository·master·Indexed 21 days ago

https://github.com/jonasbb/serde_with

Custom de/serialization helpers for Rust's serde framework. It extends serde capabilities via the `#[serde_as]` attribute for complex type transformations, support for large or const-generic arrays, and field skipping. It includes utilities like `DisplayFromStr` for trait-based serialization, `#[skip_serializing_none]` to omit Option fields, and macros such as `#[derive(SerializeDisplay)]` and `#[derive(DeserializeFromStr)]`.

Tokens
25.3K
Snippets
88
Records
99
Agent score
74%

What's inside serde_with

  1. How `serde_as` works

    master

    The #[serde_as] attribute is an improved version of serde's with attribute. It allows you to mirror the type structure of a field while specifying custom converters for its inner types.

    Key rules:

    • The #[serde_as] attribute must be placed before the #[derive] attribute.
    • You can specify converters for inner types, e.g., Vec<DisplayFromStr>.
    • To restore default de/serialization behavior for a specific part of a type, use _ as a placeholder, e.g., BTreeMap<_, DisplayFromStr>.
  2. Use `serde_as` for flexible and composable serialization

    master

    The serde_as attribute provides a more flexible and composable alternative to Serde's standard with annotation. It allows you to mirror the type structure of a field and specify converters for inner types.

    Key features:

    • Inner Type Converters: You can specify converters for nested types, such as Vec<DisplayFromStr>.
    • Placeholder Support: Use _ as a placeholder to restore the default de/serialization behavior for specific parts of a type (e.g., BTreeMap<_, DisplayFromStr>).
    • Traits: The system is built on the SerializeAs and DeserializeAs traits.

    Note: While more flexible, serde_as works with fewer types than the standard with annotation, though it aims to support all Rust Standard Library types in all combinations.

    #[serde_as]
    #[derive(Deserialize, Serialize)]
    struct Data {
        // Use a specific converter for a field
        #[serde_as(as = "DisplayFromStr")]
        address: Ipv4Addr,
    
        // Use a complex converter with placeholders
        #[serde_as(as = "Map<DisplayFromStr, _>")]
        vec_as_map: Vec<(u32, String)>,
    }
  3. Handle Optional Fields with `#[serde_as]`

    master

    When using #[serde_as] on an Option<T> field, serde_with automatically applies #[serde(default)] so the field can be missing during deserialization.

    This automatic detection works for:

    • Option
    • std::option::Option
    • core::option::Option

    Warning: Renaming Option (e.g., use std::option::Option as StdOption;) will break this detection. If detection fails, you must manually add #[serde(default)] to the field.

    #[serde_as]
    #[derive(Serialize, Deserialize)]
    struct A {
        #[serde_as(as = "Option<DisplayFromStr>")]
        // Both `Option`s are correctly identified; `#[serde(default)]` is applied automatically.
        val: Option<u32>,
    }
  4. Use the `#[apply]` attribute to conditionally apply field attributes

    master

    The #[apply] attribute allows you to apply specific Serde attributes to multiple fields within a struct or enum based on their types. This is useful for applying common attributes (like #[serde(default)] or #[serde(skip_serializing_if = "... ")]) to all fields of a certain type without repeating the attribute on every single field.

    Syntax

    The attribute follows a pattern of Type => Attribute. You can provide a comma-separated list of these rules:

    #[apply(Type1 => Attribute1, Type2 => Attribute2)]

    Type Matching Rules

    • Generics: A type pattern without generics (e.g., Option) will match any instantiation of that type (e.g., Option<String>, Option<u8>).
    • Wildcards: You can use _ as a wildcard in generic arguments (e.g., BTreeMap<_, u8> matches BTreeMap<String, u8>).
    • Arrays: For array types [T; N], you can use _ for the length to match any length, or for the element type to match any element type.
    • References: Patterns are relaxed regarding lifetimes and mutability. For example, &str will match &'static str or &'a mut str.
    • Exclusion: You can skip applying attributes to a specific field by adding #[serde_with::skip_apply] to that field.

    Customizing the Crate Path

    By default, #[apply] assumes the attributes are part of the ::serde_with crate. If you are using a different path, you can specify it using the crate option:

    #[apply(crate = "my_custom_serde_path", Type => Attribute)]

    // Example usage (conceptual based on macro logic)
    #[derive(Serialize, Deserialize)]
    #[apply(
        Option<String> => #[serde(default)],
        u32 => #[serde(skip_serializing_if = "Option::is_none")]
    )]
    struct MyStruct {
        name: Option<String>, // Receives #[serde(default)]
        age: u32,             // Receives #[serde(skip_serializing_if = "Option::is_none")]
        id: String,          // Receives nothing
    }
  5. Use the `#[serde_as]` annotation for flexible de/serialization

    master

    The #[serde_as] attribute is a more flexible and composable alternative to serde's #[serde(with = "...")]. It allows you to specify transformations for complex, nested types by mirroring the type structure of the field.

    Key rules:

    • Place #[serde_as] before the #[derive(Serialize, Deserialize)] attribute.
    • Use #[serde_as(as = "...")] on fields to specify the transformation.
    • Use _ as a placeholder to restore default de/serialization behavior for inner types (e.g., BTreeMap<_, DisplayFromStr>).
    • Transformations are composed by mirroring the structure, such as Vec<DisplayFromStr> or Option<BTreeMap<_, Vec<DisplayFromStr>>>.
    use serde::{Deserialize, Serialize};
    use serde_with::{serde_as, DisplayFromStr};
    
    #[serde_as]
    #[derive(Serialize, Deserialize)]
    struct A {
        #[serde_as(as = "DisplayFromStr")]
        mime: mime::Mime,
    }
    
    // Example of composition with nested structures
    #[serde_as]
    #[derive(Serialize, Deserialize)]
    struct B {
        #[serde_as(as = "Option<BTreeMap<_, Vec<DisplayFromStr>>>")]
        mime: Option<BTreeMap<String, Vec<mime::Mime>>>,
    }
  6. Gate `#[serde_as]` using features and `cfg_eval`

    master

    To conditionally apply #[serde_as] based on Cargo features, use the cfg_eval attribute (from the cfg_eval crate or via unstable nightly).

    Crucial: The cfg_eval attribute must be placed before the #[serde_as] attribute. You can combine them into a single #[cfg_attr] as long as the order is preserved.

    #[cfg_attr(feature="serde", cfg_eval::cfg_eval, serde_as)]
    #[cfg_attr(feature="serde", derive(Serialize, Deserialize))]
    struct Struct {
        #[cfg_attr(feature="serde", serde_as(as = "Vec<(_, _)>"))]
        map: HashMap<(i32,i32), i32>,
    }
  7. Install `serde_with` in your project

    master

    To use serde_with, add it to your Cargo.toml using cargo add:

    cargo add serde_with

    The crate also provides various feature flags for integration with other common Rust crates. Check the feature flags documentation for available options.

  8. Re-exporting `serde_as` for procedural macros

    master

    When using the #[serde_as] attribute inside a procedural macro, you may need to specify the path to serde_with using the crate argument. This prevents users of your macro from having to manually add serde_with to their own Cargo.toml dependencies.

    To do this, use #[serde_with::serde_as(crate = "...")]. This is typically used in conjunction with serde's own crate attribute to ensure the macro expands to the correct paths relative to the consumer's environment.

    // Inside a procedural macro definition
    #[proc_macro]
    pub fn define_some_type(_item: TokenStream) -> TokenStream {
        let def = quote! {
            #[serde(crate = "::some_other_lib::serde")]
            #[::some_other_lib::serde_with::serde_as(crate = "::some_other_lib::serde_with")]
            #[derive(::some_other_lib::serde::Deserialize)]
            struct Data {
                #[serde_as(as = "_")]
                a: u32,
            }
        };
    
        TokenStream::from(def)
    }
  9. JSON Schema generation for time-related types (Strict vs Flexible)

    master

    When using schemars 0.8 with serde_with, time-related types (Durations and Timestamps) support two modes of schema generation:

    1. Strict Mode: Generates a schema that matches the specific format used during serialization.
    2. Flexible Mode: Generates a one_of schema that allows for multiple representations (e.g., a number or a string). This is useful for handling more lenient JSON inputs.

    Supported time types include:

    • DurationSeconds, DurationMilliSeconds, DurationMicroSeconds, DurationNanoSeconds (and their WithFrac variants)
    • TimestampSeconds, TimestampMilliSeconds, TimestampMicroSeconds, TimestampNanoSeconds (and their WithFrac variants)

    Flexible schemas for these types use specific schema IDs:

    • serde_with::FlexibleStringTimespan
    • serde_with::FlexibleF64Timespan
    • serde_with::FlexibleU64Timespan
    • serde_with::FlexibleI64Timespan
  10. How JsonSchemaAs works with schemars 0.8

    master

    The JsonSchemaAs trait is the schemars equivalent of serde_with's SerializeAs. It bridges the gap between serde_with's custom serialization logic and schemars's JSON schema generation.

    When you use the #[serde_as(as = "...")] attribute on a field, the serde_as macro uses the Schema type from serde_with to wrap the type. This Schema type implicitly implements JsonSchema by delegating the schema generation calls to your implementation of JsonSchemaAs.

  11. Configure Deserialization Strictness

    master

    When using duration or timestamp converters, you can control how strictly the deserializer matches the expected FORMAT using the STRICTNESS type parameter.

    • formats::Strict (default): Deserialization only supports the exact type specified in FORMAT. For example, if FORMAT is u64, attempting to deserialize from an f64 will result in an error.
    • formats::Flexible: Deserialization performs a best-effort attempt to extract the value from any type. For example, DurationSeconds<f64, Flexible> will successfully parse a String as an integer and will discard sub-second precision when reading from an f64.
  12. Convert a sequence into a key-value map using `KeyValueMap`

    master

    The KeyValueMap<T> helper allows you to serialize a sequence (like a Vec<T>) into a JSON/YAML map where each element of the sequence becomes a map entry.

    To use this, you must define which part of your element serves as the map key:

    1. For Structs: Use the #[serde(rename = "$key$")] attribute on the field you want to use as the key. The field named $key$ will be extracted as the map key, and all other fields will form the map value.
    2. For Tuples, Tuple Structs, or Sequences: The first element is automatically used as the map key.
    3. For Maps: The map-key that is named $key$ is used.

    You apply this using the #[serde_as(as = "KeyValueMap<_>")] attribute on the field containing the sequence within a struct annotated with #[serde_as].

    ```rust
    # #[cfg(feature = "macros")] {
    # use serde::{Deserialize, Serialize};
    # use serde_with::{serde_as, KeyValueMap};
    #
    # #[derive(Debug, Clone, PartialEq, Eq)]
    # #[derive(Serialize, Deserialize)]
    # struct SimpleStruct {
    #     b: bool,
    #     #[serde(rename = "$key$")]
    #     id: String,
    #     i: i32,
    # }
    #
    # #[serde_as]
    # # [derive(Debug, Clone, PartialEq, Eq)]
    # #[derive(Serialize, Deserialize)]
    # struct KVMap(#[serde_as(as = "KeyValueMap<_>")] Vec<SimpleStruct>);
    #
    # let values = KVMap(vec![
    #     SimpleStruct { b: false, id: "id-0000".to_string(), i: 123 },
    #     SimpleStruct { b: true, id: "id-0001".to_string(), i: 555 },
    # ]);
    #
    # // Serializes to: {"id-0000": {"b": false, "i": 123}, "id-0001": {"b": true, "i": 555}}
    # ```