Validate fields and types in serde with TryFrom
EqualMa
Posted on October 10, 2021
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 ♥.
Posted on October 10, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.