class: center, middle
Daniel J. Pezely
dpezely
on
GitHub,
GitLab,
Linkedin
° “serde” means serialize / deserialize
and is the name of a Rust crate
class: center, middle
class: middle
- The Way Of Serde
- A Realistic Example -- minimalist data files
- Simple Hierarchy of Enums -- simple tricks
- Untagged Enums -- "... indistinguishable from magic"
- Renaming Variants -- Pretty JSON and prettier Rust
- Error Handling -- using
?
early and often - Flattening -- but still writing less code
- Asymmetric JSON -- populate Rust fields only when JSON is non-null
Of course, all this applies to far more than just JSON
But JSON is easier for presentation purposes here
class: center, middle
Take time to read https://serde.rs/ entirely
before jumping into API docs at https://crates.io/crates/serde
Spoilers: it's resolved entirely at compile-time, and
without run-time “reflection” mechanisms
class: middle
Bonus: Deep or mixed structures? Easy!
-
Container attributes for
struct
orenum
declaration -
Variant attributes for each variant of an
enum
-
Field attributes for individual
struct
field or withinenum
variant
-
If writing code handling common patterns:
- That's probably the wrong approach!
-
If writing code to handle name or value conversions:
- That's probably the wrong approach!
-
If checking for existence of nulls or special values:
- That's probably the wrong approach!
-
Make aggressive use of
?
operator- e.g., use
Result
andErrorKind
together
- e.g., use
-
Implement various methods of
From
andInto
traits- compiler reveals exactly what you need
- so this becomes fairly straight-forward plug-and-chug
-
A common Rust idiom-- not just a
serde
thing
-
Populate a nested
enum
and their variants from a flattened set- i.e., each variant must map to exactly one Enum
- then, nested Enums may be resolved when decorating with a single attribute
-
Ingest minimal data file structures to well-defined structures in Rust
- e.g., JSON without naming each structural component
- where keys contain data (NOT name of struct)
-
Thus, have your idiomatic Rust cake and eat minimalist data files too!
???
For those that are non-native to English, "Wanting to have your cake and eat it too" simply indicates the impression of a paradox. For those using serde, however, there is no paradox at all.
class: middle
{
"energy-preferences": {
"2000s": ["solar", "wind"],
"1900s": ["kerosene", "soy", "peanut", "petroleum"],
"1800s": ["wind", "whale", "seal", "kerosene"]
}
}
Notable:
-
Outer structure is an object (NOT an array)
-
Top-level keys contain information (NOT name of structure)
-
Inner values within array indicate mixed categories
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct EnergyPreferenceHistory {
energy_preferences: EnergyPreferences
}
#[derive(Serialize, Deserialize, Debug)]
struct EnergyPreferences (HashMap<Century, Vec<EnergySources>>);
Particularly, serde.rs/container-attrs.html
enum EnergySources { // Don't mix categories like this!
Solar,
Wind,
// ...
Kerosene,
Petroleum,
// ...
PeanutOil,
SoyOil,
// ...
SealBlubber,
WhaleBlubber,
// ...
}
Continuing from previous example...
enum EnergySources {
Sustainable(Inexhaustible),
Animal(Blubber),
Vegetable(Crop),
Mineral(Fossil),
}
enum Inexhaustible { Solar, Wind, /* ... */ }
enum Blubber { Seal, Whale, /* ... */ }
enum Crop { Peanut, Soy, /* ... */ }
enum Fossil { Kerosene, Petroleum, /* ... */ }
???
Focus on the Rust code, not precision of these categories.
For instance, pulp or pellets made from trees or other vegetable matter are all ignored here yet were in common usage during the late Nineteenth and early Twentieth Century within North America.
Other divisions or categories might be better, such as petrochemical, oleochemical, etc. Or rendered, cultivated, extracted, etc.
Continuing from previous example...
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)] // <-- Unflatten from compact JSON
enum EnergySources {
Sustainable(Inexhaustible),
Animal(Blubber),
Vegetable(Crop),
Mineral(Fossil),
}
See "Untagged" section in serde.rs/enum-representations.html
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
enum Century {
#[serde(rename = "1800s")]
NinteenthCentury,
#[serde(rename = "1900s")]
TwentiethCentury,
#[serde(rename = "2000s")]
TwentyfirstCentury
}
fn main() -> Result<(), ErrorKind> {
let json_string = fs::read_to_string("energy.json")?;
let sources: EnergyPreferenceHistory =
serde_json::de::from_str(&json_string)?;
println!("{:#?}", sources);
Ok(())
}
Note uses of question mark ?
operator above
Implementing just the above, the compiler helpfully tells you exactly which impl From
methods to add
Continuing from previous example...
#[derive(Debug)]
enum ErrorKind {
BadJson,
NoJson,
NoFilePath,
Unknown,
}
impl From<serde_json::Error> for ErrorKind {
fn from(err: serde_json::Error) -> ErrorKind {
use serde_json::error::Category;
match err.classify() {
Category::Io => {
println!("Serde JSON IO-error: {:?}", &err);
ErrorKind::NoJson
}
Category::Syntax | Category::Data | Category::Eof => {
println!("Serde JSON error: {:?} {:?}",
err.classify(), &err);
ErrorKind::BadJson
}
}
}
}
class: center, middle
#[derive(Serialize, Deserialize)]
struct CatalogueEntry {
id: u64,
#[serde(flatten)] // <-- Field Attribute
description: HashMap<String, String>,
}
Would ultimately produce the following JSON representation:
{
"id": 1234,
"size": "bigger than a car",
"weight": "less than an airplane"
}
All fields rendered to same level within JSON
fn populate_catalogue() -> Result<(), ErrorKind> {
let id = 1234;
let mut description = HashMap::new();
description.insert("size".to_string(),
"bigger than a house".to_string());
description.insert("weight".to_string(),
"less than an airplane".to_string());
let catalogue = vec![CatalogueEntry{id, description}];
fs::write("foo.json", serde_json::to_string(&catalogue)?)?;
Ok(())
}
struct Thing {
pub keyword: String,
#[serde(default="Vec::new")] // <-- constructor
pub attributes: Vec<String>,
}