Rust Pattern Matching: Taming the “Schrödinger’s Cat” of JSON
The Pain of API JSON Schemas: The Love-Hate Relationship with JSON
After exploring the HTTP Request API HMAC signature: adding a “security label” to API requests, we finally saw the long-awaited HTTP response. As a heavy user of APIs, I deal with various JSON data every day. They are like the “Schrödinger’s cat” of the internet world—until you actually open the response, you never know what’s inside. No complaints here.
“I clearly defined the struct according to the documentation, why did deserialization fail?” — A daily lament of every Rust developer
Just like the example below:
#[derive(Debug, Serialize, Deserialize)]
pub struct Parameter {
name: String,
current_value: String,
default_value: String, // This might explode!
// Other fields...
}
When the <span>defaultValue</span> field suddenly becomes <span>null</span>, or simply disappears, our beautiful dream shatters. At this point, Rust’s type system will mercilessly slap us in the face.
JSON: The “Dialect” of the Internet World
Before diving into our solution, if you’re not familiar, you can first visit the “hometown” of JSON — the official JSON website and JSON Wikipedia.
In the world of REST APIs, JSON is like the “Mandarin” of the internet, but the problem is — everyone speaks “Mandarin” with a bit of an accent.
Characteristics of JSON:
- • Free-spirited: No fixed schema, it can change however it wants
- • Type-agnostic: The same field can be a string one moment and a number the next
- • Field capriciousness: A field present today may disappear tomorrow
It’s like ordering food at a restaurant:
- • The menu says “Braised Pork” (documentation)
- • What actually arrives might be “Sweet and Sour Ribs” (actual return)
- • Sometimes, you might not even get a plate (field missing)
Rust’s Solution: Pattern Matching to the Rescue
When strong-typed structs can’t handle the ever-changing JSON, we can call upon Rust’s pattern matching, the “Swiss Army knife”.
Step 1: Embrace <span>serde_json::Value</span>
<span>serde_json::Value</span> is like the “universal container” for JSON in the Rust world; it can represent any JSON value:
use serde_json::Value;
let data: Value = serde_json::from_str(json_str)?;
This is equivalent to saying: “I don’t care what type you are, just put you in this universal box for now”.
Step 2: Pattern Matching Comes into Play
Now we can use Rust’s powerful pattern matching to “fish” — only catching the fields we want:
if let Some(contents) = data.get("data").and_then(|d| d.get("contents")) {
// Only output if both match
if let Some(contents_array) = contents.as_array() {
for item in contents_array {
if let (Some(name), Some(current_value)) = (
item.get("name").and_then(|v| v.as_str()),
item.get("currentValue").and_then(|v| v.as_str()),
) {
// Finally caught the fish we wanted!
println!("Name: {}, Value: {}", name, current_value);
}
}
}
}
This writing style is like:
- 1. First, check if there is a “data” bag
- 2. Then look for the “contents” box inside
- 3. Confirm that the box contains an array
- 4. For each item in the array, try to extract “name” and “currentValue”
- 5. If successful, cheerfully use them
A More Elegant Approach: <span>and_then</span> Chaining
Rust’s <span>Option</span> type provides the <span>and_then</span> method, allowing us to write the above “fishing” process more elegantly:
let kvset: Vec<_> = data["data"]["contents"]
.as_array()
.unwrap()
.iter()
.filter_map(|item| {
let name = item.get("name").and_then(|v| v.as_str())?;
let current_value = item.get("currentValue").and_then(|v| v.as_str())?;
Some(KeyValue {
name: name.to_string(),
current_value: current_value.to_string(),
})
})
.collect();
This is like:
- 1. Following the path
<span>data -> contents</span>deeper - 2. Treating it as an array
- 3. Attempting to extract name and currentValue from each element
- 4. If successful, constructing a KeyValue
- 5. Finally collecting all successful cases
Defensive Programming: The “Seatbelt” of JSON Parsing
When dealing with unknown JSON structures, we need to fasten our “seatbelt”:
- 1. Use
<span>Option</span>frequently: Fields may not exist - 2. Type checking: Fields may be of unexpected types
- 3. Step-by-step exploration: Explore the JSON structure gradually
- 4. Error handling: Be prepared to handle various unexpected situations
// Defensive programming example
let name= content.get("name").and_then(|v| v.as_str()).unwrap_or("NULL");
let current_value =content.get("currentValue").and_then(|v| v.as_str()).unwrap_or("NULL");
Performance Considerations: Avoid Overusing Value
While <span>serde_json::Value</span> is very convenient, it comes with some performance overhead:
- • Higher memory usage
- • Access speed is slower than direct structs
Therefore, if the JSON structure of the API is relatively stable, prefer using strong-typed structs. Only use <span>Value</span> with pattern matching when the structure is uncertain.
Conclusion: Flexibly Responding to JSON’s “Seventy-Two Changes”
Handling unknown JSON structures in Rust is like playing with a capricious cat:
- 1. Strong-typed structs are the cat’s “standard posture” — the ideal situation
- 2. serde_json::Value is the cat’s “universal nest” — it can accommodate any posture
- 3. Pattern matching is our “cat teaser” — precisely capturing the parts we want
Remember: In the world of Rust, you can leverage the powerful type system for performance advantages while also using flexible pattern matching to handle the ever-changing JSON. The combination of both is the true “cat-taming expert”!
“In Rust, you’re not just parsing JSON; you’re dancing the tango with JSON — sometimes you lead, sometimes you have to follow its rhythm.” — An anonymous Rustacean
Now, go conquer those capricious JSONs! Remember to bring your pattern matching “Swiss Army knife” and your defensive programming “airbag”; you will surely survive in the jungle of API development!
Initial exploration of HTTP Request API HMAC signature: adding a “security label” to API requests
## Rust Demo
use anyhow::Result;use serde::{Deserialize, Serialize};use serde_json::Value;use tabled::{Table, Tabled};#[derive(Debug, Serialize, Deserialize)]pub struct Data { pub contents: Vec,}
#[derive(Debug, Serialize, Deserialize)]pub struct Response { pub data: Data, pub duration: i32, pub server: String, pub status: i32, pub successful: bool, pub timestamp: String, #[serde(rename = "traceId")] pub trace_id: String,}
#[derive(Debug, Serialize, Deserialize, Tabled)]pub struct KeyValue { pub name: String, pub current_value: String,}
pub fn parse_what_you_need(json: &str) -> Result<()> { let response: Response = serde_json::from_str(json).unwrap(); let mut kvset: Vec = Vec::new(); for content in response.data.contents { if let (Some(name), Some(current_value)) = ( content.get("name").and_then(|v| v.as_str()), content.get("currentValue").and_then(|v| v.as_str()), ) { kvset.push(KeyValue { name:name.to_string(), current_value:current_value.to_string(), }); } } println!("{}", Table::new(kvset)); Ok(())}
#[cfg(test)]mod tests { use super::*; #[test] fn test_json() { let json_data = r#"{ "data": { "contents": [ { "currentValue": "ON", "defaultValue": "ON", "description": "Whether to auto-commit.", "name": "autocommit", "readonly": false, "valueRange": { "allowedValues": "OFF,ON", "type": "ENUM" } }, { "currentValue": "ON", "defaultValue": "ON", "description": "Whether to allow aggregation operation pushdown.", "name": "ob_enable_aggregation_pushdown", "readonly": false, "valueRange": { "allowedValues": "OFF,ON", "type": "ENUM" } },{ "defaultValue": "ON", "description": "Whether to allow aggregation operation pushdown.", "name": "test2", "readonly": false, "valueRange": { "allowedValues": "OFF,ON", "type": "ENUM" } },{ "currentValue": "xxxx3", "defaultValue": "ON", "description": "Whether to allow aggregation operation pushdown.", "readonly": false, "valueRange": { "allowedValues": "OFF,ON", "type": "ENUM" } } ] }, "duration": 219, "server": "a83ad33525", "status": 200, "successful": true, "timestamp": "2021-09-06T01:49:16.946+08:00", "traceId": "00a58024130d474b" }"#; // Test that parsing doesn't panic and returns Ok let result = parse_what_you_need(json_data); assert!(result.is_ok()); }}