#serialize-deserialize #derive-deserialize #serde #adapter

no-std serdapt

Composable adapters for #[serde(with = ...)] attribute

1 unstable release

0.1.0 Aug 28, 2024

#429 in Encoding


Used in serdapt-base64

0BSD license

110KB
2.5K SLoC

Overview

Tools to build composable adapters for #[serde(with = ...)].

serde allows customizing how fields are serialized when deriving Serialize and Deserialize thanks to the #[serde(with = "path")] attribute. With such an attribute, path::serialize and path::deserialize are the functions used for serialization. By using a type for path, composable serialization adapters can be defined, e.g. to customize how items in a container are serialized.

These adapters can also simplify implementing Serialize and Deserialize.

Apply adapter

An adapter is applied by specifying the adapter path in #[serde(with = "...")]. The path needs to be suitable as a prefix for functions, i.e. path::serialize and path::deserialize. This means the turbofish is needed for generic adapters, e.g. Outer::<Inner>.

Example

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct Foo {
    #[serde(with = "serdapt::Seq::<serdapt::Str>")]
    xs: Vec<i32>,
}

let foo = Foo { xs: vec![3, 4] };
let v = serde_json::to_value(&foo).unwrap();
assert_eq!(v, serde_json::json!({ "xs": ["3", "4"] }));
assert_eq!(serde_json::from_value::<Foo>(v).unwrap(), foo);

Define serialization adapter

  1. Define a type to represent the new adapter.
  2. Implement SerializeWith and DeserializeWith for this type. This allows adapter composability.
  3. Define serialize and deserialize inherent functions for this type, delegating to SerializeWith and DeserializeWith respectively. These are the functions the serde-generated code calls.

Simple adapter example

use serdapt::{DeserializeWith, SerializeWith};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::json;

#[derive(Deserialize, Serialize)]
struct Point {
    x: i32,
    y: i32,
}

struct Coords;

impl Coords {
    fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
    where
        T: ?Sized,
        S: Serializer,
        Self: SerializeWith<T>,
    {
        Self::serialize_with(value, serializer)
    }

    fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
    where
        D: Deserializer<'de>,
        Self: DeserializeWith<'de, T>,
    {
        Self::deserialize_with(deserializer)
    }
}

impl SerializeWith<(i32, i32)> for Coords {
    fn serialize_with<S>(&(x, y): &(i32, i32), serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        Serialize::serialize(&Point { x, y }, serializer)
    }
}

impl<'de> DeserializeWith<'de, (i32, i32)> for Coords {
    fn deserialize_with<D>(deserializer: D) -> Result<(i32, i32), D::Error>
    where
        D: Deserializer<'de>,
    {
        let Point { x, y } = Deserialize::deserialize(deserializer)?;
        Ok((x, y))
    }
}

#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct Shape(#[serde(with = "serdapt::Seq::<Coords>")] Vec<(i32, i32)>);

let original = Shape(vec![(1, 2), (3, 4)]);
let serialized = serde_json::to_value(&original).unwrap();
assert_eq!(serialized, json!([{ "x": 1, "y": 2 }, { "x": 3, "y": 4 }]));
let deserialized = serde_json::from_value::<Shape>(serialized).unwrap();
assert_eq!(deserialized, original);

Generic adapter example

use core::marker::PhantomData;
use serdapt::{DeserializeWith, SerializeWith, WithEncoding};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::json;

#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct Point<T> {
    x: T,
    y: T,
}

struct Coords<F>(PhantomData<F>);

impl<F> Coords<F> {
    fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
    where
        T: ?Sized,
        S: Serializer,
        Self: SerializeWith<T>,
    {
        Self::serialize_with(value, serializer)
    }

    fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
    where
        D: Deserializer<'de>,
        Self: DeserializeWith<'de, T>,
    {
        Self::deserialize_with(deserializer)
    }
}

impl<F, T> SerializeWith<Point<T>> for Coords<F>
where
    F: SerializeWith<T>,
{
    fn serialize_with<S>(Point { x, y }: &Point<T>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let p: Point<WithEncoding<&F, &T>> = Point {
            x: x.into(),
            y: y.into()
        };
        Serialize::serialize(&p, serializer)
    }
}

impl<'de, F, T> DeserializeWith<'de, Point<T>> for Coords<F>
where
    F: DeserializeWith<'de, T>,
{
    fn deserialize_with<D>(deserializer: D) -> Result<Point<T>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let p: Point<WithEncoding<F, T>> = Deserialize::deserialize(deserializer)?;
        Ok(Point {
            x: p.x.into_inner(),
            y: p.y.into_inner(),
        })
    }
}

#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct Shape(
    #[serde(with = "serdapt::Seq::<Coords<serdapt::Str>>")] Vec<Point<i32>>,
);

let original = Shape(vec![Point { x: 1, y: 2 }, Point { x: 3, y: 4 }]);
let serialized = serde_json::to_value(&original).unwrap();
assert_eq!(serialized, json!([{ "x": "1", "y": "2" }, { "x": "3", "y": "4" }]));
let deserialized = serde_json::from_value::<Shape>(serialized).unwrap();
assert_eq!(deserialized, original);

Related project

serde_with allows the same composability with the help of an additional proc-macro, though it is also possible to use #[serde(with = ...)] directly.

Some key differences are:

  • serdapt is simpler and does not need any additional proc-macro, giving up on any ergonomics such a macro provides.
  • It avoids a macro ordering issue that can lead to generated serialization code not using the requested adapter despite a sucessful compilation.
  • It works seamlessly with conditional compilation.
  • It is limited to supporting types in the standard library, with support for third-party types delegated to other crates, which solves dependency issues.

Contribute

All contributions shall be licensed under the 0BSD license.

Dependencies

~100–330KB