Search code examples
jsonrustdeserializationserde

Rust Serde deserialization: how can I make sure a field does NOT exist and throw an error if it does exist?


I want to make sure certain fields are NOT specified when deserializing. I want to throw an error during deserialization if the field is present. Note that I am not asking about the field being None. I am asking about making sure the field itself isn't specified.

I asked ChatGPT and it gave me the following:

#[derive(Deserialize)]
struct MyStruct {
    #[serde(default, deserialize_with = "check_field")]
    required_field: Option<String>,
    other_field: String,
}

fn check_field<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
    D: Deserializer<'de>,
{
    if deserializer.is_human_readable() {
        // If deserialization is happening from a human-readable format, throw an error
        Err(serde::de::Error::custom("Required field should not be present"))
    } else {
        // If deserialization is happening from a non-human-readable format, proceed normally
        Ok(None)
    }
}

While this does seem to work, I am not sure if deserializer.is_human_readable() is the right way? I read the documents and it says the following:

"Some types have a human-readable form that may be somewhat expensive to construct, as well as a binary form that is compact and efficient. Generally text-based formats like JSON and YAML will prefer to use the human-readable one and binary formats like Postcard will prefer the compact one."

So this doesn't seem right as if the field is in binary form, then it would allow it to exist.

Is there a better/correct way?


Solution

  • I believe you can just unconditionally error. The function will only run if the field name has been found, at which point the data is definitely invalid.

    #[derive(Deserialize, Debug)]
    pub struct MyStruct {
        #[serde(default, deserialize_with = "deny_field", rename = "required_field")]
        _required_field: (),
        pub other_field: String,
    }
    
    fn deny_field<'de, D>(_deserializer: D) -> Result<(), D::Error>
    where
        D: Deserializer<'de>,
    {
        Err(serde::de::Error::custom(
            "Required field should not be present",
        ))
    }
    

    I've also cleaned up some dead code warnings. You can either start the field name with an underscore and rename it, or annotate it with #[allow(dead_code)]. The underscore method has the advantage of being visibly unused if encountered elsewhere, so I've done that.

    It seems to work with all the relevant cases.

    use serde_json::{from_value, json};
    
    fn main() {
        let required_int = json! ({
            "required_field": 30,
            "other_field": "other data",
        });
        let err = from_value::<MyStruct>(required_int).unwrap_err();
        dbg!(err);
    
        let required_null = json! ({
            "other_field": "other data",
            "required_field": null,
        });
        let err = from_value::<MyStruct>(required_null).unwrap_err();
        dbg!(err);
    
        let required_string = json! ({
            "required_field": "some_data",
            "other_field": "other data",
        });
        let err = from_value::<MyStruct>(required_string).unwrap_err();
        dbg!(err);
    
        let valid = json! ({
            "other_field": "other data",
        });
        let my_struct: MyStruct = from_value(valid).unwrap();
        dbg!(my_struct);
    
        let valid_with_extra = json! ({
            "another_field": "another data",
            "other_field": "other data",
        });
        let my_struct: MyStruct = from_value(valid_with_extra).unwrap();
        dbg!(my_struct);
    }
    

    This will only work for self-describing formats like JSON, but other formats usually have no concept of "field names", so this is probably fine.

    Also remember to add #[serde(skip_serializing)] to the field if you decide to derive Serialize.