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")], 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.
pub struct Email(String);
Email should only be constructed after validation so we can impl try_new as the only way to construct Email.
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
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:
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.
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)
}
}
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:
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.
#[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:
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.
#[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.
#[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.
let range: ValueRange = serde_json::from_str(r#"{"min": 1, "max": 10}"#).unwrap();
The above code for deserialization is equivalent to:
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:
EqualMa
/
serde-validation-with-try-from
Validate fields in serde with TryFrom trait
Thanks for reading! If this post helps you, you can buy me a coffee ♥.
Top comments (0)