Nice 'minimal code' way to serde::Deserialize with custom needs?

Hello, I have this struct

pub struct Foo {
    a: usize,
    b: usize,
}

I have one special deserialize requirement, that b may be optional, and if not present it should be 8, but then finally a must never be less than b. (Error if so)

Just expanding the derive deserialize macro for this simple struct produces a wall of text, which is many lines of boilerplate I'd like to avoid, so I was wondering if anyone knows of a nice way I could inject my requirements in a minimal-code kind of way, something like deserialize_with but obviously that doesn't have to power to look at adjacent fields, unfortunately.

Thank you in advance!

First off, the expanded #[derive(Deserialize)] impl (or any derived impl, in fact) is not even remotely similar to what you would write by hand. Generated code has to cover a lot of general cases and it must be bullet-proof to call from anywhere, basically, which requires a lot of careful code wrangling. If you were to manually implement Deserialize, it would be much nicer.

When implementing the deserializer manually, you can just create a proxy type, derive a deserializer for it, and validate it after the fact, like this Playground:

#[derive(Debug, Error)]
#[error("a = {a} is less than b = {b}")]
pub struct ALessThanB {
    a: usize,
    b: usize,
}

#[derive(Deserialize)]
struct FooProxy {
    a: usize,
    #[serde(default = "eight")]
    b: usize,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize)]
pub struct Foo {
    a: usize,
    #[serde(default = "eight")]
    b: usize,
}

impl<'de> Deserialize<'de> for Foo {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::de::Deserializer<'de>,
    {
        let proxy = FooProxy::deserialize(deserializer)?;
        
        if proxy.a >= proxy.b {
            Ok(Foo { a: proxy.a, b: proxy.b })
        } else {
            Err(serde::de::Error::custom(ALessThanB { a: proxy.a, b: proxy.b }))
        }
    }
}

fn eight() -> usize { 8 }

Edit: Misread the question first; the section below is not relevant any more.

But then the key thing to observe is that this is not really a requirement on your struct Foo. Your requirement is actually an invariant of the type of whatever its b field is. Thus, a newtype is appropriate. Just implement that one requirement for the type of the field b, and continue deriving your impls for Foo.

Playground.

1 Like

You can let serde do the heavy lifting how to deserialize, by using a type which matches your data model (i.e., b: Option<usize>), and do any further checks in a TryFrom.

#[derive(Debug, serde::Deserialize)]
#[serde(try_from = "FooDe")]
pub struct Foo {
    a: usize,
    b: usize,
}

#[derive(serde::Deserialize)]
pub struct FooDe {
    a: usize,
    b: Option<usize>,
}

impl TryFrom<FooDe> for Foo {
    type Error = &'static str;

    fn try_from(other: FooDe) -> Result<Self, Self::Error> {
        let a = other.a;
        let b = other.b.unwrap_or(8);
        if a < b {
            Err("a must never be less than b")
        } else {
            Ok(Foo {
                a,
                b
            })
        }
    }
}

fn main() {
    assert!(serde_json::from_str::<Foo>(r#"{"a": 1}"#).is_err());
    assert!(serde_json::from_str::<Foo>(r#"{"a": 1, "b": 0}"#).is_ok());
    dbg!(serde_json::from_str::<Foo>(r#"{"a": 16}"#).unwrap());
}
2 Likes

Fantastic information in this thread, both to @H2CO3 and @jonasbb.

I wonder if I can title this thread better because this is sure to be a forum thread that many are going to want see!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.