JSON响应 #

JSON是现代Web API的标准数据格式。本节将深入介绍如何设计优雅的JSON API响应。

标准响应格式 #

统一响应结构 #

rust
use rocket::serde::{Serialize, Deserialize};
use rocket::serde::json::Json;

#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct ApiResponse<T> {
    success: bool,
    data: Option<T>,
    error: Option<ApiError>,
    message: Option<String>,
}

#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct ApiError {
    code: String,
    message: String,
}

impl<T: Serialize> ApiResponse<T> {
    fn success(data: T) -> Self {
        ApiResponse {
            success: true,
            data: Some(data),
            error: None,
            message: None,
        }
    }

    fn error(code: &str, message: &str) -> Self {
        ApiResponse {
            success: false,
            data: None,
            error: Some(ApiError {
                code: code.to_string(),
                message: message.to_string(),
            }),
            message: None,
        }
    }
}

使用示例 #

rust
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct User {
    id: u32,
    name: String,
    email: String,
}

#[get("/users/<id>")]
fn get_user(id: u32) -> Json<ApiResponse<User>> {
    if id == 0 {
        Json(ApiResponse::error("USER_NOT_FOUND", "User does not exist"))
    } else {
        Json(ApiResponse::success(User {
            id,
            name: "Alice".to_string(),
            email: "alice@example.com".to_string(),
        }))
    }
}

分页响应 #

分页结构 #

rust
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct PaginatedResponse<T> {
    data: Vec<T>,
    pagination: Pagination,
}

#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct Pagination {
    page: u32,
    per_page: u32,
    total: u64,
    total_pages: u32,
    has_next: bool,
    has_prev: bool,
}

impl<T: Serialize> PaginatedResponse<T> {
    fn new(data: Vec<T>, page: u32, per_page: u32, total: u64) -> Self {
        let total_pages = ((total as f64) / (per_page as f64)).ceil() as u32;
        PaginatedResponse {
            data,
            pagination: Pagination {
                page,
                per_page,
                total,
                total_pages,
                has_next: page < total_pages,
                has_prev: page > 1,
            },
        }
    }
}

分页路由 #

rust
use rocket::request::Query;
use rocket::FromForm;

#[derive(FromForm)]
struct PaginationParams {
    #[field(default = 1)]
    page: u32,
    
    #[field(default = 10)]
    per_page: u32,
}

#[get("/users?<params..>")]
fn list_users(params: Query<PaginationParams>) -> Json<PaginatedResponse<User>> {
    let users = vec![
        User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string() },
        User { id: 2, name: "Bob".to_string(), email: "bob@example.com".to_string() },
    ];
    
    Json(PaginatedResponse::new(users, params.page, params.per_page, 100))
}

错误响应 #

错误码设计 #

rust
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct ErrorResponse {
    success: bool,
    error: ErrorDetail,
}

#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct ErrorDetail {
    code: String,
    message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    details: Option<Vec<FieldError>>,
}

#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct FieldError {
    field: String,
    message: String,
}

impl ErrorResponse {
    fn new(code: &str, message: &str) -> Self {
        ErrorResponse {
            success: false,
            error: ErrorDetail {
                code: code.to_string(),
                message: message.to_string(),
                details: None,
            },
        }
    }

    fn with_details(code: &str, message: &str, details: Vec<FieldError>) -> Self {
        ErrorResponse {
            success: false,
            error: ErrorDetail {
                code: code.to_string(),
                message: message.to_string(),
                details: Some(details),
            },
        }
    }
}

错误码常量 #

rust
mod error_codes {
    pub const VALIDATION_ERROR: &str = "VALIDATION_ERROR";
    pub const NOT_FOUND: &str = "NOT_FOUND";
    pub const UNAUTHORIZED: &str = "UNAUTHORIZED";
    pub const FORBIDDEN: &str = "FORBIDDEN";
    pub const INTERNAL_ERROR: &str = "INTERNAL_ERROR";
}

RESTful API设计 #

资源命名 #

rust
#[get("/users")]
fn list_users() -> Json<Vec<User>> {
    Json(vec![])
}

#[get("/users/<id>")]
fn get_user(id: u32) -> Result<Json<User>, Status> {
    Ok(Json(User { id, name: "User".to_string(), email: "user@example.com".to_string() }))
}

#[post("/users", format = "json", data = "<user>")]
fn create_user(user: Json<CreateUser>) -> status::Created<Json<User>> {
    status::Created(Json(User {
        id: 1,
        name: user.name.clone(),
        email: user.email.clone(),
    }))
}

#[put("/users/<id>", format = "json", data = "<user>")]
fn update_user(id: u32, user: Json<UpdateUser>) -> Json<User> {
    Json(User {
        id,
        name: user.name.clone().unwrap_or_default(),
        email: user.email.clone().unwrap_or_default(),
    })
}

#[delete("/users/<id>")]
fn delete_user(id: u32) -> status::NoContent {
    status::NoContent
}

嵌套资源 #

rust
#[get("/users/<user_id>/posts")]
fn list_user_posts(user_id: u32) -> Json<Vec<Post>> {
    Json(vec![])
}

#[get("/users/<user_id>/posts/<post_id>")]
fn get_user_post(user_id: u32, post_id: u32) -> Json<Post> {
    Json(Post { id: post_id, user_id, title: "Post".to_string() })
}

#[post("/users/<user_id>/posts", format = "json", data = "<post>")]
fn create_user_post(user_id: u32, post: Json<CreatePost>) -> status::Created<Json<Post>> {
    status::Created(Json(Post {
        id: 1,
        user_id,
        title: post.title.clone(),
    }))
}

响应头设置 #

自定义响应头 #

rust
use rocket::http::Header;
use rocket::response::content::RawJson;

#[get("/with-headers")]
fn with_headers() -> (RawJson<&'static str>, Header<'static>) {
    (
        RawJson(r#"{"message":"ok"}"#),
        Header::new("X-Request-Id", "abc123"),
    )
}

CORS头 #

rust
use rocket::http::{Header, Status};
use rocket::response::{Responder, Result};
use rocket::request::Request;

struct Cors<T>(T);

impl<'r, T: Responder<'r, 'r>> Responder<'r, 'r> for Cors<T> {
    fn respond_to(self, request: &'r Request<'_>) -> Result<'r> {
        let mut response = self.0.respond_to(request)?;
        response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
        response.set_header(Header::new("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"));
        response.set_header(Header::new("Access-Control-Allow-Headers", "Content-Type, Authorization"));
        Ok(response)
    }
}

#[get("/api/data")]
fn api_data() -> Cors<Json<Data>> {
    Cors(Json(Data { value: "Hello".to_string() }))
}

完整示例 #

rust
#[macro_use] extern crate rocket;

use rocket::serde::{Serialize, Deserialize};
use rocket::serde::json::Json;
use rocket::http::Status;
use rocket::response::status;
use rocket::request::Query;
use rocket::FromForm;
use std::collections::HashMap;
use std::sync::Mutex;
use rocket::State;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct User {
    id: u32,
    name: String,
    email: String,
}

#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
struct CreateUser {
    name: String,
    email: String,
}

#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
struct UpdateUser {
    name: Option<String>,
    email: Option<String>,
}

#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct ApiResponse<T> {
    success: bool,
    data: Option<T>,
    error: Option<ErrorDetail>,
}

#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct ErrorDetail {
    code: String,
    message: String,
}

#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct PaginatedResponse<T> {
    data: Vec<T>,
    page: u32,
    per_page: u32,
    total: u64,
}

type Db = Mutex<HashMap<u32, User>>;

impl<T: Serialize> ApiResponse<T> {
    fn success(data: T) -> Self {
        ApiResponse {
            success: true,
            data: Some(data),
            error: None,
        }
    }

    fn error(code: &str, message: &str) -> Self {
        ApiResponse {
            success: false,
            data: None,
            error: Some(ErrorDetail {
                code: code.to_string(),
                message: message.to_string(),
            }),
        }
    }
}

#[derive(FromForm)]
struct Pagination {
    #[field(default = 1)]
    page: u32,
    #[field(default = 10)]
    per_page: u32,
}

#[get("/users")]
fn list_users(db: &State<Db>, pagination: Query<Pagination>) -> Json<PaginatedResponse<User>> {
    let users = db.lock().unwrap();
    let all_users: Vec<User> = users.values().cloned().collect();
    let total = all_users.len() as u64;
    
    let start = ((pagination.page - 1) * pagination.per_page) as usize;
    let end = std::cmp::min(start + pagination.per_page as usize, all_users.len());
    let page_users: Vec<User> = all_users.into_iter().skip(start).take(end - start).collect();
    
    Json(PaginatedResponse {
        data: page_users,
        page: pagination.page,
        per_page: pagination.per_page,
        total,
    })
}

#[get("/users/<id>")]
fn get_user(id: u32, db: &State<Db>) -> Json<ApiResponse<User>> {
    let users = db.lock().unwrap();
    match users.get(&id) {
        Some(user) => Json(ApiResponse::success(user.clone())),
        None => Json(ApiResponse::error("NOT_FOUND", &format!("User {} not found", id))),
    }
}

#[post("/users", format = "json", data = "<data>")]
fn create_user(data: Json<CreateUser>, db: &State<Db>) -> status::Created<Json<ApiResponse<User>>> {
    let mut users = db.lock().unwrap();
    let id = (users.len() + 1) as u32;
    let user = User {
        id,
        name: data.name.clone(),
        email: data.email.clone(),
    };
    users.insert(id, user.clone());
    status::Created(Json(ApiResponse::success(user)))
}

#[put("/users/<id>", format = "json", data = "<data>")]
fn update_user(id: u32, data: Json<UpdateUser>, db: &State<Db>) -> Json<ApiResponse<User>> {
    let mut users = db.lock().unwrap();
    match users.get_mut(&id) {
        Some(user) => {
            if let Some(name) = &data.name {
                user.name = name.clone();
            }
            if let Some(email) = &data.email {
                user.email = email.clone();
            }
            Json(ApiResponse::success(user.clone()))
        }
        None => Json(ApiResponse::error("NOT_FOUND", &format!("User {} not found", id))),
    }
}

#[delete("/users/<id>")]
fn delete_user(id: u32, db: &State<Db>) -> Result<status::NoContent, Json<ApiResponse<()>>> {
    let mut users = db.lock().unwrap();
    if users.remove(&id).is_some() {
        Ok(status::NoContent)
    } else {
        Err(Json(ApiResponse::error("NOT_FOUND", &format!("User {} not found", id))))
    }
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(Mutex::new(HashMap::<u32, User>::new()))
        .mount("/api", routes![
            list_users,
            get_user,
            create_user,
            update_user,
            delete_user
        ])
}

下一步 #

掌握了JSON响应后,让我们继续学习 模板渲染,了解如何渲染动态HTML页面。

最后更新:2026-03-28