Skip to content

Работа с JSON

В современных веб-приложениях и API формат JSON является стандартом для обмена данными. Axum предоставляет мощные и гибкие инструменты для работы с JSON как для приема данных от клиентов, так и для отправки ответов. В этом разделе мы рассмотрим различные аспекты работы с JSON в Axum, включая сериализацию, десериализацию, валидацию и обработку ошибок.

Основы работы с JSON

Axum использует крейт serde_json для сериализации и десериализации JSON. Для работы с JSON необходимо добавить следующие зависимости в Cargo.toml:

toml
[dependencies]
axum = "0.7.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

JSON экстрактор

Для извлечения JSON из тела запроса Axum предоставляет экстрактор Json<T>, где T — тип данных, в который необходимо десериализовать JSON:

rust
use axum::{
    routing::post,
    extract::Json,
    Router,
};
use serde::{Deserialize, Serialize};

// Определение структуры для десериализации входных данных
#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
    age: Option<u8>,
}

// Определение структуры для сериализации ответа
#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
    age: Option<u8>,
}

// Хендлер, принимающий JSON
async fn create_user(
    Json(payload): Json<CreateUser>,
) -> Json<User> {
    // Создание пользователя на основе входных данных
    let user = User {
        id: 42, // В реальном приложении ID генерируется или берется из БД
        name: payload.name,
        email: payload.email,
        age: payload.age,
    };
    
    // Возвращаем JSON-ответ
    Json(user)
}

// Регистрация маршрута
let app = Router::new()
    .route("/users", post(create_user));

В этом примере:

  1. Json(payload): Json<CreateUser> извлекает и десериализует данные из тела запроса
  2. Json(user) сериализует структуру User и возвращает ее в ответе

Отправка JSON-ответов

Axum предоставляет несколько способов вернуть JSON-ответ:

1. Возврат Json<T>

Самый простой способ — вернуть Json<T>, где T — тип, который будет сериализован в JSON:

rust
async fn get_user() -> Json<User> {
    let user = User {
        id: 42,
        name: "John Doe".to_string(),
        email: "john@example.com".to_string(),
        age: Some(30),
    };
    
    Json(user)
}

2. Возврат с явным статус-кодом

Для возврата JSON-ответа с определенным статус-кодом можно использовать кортеж:

rust
use axum::http::StatusCode;

async fn create_user(
    Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
    let user = User {
        id: 42,
        name: payload.name,
        email: payload.email,
        age: payload.age,
    };
    
    // Возвращаем статус 201 Created и JSON-ответ
    (StatusCode::CREATED, Json(user))
}

3. Использование IntoResponse

Для более сложных ответов можно использовать типаж IntoResponse:

rust
use axum::response::IntoResponse;

async fn get_user_with_headers(
    Path(id): Path<u64>,
) -> impl IntoResponse {
    let user = User {
        id,
        name: "John Doe".to_string(),
        email: "john@example.com".to_string(),
        age: Some(30),
    };
    
    // Ответ с заголовками и JSON
    (
        StatusCode::OK,
        [
            ("X-RateLimit-Remaining", "99"),
            ("X-Resource-ID", &id.to_string()),
        ],
        Json(user)
    )
}

Обработка ошибок при работе с JSON

При работе с JSON могут возникать различные ошибки: неверный формат JSON, несоответствие типов, отсутствующие обязательные поля и т.д. Axum предоставляет механизмы для обработки этих ошибок.

Автоматическая обработка ошибок

По умолчанию, если экстрактор Json<T> не может десериализовать входные данные, Axum вернет ответ с кодом 400 Bad Request.

rust
// При отправке невалидного JSON Axum автоматически вернет ошибку 400
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
    // Этот код выполнится только если JSON валидный и соответствует структуре CreateUser
    // ...
}

Ручная обработка ошибок

Для более детального контроля над ошибками можно получить Result от экстрактора JSON:

rust
use axum::extract::rejection::JsonRejection;

async fn create_user_with_error_handling(
    result: Result<Json<CreateUser>, JsonRejection>,
) -> impl IntoResponse {
    match result {
        Ok(Json(payload)) => {
            // Обработка валидных данных
            let user = User {
                id: 42,
                name: payload.name,
                email: payload.email,
                age: payload.age,
            };
            
            (StatusCode::CREATED, Json(user))
        },
        Err(err) => {
            // Различные типы ошибок JSON
            let (status, error_message) = match err {
                JsonRejection::JsonDataError(err) => {
                    // Ошибка соответствия типов или отсутствие обязательных полей
                    (StatusCode::BAD_REQUEST, format!("Неверные данные: {}", err))
                },
                JsonRejection::JsonSyntaxError(err) => {
                    // Ошибка синтаксиса JSON
                    (StatusCode::BAD_REQUEST, format!("Неверный синтаксис JSON: {}", err))
                },
                JsonRejection::MissingJsonContentType(err) => {
                    // Отсутствует заголовок Content-Type: application/json
                    (StatusCode::BAD_REQUEST, format!("Должен быть указан Content-Type: application/json: {}", err))
                },
                _ => (StatusCode::BAD_REQUEST, format!("Ошибка обработки JSON: {}", err)),
            };
            
            // Возвращаем JSON с информацией об ошибке
            let error_response = serde_json::json!({
                "error": error_message,
            });
            
            (status, Json(error_response))
        }
    }
}

Кастомный тип ошибки

Для более структурированной обработки ошибок можно создать собственный тип ошибки:

rust
use axum::{
    response::{IntoResponse, Response},
    http::StatusCode,
};
use serde_json::json;

// Определение кастомного типа ошибки
enum ApiError {
    JsonError(String),
    ValidationError(String),
    DatabaseError(String),
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, error_message) = match self {
            ApiError::JsonError(msg) => (StatusCode::BAD_REQUEST, msg),
            ApiError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg),
            ApiError::DatabaseError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
        };
        
        let body = Json(json!({
            "success": false,
            "error": error_message,
        }));
        
        (status, body).into_response()
    }
}

// Функция для преобразования JsonRejection в ApiError
fn handle_json_error(err: JsonRejection) -> ApiError {
    match err {
        JsonRejection::JsonDataError(err) => {
            ApiError::ValidationError(format!("Неверные данные: {}", err))
        },
        JsonRejection::JsonSyntaxError(err) => {
            ApiError::JsonError(format!("Неверный синтаксис JSON: {}", err))
        },
        _ => ApiError::JsonError(format!("Ошибка обработки JSON: {}", err)),
    }
}

// Использование в хендлере
async fn create_user(
    result: Result<Json<CreateUser>, JsonRejection>,
) -> Result<(StatusCode, Json<User>), ApiError> {
    let Json(payload) = result.map_err(handle_json_error)?;
    
    // Обработка данных...
    let user = User {
        id: 42,
        name: payload.name,
        email: payload.email,
        age: payload.age,
    };
    
    Ok((StatusCode::CREATED, Json(user)))
}

Валидация JSON-данных

Важной частью работы с JSON является валидация входных данных. Рассмотрим несколько подходов к валидации JSON.

Базовая валидация с помощью Serde

Serde позволяет задать некоторые базовые ограничения на поля:

rust
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUser {
    #[serde(default)]  // Использовать значение по умолчанию, если поле отсутствует
    name: String,
    
    #[serde(rename = "userEmail")]  // Переименование поля в JSON
    email: String,
    
    #[serde(skip_serializing_if = "Option::is_none")]  // Не включать в JSON, если None
    age: Option<u8>,
    
    #[serde(deserialize_with = "deserialize_role")]  // Кастомная десериализация
    role: UserRole,
}

// Кастомная функция десериализации для поля role
fn deserialize_role<'de, D>(deserializer: D) -> Result<UserRole, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let role_str = String::deserialize(deserializer)?;
    match role_str.as_str() {
        "admin" => Ok(UserRole::Admin),
        "user" => Ok(UserRole::User),
        _ => Err(serde::de::Error::custom("Invalid role")),
    }
}

Валидация с помощью библиотеки validator

Для более сложной валидации можно использовать крейт validator:

rust
use serde::Deserialize;
use validator::{Validate, ValidationError};

#[derive(Deserialize, Validate)]
struct CreateUser {
    #[validate(length(min = 2, max = 100, message = "Имя должно содержать от 2 до 100 символов"))]
    name: String,
    
    #[validate(email(message = "Некорректный email"))]
    email: String,
    
    #[validate(range(min = 18, max = 120, message = "Возраст должен быть от 18 до 120 лет"))]
    age: u8,
    
    #[validate(custom = "validate_password")]
    password: String,
}

// Кастомная функция валидации для пароля
fn validate_password(password: &str) -> Result<(), ValidationError> {
    if password.len() < 8 {
        let mut err = ValidationError::new("too_short");
        err.message = Some("Пароль должен содержать минимум 8 символов".into());
        return Err(err);
    }
    
    if !password.chars().any(|c| c.is_uppercase()) {
        let mut err = ValidationError::new("no_uppercase");
        err.message = Some("Пароль должен содержать хотя бы одну заглавную букву".into());
        return Err(err);
    }
    
    if !password.chars().any(|c| c.is_numeric()) {
        let mut err = ValidationError::new("no_digit");
        err.message = Some("Пароль должен содержать хотя бы одну цифру".into());
        return Err(err);
    }
    
    Ok(())
}

// Использование в хендлере
async fn create_user(
    Json(payload): Json<CreateUser>,
) -> Result<Json<User>, ApiError> {
    // Валидация входных данных
    if let Err(validation_errors) = payload.validate() {
        let error_message = validation_errors
            .field_errors()
            .iter()
            .map(|(field, errors)| {
                let message = errors[0].message.clone().unwrap_or_else(|| {
                    format!("Ошибка в поле {}", field)
                });
                format!("{}: {}", field, message)
            })
            .collect::<Vec<_>>()
            .join(", ");
        
        return Err(ApiError::ValidationError(error_message));
    }
    
    // Создание пользователя...
    let user = User {
        id: 42,
        name: payload.name,
        email: payload.email,
        age: Some(payload.age),
    };
    
    Ok(Json(user))
}

Работа с вложенными и сложными JSON-структурами

Serde позволяет легко работать со сложными JSON-структурами, включая вложенные объекты, массивы и т.д.

Вложенные объекты

rust
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct Address {
    street: String,
    city: String,
    country: String,
    postal_code: String,
}

#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
    address: Address,
    alternate_addresses: Option<Vec<Address>>,
}

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
    address: Address,
    alternate_addresses: Vec<Address>,
}

async fn create_user(
    Json(payload): Json<CreateUser>,
) -> Json<User> {
    let user = User {
        id: 42,
        name: payload.name,
        email: payload.email,
        address: payload.address,
        alternate_addresses: payload.alternate_addresses.unwrap_or_default(),
    };
    
    Json(user)
}

Динамический JSON с помощью serde_json::Value

Для случаев, когда структура JSON заранее неизвестна или может меняться, можно использовать serde_json::Value:

rust
use axum::{
    extract::Json,
    routing::post,
    Router,
};
use serde_json::{json, Value};

// Работа с динамическим JSON
async fn process_dynamic_json(
    Json(payload): Json<Value>,
) -> Json<Value> {
    // Доступ к полям с помощью индексации
    let name = payload["name"].as_str().unwrap_or("Unknown");
    let age = payload["age"].as_u64().unwrap_or(0);
    
    // Обработка данных...
    
    // Создание динамического ответа
    let response = json!({
        "message": format!("Привет, {}! Вам {} лет.", name, age),
        "received": payload,
        "timestamp": chrono::Utc::now().to_rfc3339(),
    });
    
    Json(response)
}

let app = Router::new()
    .route("/process", post(process_dynamic_json));

Использование HashMap для динамических полей

Еще один подход для работы с динамическими данными — использование HashMap:

rust
use std::collections::HashMap;
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct DynamicUser {
    name: String,
    email: String,
    #[serde(flatten)]  // Все дополнительные поля попадут в meta
    meta: HashMap<String, Value>,
}

#[derive(Serialize)]
struct ProcessedUser {
    id: u64,
    name: String,
    email: String,
    #[serde(flatten)]  // meta будет добавлена в корень JSON
    meta: HashMap<String, Value>,
}

async fn process_user(
    Json(payload): Json<DynamicUser>,
) -> Json<ProcessedUser> {
    let mut meta = payload.meta;
    
    // Дополнительная обработка meta-данных
    meta.insert("processed_at".to_string(), json!(chrono::Utc::now().to_rfc3339()));
    
    let user = ProcessedUser {
        id: 42,
        name: payload.name,
        email: payload.email,
        meta,
    };
    
    Json(user)
}

Оптимизация и производительность

При работе с JSON в высоконагруженных приложениях важно учитывать аспекты производительности.

Потоковая обработка больших JSON

Для больших JSON-данных можно использовать потоковую обработку:

rust
use axum::{
    extract::Request,
    response::IntoResponse,
    routing::post,
    Router,
};
use futures::{Stream, StreamExt, TryStreamExt};
use axum::body::{Body, HttpBody};
use serde_json::Value;
use std::pin::Pin;

async fn stream_json(
    request: Request,
) -> impl IntoResponse {
    let body_stream = request.into_body();
    
    // Преобразуем тело запроса в поток байтов
    let bytes_stream = body_stream.into_data_stream();
    
    // Здесь можно обрабатывать поток по частям
    // Например, для больших JSON массивов
    
    "Обработка завершена"
}

Предварительная проверка JSON с ограничением размера

Для защиты от слишком больших запросов можно использовать middleware для ограничения размера:

rust
use tower_http::limit::RequestBodyLimitLayer;

let app = Router::new()
    .route("/users", post(create_user))
    // Ограничение размера тела запроса (10 МБ)
    .layer(RequestBodyLimitLayer::new(10 * 1024 * 1024));

Примеры типичных задач

API для CRUD-операций

rust
use axum::{
    routing::{get, post, put, delete},
    extract::{Json, Path, State},
    http::StatusCode,
    response::IntoResponse,
    Router,
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;

// Модели данных
#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}

#[derive(Deserialize)]
struct UpdateUser {
    name: Option<String>,
    email: Option<String>,
}

#[derive(Serialize, Clone)]
struct User {
    id: u64,
    name: String,
    email: String,
}

// Хранилище данных (в реальном приложении это была бы база данных)
struct AppState {
    users: Mutex<HashMap<u64, User>>,
    next_id: Mutex<u64>,
}

// Хендлеры
async fn list_users(
    State(state): State<Arc<AppState>>,
) -> Json<Vec<User>> {
    let users = state.users.lock().unwrap();
    let users_list: Vec<User> = users.values().cloned().collect();
    Json(users_list)
}

async fn get_user(
    Path(id): Path<u64>,
    State(state): State<Arc<AppState>>,
) -> Result<Json<User>, StatusCode> {
    let users = state.users.lock().unwrap();
    
    users.get(&id)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

async fn create_user(
    State(state): State<Arc<AppState>>,
    Json(payload): Json<CreateUser>,
) -> impl IntoResponse {
    let mut next_id = state.next_id.lock().unwrap();
    let id = *next_id;
    *next_id += 1;
    
    let user = User {
        id,
        name: payload.name,
        email: payload.email,
    };
    
    state.users.lock().unwrap().insert(id, user.clone());
    
    (StatusCode::CREATED, Json(user))
}

async fn update_user(
    Path(id): Path<u64>,
    State(state): State<Arc<AppState>>,
    Json(payload): Json<UpdateUser>,
) -> Result<Json<User>, StatusCode> {
    let mut users = state.users.lock().unwrap();
    
    let user = users.get_mut(&id).ok_or(StatusCode::NOT_FOUND)?;
    
    if let Some(name) = payload.name {
        user.name = name;
    }
    
    if let Some(email) = payload.email {
        user.email = email;
    }
    
    Ok(Json(user.clone()))
}

async fn delete_user(
    Path(id): Path<u64>,
    State(state): State<Arc<AppState>>,
) -> StatusCode {
    let mut users = state.users.lock().unwrap();
    
    if users.remove(&id).is_some() {
        StatusCode::NO_CONTENT
    } else {
        StatusCode::NOT_FOUND
    }
}

// Создание маршрутов
fn create_app() -> Router {
    let state = Arc::new(AppState {
        users: Mutex::new(HashMap::new()),
        next_id: Mutex::new(1),
    });
    
    Router::new()
        .route("/users", get(list_users).post(create_user))
        .route("/users/:id", get(get_user).put(update_user).delete(delete_user))
        .with_state(state)
}

Пагинация и фильтрация

rust
use axum::{
    extract::{Json, Query, State},
    routing::get,
    Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Deserialize)]
struct Pagination {
    page: Option<u32>,
    per_page: Option<u32>,
    sort_by: Option<String>,
    order: Option<String>,
    #[serde(flatten)]
    filters: HashMap<String, String>,
}

#[derive(Serialize)]
struct PaginatedResponse<T> {
    data: Vec<T>,
    page: u32,
    per_page: u32,
    total: u64,
    total_pages: u32,
}

async fn list_users(
    Query(params): Query<Pagination>,
    State(state): State<AppState>,
) -> Json<PaginatedResponse<User>> {
    let page = params.page.unwrap_or(1);
    let per_page = params.per_page.unwrap_or(10);
    let sort_by = params.sort_by.unwrap_or_else(|| "id".to_string());
    let order = params.order.unwrap_or_else(|| "asc".to_string());
    
    // Применение фильтров
    let mut users = state.users.lock().unwrap().values().cloned().collect::<Vec<_>>();
    
    // Пример применения фильтра по имени
    if let Some(name_filter) = params.filters.get("name") {
        users.retain(|user| user.name.contains(name_filter));
    }
    
    // Сортировка
    match sort_by.as_str() {
        "name" => {
            users.sort_by(|a, b| {
                if order == "asc" {
                    a.name.cmp(&b.name)
                } else {
                    b.name.cmp(&a.name)
                }
            });
        },
        _ => {
            users.sort_by(|a, b| {
                if order == "asc" {
                    a.id.cmp(&b.id)
                } else {
                    b.id.cmp(&a.id)
                }
            });
        }
    }
    
    // Пагинация
    let total = users.len() as u64;
    let total_pages = ((total as f64) / (per_page as f64)).ceil() as u32;
    
    let start = ((page - 1) * per_page) as usize;
    let end = (start + per_page as usize).min(users.len());
    
    let paginated_users = if start < users.len() {
        users[start..end].to_vec()
    } else {
        vec![]
    };
    
    Json(PaginatedResponse {
        data: paginated_users,
        page,
        per_page,
        total,
        total_pages,
    })
}

Заключение

Axum предлагает мощные и гибкие инструменты для работы с JSON, обеспечивая типобезопасность, производительность и хорошую обработку ошибок. Комбинируя экстракторы, валидацию и сериализацию, можно создавать надежные и эффективные API.

Основные рекомендации при работе с JSON в Axum:

  1. Используйте строго типизированные структуры для входных и выходных данных
  2. Применяйте валидацию для проверки входных данных
  3. Обеспечьте качественную обработку ошибок с информативными сообщениями
  4. Для сложных или динамических данных используйте serde_json::Value или HashMap
  5. При необходимости применяйте ограничения размера запросов и другие меры безопасности

В следующих разделах мы рассмотрим другие аспекты обработки данных в Axum, включая работу с query-параметрами, параметрами пути и формами.