`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