Serde Deserialize custom implementation


#1

Hello,

I would like to implement Deserialize for my struct which represents JSON payload.

#[derive(Debug, Deserialize, PartialEq)]
pub struct Params {
    filter: Filter,
}

#[derive(Debug, PartialEq)]
pub struct Filter {
    namespace_id: Option<Uuid>,
    object_id: Option<String>,
}

And I would like JSON to have a format like

{
  "filter": "namespace_id:bab37008-3dc5-492c-af73-80c241241d71 AND object_id:foo"
}

As far as I understand for that I should implement Deserialize for Filter.
Unfortunately I don’t quite understand how to do it.

I have tried to make it, and it even passes the tests.
You can see the implementation on Playground.

Could you please tell me if I made it right or wrong?
I guess there is a much better way of doing this.
Also, since I have Option types I can define empty struct and assign the values later on match.
But what would I do if I wouldn’t have Option?

Also, I am interested in implementation of Serialize, just for myself, for better understanding.
Maybe you can help with it too?

Thank you.

P. S.

I have implemented Serialize too but it’s very hardcoded.
I guess there is a better way too.


#2

I would write this as:

impl Serialize for Filter {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut s = String::new();

        if let Some(ref namespace_id) = self.namespace_id {
            s.push_str("namespace_id:");
            s.push_str(&namespace_id.to_string());
        }

        if let Some(ref object_id) = self.object_id {
            if !s.is_empty() {
                s.push_str(" AND ");
            }
            s.push_str("object_id:");
            s.push_str(&object_id);
        }

        s.serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for Filter {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let mut filter = Filter {
            namespace_id: None,
            object_id: None,
        };

        let s = String::deserialize(deserializer)?;
        for part in s.split(" AND ") {
            let mut kv = part.splitn(2, ":");
            match (kv.next(), kv.next()) {
                (Some("namespace_id"), Some(v)) => {
                    let uuid = Uuid::parse_str(v).map_err(de::Error::custom)?;
                    filter.namespace_id = Some(uuid);
                }
                (Some("object_id"), Some(v)) => {
                    filter.object_id = Some(v.to_owned());
                }
                _ => { /* report error if you want */ }
            }
        }

        Ok(filter)
    }
}

#3

Thank you very much David!

Somehow I thought that I must implement Visitor.
Also thank you for the splitn, I actually wanted something like that.


#4

@dtolnay could you please help me to figure out one more question?

I would like this test to pass

assert_eq!(Params { filter: None }, serde_json::from_str(r#"{"filter":""}"#).unwrap());

For that I used deserialize_with and created the following function

#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Params {
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_empty_as_none")]
    filter: Option<Filter>,
}

fn deserialize_empty_as_none<'de, D>(d: D) -> Result<Option<Filter>, D::Error>
where
    D: Deserializer<'de>,
{
    let f = Filter::deserialize(d)?;
    let res = if let Filter { namespace_id: None, object_id: None } = f {
        None
    } else {
        Some(f)
    };

    Ok(res)
}

It works but I don’t like how it is done.
I would like the logic to be (if it is possible) like:

  • deserialize into string
  • if string is empty then return None
  • if string is not empty deserialize it into Filter and return Some(Filter)

The key point is that my current solution deserializes Filter anyway and only inspects the result after that. And I want to call Filter's deserialization only if string is not empty.

Since deserialize consumes Deserializer I can’t reuse it.
Current solution is placed on Playground.

In another project I have already do a similar thing but there was Option<String> and it was much easier

#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct QueryParameters {
    #[serde(default)]
    #[serde(deserialize_with = "deserialize_empty_as_none")]
    pub fq: Option<String>,
}

fn deserialize_empty_as_none<'de, D>(d: D) -> Result<Option<String>, D::Error>
where
    D: Deserializer<'de>,
{
    String::deserialize(d).map(|string| {
        if string.is_empty() {
            None
        } else {
            Some(string)
        }
    })
}

What is a right way for doing this?
Thank you.