JSON and Serde
TypeScript types vs Rust structs
Section titled “TypeScript types vs Rust structs”In TypeScript you define a type or interface for your API payload and use zod (or a similar library) for runtime validation. In Rust, the struct is the type AND the validator — serde derives serialization and deserialization at compile time, and missing or wrong-typed fields are rejected at the deserialization step automatically.
import { z } from 'zod';
// Define schema + type togetherconst UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(),});
type User = z.infer<typeof UserSchema>;
// Validate at runtimeconst parsed = UserSchema.parse(rawJson); // throws ZodError if invaliduse serde::{Deserialize, Serialize};
// Derive gives you compile-time serialization AND runtime validation#[derive(Debug, Serialize, Deserialize)]struct User { id: u32, name: String, email: String,}
// Deserializing validates structure and types automaticallylet user: User = serde_json::from_str(raw_json)?; // returns Err if invalidJson<T> in Axum — request and response
Section titled “Json<T> in Axum — request and response”Axum’s Json<T> extractor/responder is the bridge between HTTP bodies and your Rust types. When used as a parameter, it deserializes the request body. When used as a return value, it serializes the response.
// Express — parse body + respond with JSONapp.post('/users', express.json(), async (req, res) => { const user: CreateUser = CreateUserSchema.parse(req.body); const saved = await db.save(user); res.status(201).json(saved); // manual serialization call});use axum::{http::StatusCode, Json};use serde::{Deserialize, Serialize};
#[derive(Deserialize)]struct CreateUser { name: String, email: String }
#[derive(Serialize)]struct User { id: u32, name: String, email: String }
// Json<CreateUser> deserializes the body// (StatusCode, Json<User>) serializes the responseasync fn create_user( Json(payload): Json<CreateUser>,) -> (StatusCode, Json<User>) { let user = User { id: 1, name: payload.name, email: payload.email }; (StatusCode::CREATED, Json(user))}Serde field renaming and skipping
Section titled “Serde field renaming and skipping”Serde supports the same field-level transformations you get from TypeScript’s class-transformer or zod .transform():
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]struct User { id: u32,
#[serde(rename = "fullName")] name: String,
#[serde(skip_serializing_if = "Option::is_none")] avatar_url: Option<String>,
#[serde(skip_deserializing)] created_at: String,}Try it — serde serialize / deserialize
Section titled “Try it — serde serialize / deserialize”The snippet below runs entirely in the browser. It does not use Axum (no server needed) — just serde and serde_json, which are available on the Rust Playground.
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]struct User { id: u32, name: String, email: String,}
fn main() { // Serialize struct -> JSON string let user = User { id: 1, name: String::from("Alice"), }; let json = serde_json::to_string_pretty(&user).unwrap(); println!("{}", json);
// Deserialize JSON string -> struct let parsed: User = serde_json::from_str(json_str).unwrap(); println!("{:?}", parsed);}Compiling…