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)