Skip to content

JSON and Serde

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 validatorserde derives serialization and deserialization at compile time, and missing or wrong-typed fields are rejected at the deserialization step automatically.

TypeScript
import { z } from 'zod';
// Define schema + type together
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
// Validate at runtime
const parsed = UserSchema.parse(rawJson); // throws ZodError if invalid
Rust
use 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 automatically
let user: User = serde_json::from_str(raw_json)?; // returns Err if invalid

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.

TypeScript
// Express — parse body + respond with JSON
app.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
});
Rust
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 response
async 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 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,
}

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"),
email: String::from("[email protected]"),
};
let json = serde_json::to_string_pretty(&user).unwrap();
println!("{}", json);
// Deserialize JSON string -> struct
let json_str = r#"{"id":2,"name":"Bob","email":"[email protected]"}"#;
let parsed: User = serde_json::from_str(json_str).unwrap();
println!("{:?}", parsed);
}
Which two derive macros does serde provide for JSON serialization?
When `Json<T>` is used as an Axum handler **parameter**, what does it do?
Which serde attribute hides a field from the JSON output when it is `None`?