Работа с JSON
В современных веб-приложениях и API формат JSON является стандартом для обмена данными. Axum предоставляет мощные и гибкие инструменты для работы с JSON как для приема данных от клиентов, так и для отправки ответов. В этом разделе мы рассмотрим различные аспекты работы с JSON в Axum, включая сериализацию, десериализацию, валидацию и обработку ошибок.
Основы работы с JSON
Axum использует крейт serde_json
для сериализации и десериализации JSON. Для работы с JSON необходимо добавить следующие зависимости в Cargo.toml
:
[dependencies]
axum = "0.7.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
JSON экстрактор
Для извлечения JSON из тела запроса Axum предоставляет экстрактор Json<T>
, где T
— тип данных, в который необходимо десериализовать JSON:
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));
В этом примере:
Json(payload): Json<CreateUser>
извлекает и десериализует данные из тела запросаJson(user)
сериализует структуруUser
и возвращает ее в ответе
Отправка JSON-ответов
Axum предоставляет несколько способов вернуть JSON-ответ:
1. Возврат Json<T>
Самый простой способ — вернуть Json<T>
, где T
— тип, который будет сериализован в JSON:
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-ответа с определенным статус-кодом можно использовать кортеж:
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
:
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.
// При отправке невалидного JSON Axum автоматически вернет ошибку 400
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
// Этот код выполнится только если JSON валидный и соответствует структуре CreateUser
// ...
}
Ручная обработка ошибок
Для более детального контроля над ошибками можно получить Result
от экстрактора JSON:
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))
}
}
}
Кастомный тип ошибки
Для более структурированной обработки ошибок можно создать собственный тип ошибки:
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 позволяет задать некоторые базовые ограничения на поля:
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
:
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-структурами, включая вложенные объекты, массивы и т.д.
Вложенные объекты
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
:
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
:
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-данных можно использовать потоковую обработку:
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 для ограничения размера:
use tower_http::limit::RequestBodyLimitLayer;
let app = Router::new()
.route("/users", post(create_user))
// Ограничение размера тела запроса (10 МБ)
.layer(RequestBodyLimitLayer::new(10 * 1024 * 1024));
Примеры типичных задач
API для CRUD-операций
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)
}
Пагинация и фильтрация
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:
- Используйте строго типизированные структуры для входных и выходных данных
- Применяйте валидацию для проверки входных данных
- Обеспечьте качественную обработку ошибок с информативными сообщениями
- Для сложных или динамических данных используйте
serde_json::Value
илиHashMap
- При необходимости применяйте ограничения размера запросов и другие меры безопасности
В следующих разделах мы рассмотрим другие аспекты обработки данных в Axum, включая работу с query-параметрами, параметрами пути и формами.