๐ŸŽ“ A1Learner
๐Ÿ”

Rust Serde Custom Rules

Serde custom rules are field attributes that plug a custom function into one field. Derive covers the plain cases.

The problem: a number in quotes

The level field is a u32 in Rust. The feed sends it as text. The parse fails.

#[derive(Deserialize)]
struct Reading {
    level: u32,
}

let p = serde_json::from_str::<Reading>("{\"level\":\"7\"}");
println!("{}", p.unwrap_err());

A reader function for one field

deserialize_with names a function with a fixed shape. It receives the raw value. It returns the field, or an error of its own.

fn number_from_text<'de, D: serde::Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
    let s = String::deserialize(d)?;
    s.parse().map_err(serde::de::Error::custom)
}

Point the field at the function

The attribute sits on the one field that misbehaves. Every other field still uses derive.

#[derive(Deserialize, Debug)]
struct Reading {
    #[serde(deserialize_with = "number_from_text")]
    level: u32,
}

let r: Reading = serde_json::from_str("{\"level\":\"7\"}").unwrap();
println!("level = {}", r.level);

A writer function for one field

serialize_with is the mirror image. This one writes a bool as the word yes or no.

fn yes_no<S: serde::Serializer>(on: &bool, s: S) -> Result<S::Ok, S::Error> {
    s.serialize_str(if *on { "yes" } else { "no" })
}

#[derive(Serialize)]
struct Lamp {
    #[serde(serialize_with = "yes_no")]
    on: bool,
}

println!("{}", serde_json::to_string(&Lamp { on: true }).unwrap());

with pairs the two functions

The with attribute names a module instead of two functions. The module must hold a serialize function and a deserialize function.

#[serde(with = "number_text")]
level: u32,

Example

// Cargo.toml: serde = { version = "1.0.229", features = ["derive"] }, serde_json = "1.0.154"
use serde::{Deserialize, Deserializer, Serialize, Serializer};

fn number_from_text<'de, D: Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
    let s = String::deserialize(d)?;
    s.parse().map_err(serde::de::Error::custom)
}

fn number_as_text<S: Serializer>(n: &u32, s: S) -> Result<S::Ok, S::Error> {
    s.serialize_str(&n.to_string())
}

#[derive(Serialize, Deserialize, Debug)]
struct Meter {
    #[serde(serialize_with = "number_as_text", deserialize_with = "number_from_text")]
    level: u32,
}

fn main() {
    let m: Meter = serde_json::from_str("{\"level\":\"7\"}").unwrap();
    println!("level = {}", m.level);
    println!("{}", serde_json::to_string(&m).unwrap());
}

Key points

  • Custom rules work one field at a time.
  • The rest of the struct keeps the derive.
  • Use with when a field needs both directions under one name.
  • It points at a module with two functions.
  • Your reader can reject bad values with its own message.
  • Error::custom passes that message to serde.
  • A type from another crate cannot take your derive.
  • Wrap it in a small struct of your own first.
  • Writing a whole Serialize impl by hand is rare.
  • Save it for a type whose shape is truly strange.
  • It is fast, and its edge cases are already tested.
  • A hand-written rule is code you must test yourself.

In this example, one field gets both custom rules. The reader turns the quoted text into the number 7. The writer turns the number back into quoted text. The field round-trips through the feed's odd shape. The struct around it never changed.

Note

Derive is the default for a reason.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

One field arrives as the text "7". The Rust field must be a u32. What is the smallest fix?