`serde`
Implementing Deserialize<'de> on a struct always makes me take a step back. I start to question the validity of having to write a Visitor<'de>-trait compatible struct. Then I slowly remember that JSON can be a nested tree of pretty much any kind of data-object representation and I remember the brilliance of the with serde's API approach all over again.
It's a good set of feels.
That being said, the documentation always takes a moment for me to remember what to do. Therefore I came up with these snippets to help me remember what I need to do, quicker than it would take for me to wait for ChatGPT to finish its generation phase
Renaming a property on the Object
Rust likes to use snake_case pretty much everywhere a variable, method, or function is written. Having the ability to rename the json object properties to something more Rust-like-able, allows to skip having the #[allow(dead_code)] macro
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Content {
#[serde(rename="Name")]
name: String,
}
#[tokio::main]
async fn main() {
let content = r#"{"Name": "my name is, serde"}"#;
let content: Content = serde_json::from_str(content).unwrap();
println!("{:?}", content.name);
}
In Content, name is renamed from Name using #[serde(rename="Name").
Deserializing a digit into an enum
Sometimes #[derive(Deserialize)] isn't enough and we need to drop down into writing a trait implementation. Enums are straight forward using TryFrom. It can be used to map an error into something Deserialize<'de> will understand.
For example, if trying to keep a log of people logging into and out of an application. Tracking that information in a struct Action{event:String} is one way to do it. I personally would prefer to use a Rust enum. The problem though is serde doesn't provide a macro to support enums the way I like to implement. ( At least from what I'm aware of ).
#[derive(Debug)]
pub enum AuditLogAction {
LoggedIn,
LoggedOut,
}
Implement the TryFrom trait on the enum.
impl TryFrom<i32> for AuditLogAction {
type Error = AuditEndpointError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
Ok(match value {
0 => Self::LoggedIn,
1 => Self::LoggedOut,
_ => return Err(AuditEndpointError::AuditLogActionNotSupported(value))
})
}
}
Finally, implement the deserialize trait which calls the TryFrom trait and maps the error to something Deserialize<'de> can understand.
impl<'de> Deserialize<'de> for AuditLogAction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: serde::Deserializer<'de> {
let value = i32::deserialize(deserializer)?;
AuditLogAction::try_from(value).map_err(serde::de::Error::custom)
}
}
With everything setup, time to implement the main function that'll run the program and convert an array of integers from JSON into enum variants.
#[tokio::main]
async fn main() {
let content = r#"[0, 1]"#;
let content: Vec<AuditLogAction> = serde_json::from_str(content).unwrap();
println!("{content:?}");
}
I'm still looking for a reason to use the Visitor<'de> pattern. Any suggestions?
Full source code can be found here