use serde::{Deserialize, Deserializer};
use time::OffsetDateTime;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserProfile {
#[serde(rename = "userId")]
pub id: u64,
pub display_name: String,
#[serde(default)]
pub bio: Option<String>,
#[serde(default)]
pub roles: Vec<String>,
pub address: Option<Address>,
#[serde(deserialize_with = "parse_timestamp")]
pub joined_at: OffsetDateTime,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Address {
pub line1: String,
#[serde(default)]
pub line2: Option<String>,
pub postal_code: String,
pub country: String,
}
fn parse_timestamp<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
OffsetDateTime::parse(&raw, &time::format_description::well_known::Rfc3339)
.map_err(serde::de::Error::custom)
}
use crate::models::UserProfile;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ApiError {
#[error("request failed: {0}")]
Transport(#[from] reqwest::Error),
#[error("could not decode profile: {0}")]
Decode(String),
}
pub struct ApiClient {
http: reqwest::Client,
base_url: String,
}
impl ApiClient {
pub fn new(base_url: impl Into<String>) -> Self {
ApiClient {
http: reqwest::Client::new(),
base_url: base_url.into(),
}
}
pub async fn fetch_profile(&self, user_id: u64) -> Result<UserProfile, ApiError> {
let url = format!("{}/users/{}", self.base_url, user_id);
let response = self
.http
.get(&url)
.header("Accept", "application/json")
.send()
.await?
.error_for_status()?;
let profile = response
.json::<UserProfile>()
.await
.map_err(|e| ApiError::Decode(e.to_string()))?;
Ok(profile)
}
}
Real-world JSON APIs rarely match Rust's naming conventions or guarantee every field is present, so a client needs a data layer that maps external camelCase keys onto idiomatic snake_case structs and tolerates missing or null values without panicking. The models.rs tab shows how serde handles this declaratively through attributes rather than hand-written parsing.
At the container level, #[serde(rename_all = "camelCase")] on UserProfile rewrites every field name during deserialization, so display_name binds to displayName on the wire without per-field annotations. Individual overrides use #[serde(rename = "...")], as with id mapping to the provider's userId. Truly optional data is modeled as Option<T>, and #[serde(default)] on bio means a completely absent key deserializes to None instead of raising an error — the distinction being that Option alone still requires the key to appear (even as JSON null), while default also covers omission.
The #[serde(default)] on the roles vector demonstrates a common pattern: a missing array becomes an empty Vec rather than forcing callers to unwrap. The nested Address struct shows that these attributes compose recursively, and deserialize_with on joined_at delegates to a custom parse_timestamp function for a format serde can't infer.
The client.rs tab wires this into an HTTP call. fetch_profile uses reqwest to request the resource, calls error_for_status to convert non-2xx responses into errors early, and then .json::<UserProfile>() drives the whole deserialization graph. Because the fallible steps all return compatible error types, the function threads them together with ? and a thiserror-style ApiError enum, keeping transport failures distinct from decoding failures.
The trade-off of this approach is that malformed-but-plausible payloads fail loudly at the boundary, which is usually desirable: bugs surface as typed ApiError::Decode values instead of silent Nones deep in business logic. When an API is volatile, developers lean harder on default and Option to stay forward-compatible; when it is strict, they omit them so missing fields become hard errors. This attribute-driven style keeps the parsing intent visible next to the data model.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
Share this code
Here's the card — post it anywhere.