Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation Request: how to implement SerializeAs/DeserializeAs for generic structs #644

Open
coriolinus opened this issue Sep 15, 2023 · 1 comment

Comments

@coriolinus
Copy link

Consider a generic metadata struct:

/// Whether this value has been manually approved
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(bound(deserialize = "T: Deserialize<'de>"))]
struct Metadata<T> {
    /// Current, validated value of this field
    ///
    /// `None` if validation failed
    pub value: Option<T>,
    /// Value before changes were made during approval
    ///
    /// `None` if no changes were made
    #[serde(skip_serializing_if = "Option::is_none")]
    pub original_value: Option<T>,
    /// Whether this field has been manually vetted
    has_been_vetted: bool,
}

For basic use cases, this is fine. However, we need to implement SerializeAs and DeserializeAs in order to support modifications to the interior. The desired use case looks like

#[serde_as]
#[derive(Debug, Clone, Deserialize, Serialize)]
struct User {
    username: Metadata<String>,
    // `date_as_string::Ymd` is working fine, so we don't demo the implementation here
    #[serde_as(as = "Option<Metadata<date_as_string::Ymd>>")]
    birthday: Option<Metadata<time::Date>>,
}

For that use case, we need implementations for SerializeAs and DeserializeAs:

impl<T, U> SerializeAs<Metadata<T>> for Metadata<U>
where
    U: SerializeAs<T>,
{
    fn serialize_as<S>(source: &Metadata<T>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        todo!()
    }
}

impl<'de, T, U> DeserializeAs<'de, Metadata<T>> for Metadata<U>
where
    U: DeserializeAs<'de, T>,
{
    fn deserialize_as<D>(deserializer: D) -> Result<Metadata<T>, D::Error>
    where
        D: Deserializer<'de>,
    {
        todo!()
    }
}

Unfortunately, the documentation is very unclear as to how precisely to accomplish this, and I'm finding myself confused about how to proceed. At this point, it seems as though the best process might actually be to cargo expand the Serialize and Deserialize implementations, and then adjust wherever generic values are present in order to wrap them with SerializeAsWrap or DeserializeAsWrap wherever a generic parameter exists, essentially reimplementing precisely the same logic that serde already does for field naming, optional fields, etc. But that is extremely cumbersome, so hopefully there is a better way!

@panicbit
Copy link

panicbit commented Sep 25, 2023

To implement DeserializeAs and SerializeAs in this situation, one can simply use the derived Deserialize and Serialize impls by using Metadata<DeserializeAsWrap<T>> / Metadata<SerializeAsWrap<T>> and repacking the types as needed:

impl<T, U> SerializeAs<Metadata<T>> for Metadata<U>
where
    U: SerializeAs<T>,
{
    fn serialize_as<S>(source: &Metadata<T>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let source = Metadata::<SerializeAsWrap<T, U>> {
            value: source.value.as_ref().map(SerializeAsWrap::new),
            original_value: source.original_value.as_ref().map(SerializeAsWrap::new),
            has_been_vetted: source.has_been_vetted,
        };

        source.serialize(serializer)
    }
}

impl<'de, T, U> DeserializeAs<'de, Metadata<T>> for Metadata<U>
where
    U: DeserializeAs<'de, T>,
{
    fn deserialize_as<D>(deserializer: D) -> Result<Metadata<T>, D::Error>
    where
        D: Deserializer<'de>,
    {
        let this = Metadata::<DeserializeAsWrap<T, U>>::deserialize(deserializer)?;

        Ok(Metadata {
            value: this.value.map(DeserializeAsWrap::into_inner),
            original_value: this.original_value.map(DeserializeAsWrap::into_inner),
            has_been_vetted: this.has_been_vetted,
        })
    }
}

The SerializeAs implementation above may require copying or cloning.
If cloning is not possible or undesireable, then the type definition probably has to be duplicated and tweaked to only contain references.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants