Skip to main content
Lesson 6 of 12Beginner⏱ 10 mins

Returning JSON

Sending JSON APIs.

42% of path

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

code_blockstoml
[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

code_blocksrust
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:

code_blocksrust
#[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:

code_blocksrust
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:

code_blocksrust
#[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.

src/main.rs
1use actix_web::{get, web, Responder};
2use serde::Serialize;
3
4#[derive(Serialize)]
5struct User {
6 id: u32,
7 name: String,
8}
9
10#[get("/user")]
11async fn get_user() -> impl Responder {
12 let user = User { id: 1, name: "Alice".to_string() };
13 web::Json(user)
14}
Line 4
#[derive(Serialize)]

What it does

Automatically generates code to convert the struct into JSON.

Why use it?

Writing manual JSON conversion is tedious and error-prone.

How it works underneath

Provided by the `serde` crate.

If you remove it

The code will not compile because `web::Json` requires the `Serialize` trait.

"What If?" Experiment

Learning by doing. Modify the code to see what happens.

Goal

See how rename_all changes the JSON output.

Starting Code
#[derive(Serialize)] struct Profile { user_name: String, }
Suggested Modification

Add #[serde(rename_all = "camelCase")] above the struct.

Expected Result

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.

Incorrect Pattern
serde = "1"
Correct Pattern
serde = { version = "1", features = ["derive"] }

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.

Incorrect Pattern
struct User { id: u32 } web::Json(User { id: 1 })
Correct Pattern
#[derive(Serialize)] struct User { id: u32 } web::Json(User { id: 1 })

Explanation: Responder for web::Json<T> requires T: Serialize. The compiler error mentions that trait bound.

Practical Exercise

Build a JSON product endpoint

Task

Create 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.