Returning JSON
Sending JSON APIs.
Learning Objectives
- Serialize structs to JSON
##Sending JSON from your API
Almost every modern API talks in JSON. Actix Web does not do this on its own; it works together with serde, Rust's standard serialization library. The combination is type safe: you describe your data with a struct, and the compiler guarantees the shape.
#Add the dependencies
[dependencies]
actix-web = "4"
serde = { version = "1", features = ["derive"] }The derive feature lets you write #[derive(Serialize)] instead of implementing the trait by hand.
#Return a struct as JSON
use actix_web::{get, web, App, HttpServer, Responder};
use serde::Serialize;
#[derive(Serialize)]
struct User {
id: u32,
name: String,
}
#[get("/user")]
async fn user() -> impl Responder {
web::Json(User { id: 1, name: "Ada".to_string() })
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(user))
.bind(("127.0.0.1", 8080))?
.run()
.await
}The response body is {"id":1,"name":"Ada"} and Actix automatically sets the header Content-Type: application/json.
#How it works
web::Json<T> is a wrapper type. It implements Responder whenever T implements Serialize. When your handler returns it, Actix serializes the value with serde and builds the response for you. If serialization fails the client gets a 500 error.
#Returning a list
Vectors serialize as JSON arrays:
#[get("/users")]
async fn users() -> impl Responder {
web::Json(vec![
User { id: 1, name: "Ada".into() },
User { id: 2, name: "Linus".into() },
])
}#Choosing a status code
Use HttpResponse when you need more control than web::Json offers:
use actix_web::HttpResponse;
#[get("/created")]
async fn created() -> HttpResponse {
HttpResponse::Created().json(User { id: 3, name: "Grace".into() })
}.json(...) serializes the value and sets the content type in one call.
#Control the field names
serde attributes let your Rust names differ from the JSON names:
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Profile {
user_name: String, // becomes "userName"
is_active: bool, // becomes "isActive"
}Use #[serde(skip_serializing_if = "Option::is_none")] to leave out optional fields that are empty.
Interactive Code Inspector
Select any highlighted line of code below to deeply inspect its mechanics, understand why it exists, and what happens if you change it.
"What If?" Experiment
Learning by doing. Modify the code to see what happens.
See how rename_all changes the JSON output.
Add #[serde(rename_all = "camelCase")] above the struct.
The JSON key changes from "user_name" to "userName".
Explanation: serde rewrites field names while serializing, so your Rust code keeps snake_case and your API uses the camelCase that JavaScript clients prefer.
Common Mistakes
Anticipate and avoid these frequent pitfalls when working with this concept.
Forgetting the serde derive feature
Cargo.toml lists serde without features, so the derive macro does not exist.
Explanation: The Serialize derive macro lives behind the derive feature. Without it you get "cannot find derive macro Serialize".
Returning a struct that does not implement Serialize
The struct is missing #[derive(Serialize)], so web::Json cannot serialize it.
Explanation: Responder for web::Json<T> requires T: Serialize. The compiler error mentions that trait bound.
Practical Exercise
Build a JSON product endpoint
TaskCreate GET /product that returns a product with an id, a name and a price in JSON.
Requirements
- Define a Product struct that derives Serialize
- Return it with web::Json
- Output should look like {"id":1,"name":"Keyboard","price":49.9}
Need a hint?
Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.
Real-World Application
Real APIs define separate response structs (DTOs) from database models, so they control exactly which fields leave the server and can rename or hide fields without touching the database layer.
Key Takeaways
- Add serde with the derive feature and use #[derive(Serialize)] on response types.
- web::Json(value) returns JSON with a 200 status and the right Content-Type.
- HttpResponse::Created().json(value) lets you pick any status code.
- Use #[serde(rename_all = "camelCase")] to match the JSON style your clients expect.
Finished reading?
Mark this lesson as done to track your progress and earn XP.