Testing serialize/deserialize functions for serde "with" attribute
Asked Answered
M

1

6

Serde derive macros come with the ability to control how a field is serialized/deserialized through the #[serde(with = "module")] field attribute. The "module" should have serialize and deserialize functions with the right arguments and return types.

An example that unfortunately got a bit too contrived:

use serde::{Deserialize, Serialize};

#[derive(Debug, Default, PartialEq, Eq)]
pub struct StringPair(String, String);

mod stringpair_serde {
    pub fn serialize<S>(sp: &super::StringPair, ser: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        ser.serialize_str(format!("{}:{}", sp.0, sp.1).as_str())
    }

    pub fn deserialize<'de, D>(d: D) -> Result<super::StringPair, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        d.deserialize_str(Visitor)
    }

    struct Visitor;

    impl<'de> serde::de::Visitor<'de> for Visitor {
        type Value = super::StringPair;

        fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
            write!(f, "a pair of strings separated by colon (:)")
        }

        fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
        where
            E: serde::de::Error,
        {
            Ok(s.split_once(":")
                .map(|tup| super::StringPair(tup.0.to_string(), tup.1.to_string()))
                .unwrap_or(Default::default()))
        }
    }
}

#[derive(Serialize, Deserialize)]
struct UsesStringPair {
    // Other fields ...

    #[serde(with = "stringpair_serde")]
    pub stringpair: StringPair,
}

fn main() {
    let usp = UsesStringPair {
        stringpair: StringPair("foo".to_string(), "bar".to_string()),
    };

    assert_eq!(
        serde_json::json!(&usp).to_string(),
        r#"{"stringpair":"foo:bar"}"#
    );

    let usp: UsesStringPair = serde_json::from_str(r#"{"stringpair":"baz:qux"}"#).unwrap();

    assert_eq!(
        usp.stringpair,
        StringPair("baz".to_string(), "qux".to_string())
    )
}

Testing derived serialization for UsesStringPair is trivial with simple assertions. But I have looked at serde_test example as that makes sense to me too.

However, I want to be able to independently test the stringpair_serde::{serialize, deserialize} functions (e.g. if my crate provides just mycrate::StringPair and mycrate::stringpair_serde, and UsesStringPair is for the crate users to implement).

One way I've looked into is creating a serde_json::Serializer (using new, requires a io::Write implementation, which I couldn't figure out how to create and use trivially, but that's a separate question) and calling serialize with the created Serializer, then making assertions on the result as before. However, that does not test any/all implementations of serde::Serializer, just the one provided in serde_json.

I'm wondering if there's a method like in the serde_test example that works for ser/deser functions provided by a module.

Muldoon answered 10/8, 2021 at 1:1 Comment(4)
Why not derive the two traits directly for StringPair itself?Pernambuco
For your other (implied) question, &mut Vec<u8> implements io::Write.Scharaga
@RuifengXie Several reasons: there could be different ways of to serialize the same time. An example I can think of is chrono::DateTime<Tz>. It can be serialized as a string, as a Unix timestamp, as an integer timestamp but with higher resolution (e.g. milliseconds etc.) So the type does itself implement the Serialize, Deserialize traits, but chrono also provides submodules that can be used with with = ... to serialize DateTime value in different ways.Muldoon
It could also be that as the crate does not provide the actual type, just a serialization/deserialization format for a type defined elsewhere (can't implement traits or derive on types defined elsewhere). Another, related but different scenario: module writer provides the type, e.g. StringPair and it does implement the ser/de traits, but the module writer also wishes to provide ser/de for monadic containers of that type e.g. Option<StringPair> gets serialized differently from StringPair. Lots of reasons for needing a stand-alone ser/de module instead of type implementing traits.Muldoon
M
1

There is no method within serde_test that can test these functions directly. While serde_test uses its own Serializer and Deserializer internally, it does not expose these types, so you can't use them directly in your testing.

However, you can accomplish this with the serde_assert crate (disclaimer: I wrote serde_assert). serde_assert directly exposes its Serializer and Deserializer specifically for testing, and works well for your use case:

use serde_assert::{Deserializer, Serializer, Token, Tokens};

#[test]
fn serialize() {
    let serializer = Serializer::builder().build();

    assert_eq!(
        stringpair_serde::serialize(
            &StringPair("foo".to_string(), "bar".to_string()),
            &serializer
        ),
        Ok(Tokens(vec![Token::Str("foo:bar".to_string())])),
    );
}

#[test]
fn deserialize() {
    let mut deserializer = Deserializer::builder()
        .tokens(Tokens(vec![Token::Str("foo:bar".to_string())]))
        .build();

    assert_eq!(
        stringpair_serde::deserialize(&mut deserializer),
        Ok(StringPair("foo".to_string(), "bar".to_string()))
    );
}
Mistress answered 6/4, 2023 at 20:55 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.