Validate fields and types in serde with TryFrom
Tags: rust, serde, programming
Published: 2021-10-10 16:28:31

[serde](https://github.com/serde-rs/serde) might be the most popular serializing / deserializing framework in rust but it doesn't support validation out of box.

However, with the `TryFrom` trait and [`#[serde(try_from = "FromType")]`](https://serde.rs/container-attrs.html#try_from), we can easily validate types and fields when deserializing.

## Validate scalar values

Imagine we are developing a user system, where users' email should be validated before constructed. In rust, we can define a single-element tuple struct which contains a `String` to represent Email. 

```rust
pub struct Email(String);
```

`Email` should only be constructed after validation so we can impl `try_new` as the only way to construct `Email`.

```rust
impl Email {
    // Here we use a String to represent error just for simplicity
    // You can define a custom enum type like EmailParseError in your application
    pub fn try_new(email: String) -> Result<Self, String> {
        if validate_email(&email) {
            Ok(Self(email))
        } else {
            Err(format!("Invalid email {}", email))
        }
    }
}
```

And some methods to consume or reference the inner `String`

```rust
impl Email {
    pub fn into_inner(self) -> String { self.0 }
    pub fn inner(&self) -> &String { &self.0 }
}
```

With the above code, if we have a `Email`, we would know the inner string is already validated; If we have a `String`, we must call `Email::try_new` to validate it.

Now we have designed a `Email` struct and we want to deserialize it from a string with serde. Then we might code like:

```rust
use serde::Deserialize;

#[derive(Deserialize)]
pub struct Email(String);

let email: Email = serde_json::from_str("\"some_json_string\"").unwrap();
// Email("some_json_string".to_string())
```

Now `Email` can be deserialized from a string but is not validated!

Thanks to `try_from` attribute in serde, we can tell serde to deserialize a string into `String` first, and pass the `String` to `Email::try_from` to get a `Email`.

```rust
use serde::Deserialize;
use std::convert::TryFrom;

#[derive(Deserialize)]
// Here we tell serde to call `Email::try_from` with a `String`
#[serde(try_from = "String")]
pub struct Email(String);

impl TryFrom<String> for Email {
    type Error = String;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        Email::try_new(value)
    }
}
```

```rust
let email: Email = serde_json::from_str("\"user@example.com\"").unwrap();
// Email("user@example.com".to_string())
```

The above code for deserializing is equivalent to the following:

```rust
let string_value: String = serde_json::from_str("\"user@example.com\"").unwrap();
let email: Email::try_from(string_value).unwrap();
```

## Validate fields

We can easily use `Email` as the type of a field to validate it when deserializing the struct.

```rust
#[derive(Deserialize)]
pub struct User {
    name: String,
    email: Email,
}

let user: User = serde_json::from_str(
    r#"{"name": "Alice", "email": "user@example.com"}"#
).unwrap();
// User {
//     name: "Alice".to_string(),
//     email: Email("user@example.com".to_string()),
// }
```

## Validate structs

Sometimes, the struct itself should be validated before constructed. For example, we have an input struct `ValueRange` which has two fields `min` and `max`. `min` should be not larger than `max`.

Similar to `Email` we can define `ValueRange` like the following:

```rust
pub struct ValueRange {
    min: i32,
    max: i32,
}

impl ValueRange {
    pub fn try_new(min: i32, max: i32) -> Result<Self, String> {
        if min <= max {
            Ok(ValueRange { min, max })
        } else {
            Err("Invalid ValueRange".to_string())
        }
    }

    pub fn min(&self) -> i32 {
        self.min
    }
    pub fn max(&self) -> i32 {
        self.max
    }
}
```

Note that calling `ValueRange::try_new` is the only way to construct a `ValueRange`. But if we just derive `#[derive(Deserialize)]` for `ValueRange`, it will be deserialized without validation.

Thus, we can introduce a new type `ValueRangeUnchecked` which shares the same data structure with `ValueRange`.

```rust
#[derive(Deserialize)]
struct ValueRangeUnchecked {
    min: i32,
    max: i32,
}
```

Then tell serde to deserialize data into `ValueRangeUnchecked` first and then convert it into `ValueRange` by calling `ValueRange::try_from`.

```rust
#[derive(Deserialize)]
#[serde(try_from = "ValueRangeUnchecked")]
pub struct ValueRange {
    min: i32,
    max: i32,
}

impl TryFrom<ValueRangeUnchecked> for ValueRange {
    type Error = String;

    fn try_from(value: ValueRangeUnchecked) -> Result<Self, Self::Error> {
        let ValueRangeUnchecked { min, max } = value;
        Self::try_new(min, max)
    }
}
```

Note that we can keep `ValueRangeUnchecked` only visible to this mod as it is only used privately in deserialization.

```rust
let range: ValueRange = serde_json::from_str(r#"{"min": 1, "max": 10}"#).unwrap();
```

The above code for deserialization is equivalent to:

```rust
let range_unchecked: ValueRangeUnchecked = serde_json::from_str(r#"{"min": 1, "max": 10}"#).unwrap();
let range: ValueRange = ValueRange::try_from(range_unchecked).unwrap();
```

## Full code

For the full working code, you can checkout this repo:
{% github EqualMa/serde-validation-with-try-from no-readme %}

Thanks for reading! If this post helps you, you can [buy me a coffee](https://ko-fi.com/equalma) ♥.