Главная / Курсы / Rust: бэкенд на Axum / Глава 1. Введение в Axum / Сериализация и десериализация JSON
# Глава 1.2. Сериализация и десериализация JSON Практически каждый хендлер в API разбирает JSON на входе и собирает его на выходе. В экосистеме Rust стандартом для сериализации и десериализации является `serde`. Сериализация — это преобразование данных из Rust во внешний формат, например в JSON. Десериализация — обратный процесс, из JSON в Rust. Сам `serde` не умеет работать ни с JSON, ни с другими форматами. Он задаёт общий интерфейс, а конкретные форматы реализуются отдельными крейтами: `serde_json`, `toml`, `rmp-serde` и другими. Благодаря этому одну структуру можно использовать с разными форматами. Добавьте новые зависимости в `Cargo.toml`: ```bash cargo add serde --features derive cargo add serde_json ``` Фича `derive` позволяет указывать трейты serde в атрибуте `#[derive]`, чтобы компилятор сам генерировал их реализацию для наших типов. ## Основные трейты У serde два трейта: `Serialize` для сериализации и `Deserialize` для десериализации. Вручную их обычно не реализуют, а используют макрос `derive`: ```rust use serde::{Deserialize, Serialize}; // генерирует реализацию трейтов на этапе компиляции #[derive(Serialize, Deserialize)] struct User { id: i64, name: String, } // Rust-значение в JSON let json = serde_json::to_string(&user)?; // JSON в Rust-значение let user: User = serde_json::from_str(&json)?; ``` ## Основные атрибуты По умолчанию serde работает по двум правилам: все поля обязательные (если поля нет, будет ошибка), а лишние поля молча игнорируются. Большинство атрибутов нужны, чтобы изменить это поведение. ### Переименование полей Атрибут `rename_all` переименовывает все поля в нужный формат, а `rename` — одно поле: ```rust #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct User { first_name: String, #[serde(rename = "type")] kind: String, } // {"firstName":"...","type":"..."} ``` Кроме `camelCase`, есть `PascalCase`, `kebab-case`, `SCREAMING_SNAKE_CASE` и другие. ### Необязательные поля Если поле может отсутствовать, проще всего сделать его типом `Option<T>`: отсутствующее поле и `null` одинаково дают `None`. Атрибут `default` вместо этого подставляет значение по умолчанию из трейта `Default` или из вашей функции. А атрибут `skip_serializing_if` не выводит поле, если выполняется условие. Без него поле со значением `None` попадёт в JSON как `"email": null`. ```rust #[derive(Serialize, Deserialize)] struct User { name: String, #[serde(default)] is_admin: bool, // false #[serde(default = "default_role")] role: String, // "user" #[serde(skip_serializing_if = "Option::is_none")] email: Option<String>, } fn default_role() -> String { "user".to_string() } // на входе достаточно {"name":"Alice"} // на выходе {"name":"Alice","is_admin":false,"role":"user"} ``` ### Пропуск полей Атрибут `skip` полностью исключает поле из сериализации и десериализации. При десериализации вместо значения используется `Default::default()`. Это удобно для внутренних полей, которые не должны передаваться через API: ```rust #[derive(Serialize, Deserialize)] struct User { name: String, #[serde(skip)] password_hash: String, } ``` ### Строгая проверка Из-за того, что лишние поля игнорируются, опечатка клиента проходит незамеченной. Атрибут `deny_unknown_fields` превращает неизвестное поле в ошибку: ```rust #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct UpdateUser { name: Option<String>, } // {"nmae":"Bob"} // без атрибута: name = None, // обновление ничего не сделало, ответ 200 OK // с атрибутом: ошибка unknown field `nmae` ``` ### Вложенные структуры Атрибут `flatten` помещает поля вложенной структуры на один уровень с остальными: ```rust #[derive(Serialize, Deserialize)] struct User { name: String, #[serde(flatten)] metadata: Metadata, } #[derive(Serialize, Deserialize)] struct Metadata { created_at: String, } // {"name":"Alice","created_at":"2026-09-24"} ``` ### Перечисления Простые перечисления, то есть перечисления без полей, сериализуются в строку с названием варианта. Это удобно, например, для статусов заказа. Чтобы название было в привычном для JSON виде, добавьте `rename_all`: ```rust #[derive(Serialize, Deserialize)] #[serde(rename_all = "snake_case")] enum OrderStatus { New, // "new" InProgress, // "in_progress" Delivered, // "delivered" } ``` С перечислениями, у вариантов которых есть поля, всё сложнее. Например, событие в интерфейсе может быть кликом или нажатием клавиши: ```rust #[derive(Serialize, Deserialize)] enum Event { Click { x: i32, y: i32 }, KeyPress { key: String }, } // {"Click":{"x":10,"y":20}} // {"KeyPress":{"key":"Enter"}} ``` По умолчанию название варианта становится ключом объекта, а поля оказываются внутри. Клиенту такой формат разбирать неудобно: чтобы понять, что за событие пришло, приходится проверять, какой из возможных ключей есть в объекте. Обычно в API тип передают в отдельном поле, например `type`. Для этого служит атрибут `tag`: ```rust #[derive(Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] enum Event { Click { x: i32, y: i32 }, KeyPress { key: String }, } // {"type":"click","x":10,"y":20} // {"type":"key_press","key":"Enter"} ``` Теперь у всех событий есть поле `type`, по которому клиент сразу понимает, как разбирать остальные поля. Десериализация работает так же: serde выбирает вариант перечисления по значению поля `type`. ## `serde_json::Value` и `json!` Когда структура JSON заранее неизвестна, используют `serde_json::Value` — тип, представляющий произвольное JSON-значение: ```rust let value: Value = serde_json::from_str(r#"{"name": "Alice"}"#)?; println!("{}", value["name"]); // "Alice" — вместе с кавычками value["name"].as_str(); // Some("Alice") ``` При обращении по несуществующему ключу `Value` вернёт `null`: ```rust println!("{}", value["age"]); // null ``` Макрос `json!` позволяет создавать `Value` из JSON-подобного литерала. Внутри него можно использовать не только литералы, но и переменные: их значения сериализуются автоматически. Например, так можно собрать ответ с данными о работе сервера: ```rust use serde_json::json; fn count_tasks_and_workers() -> (usize, usize) { // дескриптор текущего рантайма Tokio let handle = tokio::runtime::Handle::current(); // количество рабочих потоков let workers = handle.metrics().num_workers(); // количество выполняемых задач на текущий момент let tasks = handle.metrics().num_alive_tasks(); (tasks, workers) } // количество выполняемых задач и рабочих потоков let (tasks, workers) = count_tasks_and_workers(); // формирование результирующего статуса let status = json!({ "status": "ok", "workers": workers, "tasks": tasks }); // {"status":"ok","tasks":1,"workers":12} ``` Обратите внимание, что ключи в результате отсортированы по алфавиту: по умолчанию `Value` хранит поля объекта в отсортированном порядке. Переменная внутри `json!` может быть любого типа, реализующего трейт `Serialize`: числом, строкой, вектором или вашей структурой с `#[derive(Serialize)]`. Похожую строку можно собрать и через `format!`, но `json!` надёжнее. Во-первых, ошибка в структуре JSON внутри `json!`, например пропущенное значение, обнаружится ещё при компиляции, а строку из `format!` никто не проверяет. Во-вторых, `json!` правильно экранирует значения. Если в строке окажутся кавычки, `format!` соберёт некорректный JSON, и ошибка проявится только у клиента, когда тот попытается его разобрать: ```rust let name = r#"Иван "Грозный""#; // {"name": "Иван "Грозный""} format!(r#"{{"name": "{}"}}"#, name); // {"name":"Иван \"Грозный\""} json!({ "name": name }); ``` Злоупотреблять `Value` не стоит. Если структура JSON известна заранее, лучше описать её отдельным типом. Так вы получите проверку структуры на этапе компиляции и удобное автодополнение, а не ошибки доступа к полям во время выполнения. ## JSON в Axum Функции `serde_json::to_string` и `serde_json::from_str` в хендлерах Axum вручную вызывать не нужно: Axum делает это сам с помощью обёртки `Json<T>`. Напомним, что хендлер — это асинхронная функция, которая обрабатывает запрос. Данные запроса она получает через аргументы, а ответ формирует из возвращаемого значения. Аргументы, которые Axum заполняет данными из запроса, называются экстракторами. Одни экстракторы достают данные из адреса запроса, другие — из заголовков, третьи — из тела. Все они подробно разобраны в главе 1.5, а здесь рассмотрим простой случай: данные приходят в теле запроса в формате JSON. ### Тело запроса Если указать `Json<T>` в аргументах хендлера, Axum прочитает тело запроса и десериализует его в тип `T`: ```rust use axum::Json; use serde::Deserialize; #[derive(Deserialize)] struct CreateUser { name: String, } async fn create_user(Json(user): Json<CreateUser>) -> String { format!("Получен пользователь с именем: {}", user.name) } ``` Запись `Json(user)` в аргументах — это деструктуризация: она сразу достаёт из обёртки значение типа `CreateUser`. Если тело не удалось разобрать, хендлер не вызывается, и Axum сам вернёт ошибку: - `415` — нет заголовка `content-type: application/json`. - `400` — синтаксическая ошибка в JSON. - `422` — JSON корректный, но поля не те или не того типа. В структурах запроса используйте `String`, а не `&str`, так как `Json<T>` требует тип, который ничего не заимствует из тела запроса. ### Тело ответа Если вернуть `Json<T>` из хендлера, Axum сериализует значение в JSON и выставит заголовок `content-type: application/json`: ```rust use axum::Json; use serde::Serialize; #[derive(Serialize)] struct UserResponse { id: i64, name: String, } async fn get_user() -> Json<UserResponse> { let user = UserResponse { id: 1, name: "Alice".to_string(), }; Json(user) } // {"id":1,"name":"Alice"} ``` Теперь мы умеем принимать и возвращать JSON. В следующей главе подробно разберём маршрутизацию: как описывать маршруты и как Axum выбирает хендлер для запроса.
Наша группа в telegram. Задавайте вопросы и общайтесь.
Задонатить. Поддержите развитие площадки.