Перейти к содержимому

JSON и Serde в Rust

JSON повсюду: файлы настроек, веб-API, сохранения в играх. В Rust для работы с ним почти все используют два крейта. serde (сокращение от serialize / deserialize — сериализация и десериализация) умеет превращать значения Rust в данные и обратно. serde_json учит его формату JSON.

Представь serde как переводчика. Ты один раз описываешь свои данные обычной структурой Rust, а serde сам переводит их в JSON и обратно. Тебе не нужно ни писать текст JSON вручную, ни разбирать его по символам.

Добавь в проект оба крейта. Фича derive позволяет serde самому написать код перевода:

Terminal window
cargo add serde --features derive
cargo add serde_json

Теперь в Cargo.toml есть:

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

Если Cargo.toml и cargo add тебе пока незнакомы, сначала прочитай Cargo и крейты. В Rust Playground оба крейта уже есть, поэтому все кнопки «Попробуй сам» на этой странице работают без подготовки.

Добавь к структуре #[derive(Serialize)], и serde_json::to_string сможет превратить её в текст JSON:

use serde::Serialize;
#[derive(Serialize)]
struct User {
name: String,
age: u32,
admin: bool,
}
fn main() {
let user = User { name: String::from("Аня"), age: 28, admin: false };
let compact = serde_json::to_string(&user).unwrap();
println!("{}", compact);
let pretty = serde_json::to_string_pretty(&user).unwrap();
println!("{}", pretty);
}

Вывод:

{"name":"Аня","age":28,"admin":false}
{
"name": "Аня",
"age": 28,
"admin": false
}
▶ Попробуй сам

to_string даёт компактный JSON — его удобно отправлять по сети. to_string_pretty добавляет переносы строк и пробелы — так приятнее для файлов, которые будут читать люди. Ключи идут в том же порядке, что и поля структуры. Кириллица попадает в JSON как есть, без экранирования.

Обратное превращение называется десериализацией. Добавь Deserialize в список derive и вызови serde_json::from_str. Какой тип тебе нужен, ты говоришь аннотацией типа:

use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
name: String,
age: u32,
admin: bool,
}
fn main() -> Result<(), serde_json::Error> {
let text = r#"{"name": "Аня", "age": 28, "admin": false}"#;
let user: User = serde_json::from_str(text)?;
println!("{:?}", user);
println!("{}: {} лет", user.name, user.age);
Ok(())
}

Вывод:

User { name: "Аня", age: 28, admin: false }
Аня: 28 лет
▶ Попробуй сам

Обрати внимание на две вещи:

  • r#"..."# — это сырая строка (raw string). Внутри неё можно свободно писать ", что очень удобно для JSON.
  • from_str возвращает Result, потому что текст может оказаться некорректным JSON. Здесь main возвращает Result, поэтому можно передать любую ошибку наверх через ?.

Настоящий JSON часто не такой, как ты ожидаешь. Поле отсутствует, или число пришло текстом. serde всё проверяет и вместо падения возвращает понятную ошибку. Обработай её через match:

use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct User {
name: String,
age: u32,
}
fn main() {
let inputs = [
r#"{"name": "Ana", "age": 28}"#,
r#"{"name": "Ben"}"#,
r#"{"name": "Cy", "age": "old"}"#,
];
for text in inputs {
match serde_json::from_str::<User>(text) {
Ok(user) => println!("OK: {:?}", user),
Err(e) => println!("Ошибка: {}", e),
}
}
}

Вывод:

OK: User { name: "Ana", age: 28 }
Ошибка: missing field `age` at line 1 column 15
Ошибка: invalid type: string "old", expected u32 at line 1 column 27
▶ Попробуй сам

Сообщения об ошибках serde пишет по-английски: «missing field age» — нет поля age, «invalid type: string “old”, expected u32» — пришла строка, а ожидалось число u32. Ещё serde указывает строку (line) и столбец (column), где нашёл проблему.

Часть ::<User> называют «турборыбой» (turbofish). Это ещё один способ указать нужный тип, полезный, когда нет let user: User, куда можно поставить аннотацию.

А если поле действительно необязательное? Есть два хороших варианта:

  • Сделай поле Option. Отсутствующий ключ (или null) превратится в None.
  • Добавь #[serde(default)]. Отсутствующий ключ получит значение типа по умолчанию: 0, false, пустую String или пустой Vec.
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Profile {
name: String,
bio: Option<String>,
#[serde(default)]
followers: u32,
#[serde(default)]
tags: Vec<String>,
}
fn main() {
let full = r#"{"name": "Аня", "bio": "Обожает Rust", "followers": 120, "tags": ["rust"]}"#;
let short = r#"{"name": "Бен"}"#;
let a: Profile = serde_json::from_str(full).unwrap();
let b: Profile = serde_json::from_str(short).unwrap();
println!("{:?}", a);
println!("{:?}", b);
}

Вывод:

Profile { name: "Аня", bio: Some("Обожает Rust"), followers: 120, tags: ["rust"] }
Profile { name: "Бен", bio: None, followers: 0, tags: [] }
▶ Попробуй сам

Используй Option, когда «нет значения» означает что-то другое, чем «ноль» или «пусто». Используй default, когда значения по умолчанию достаточно.

В Rust имена полей пишут в snake_case, а многие веб-API используют camelCase, а некоторые — имена, которые в Rust вообще недопустимы (например, type). Отказываться от красивых имён в Rust не нужно. Используй rename_all для всей структуры и rename для отдельного поля:

use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct UserProfile {
user_id: u64,
display_name: String,
is_verified: bool,
#[serde(rename = "type")]
account_type: String,
}
fn main() {
// такой ответ мог бы прислать веб-API
let response = r#"{
"userId": 42,
"displayName": "Аня Смирнова",
"isVerified": true,
"type": "premium"
}"#;
let profile: UserProfile = serde_json::from_str(response).unwrap();
println!("{} (id {}): аккаунт {}", profile.display_name, profile.user_id, profile.account_type);
// при сериализации используются те же имена, так что JSON совпадает туда и обратно
println!("{}", serde_json::to_string(&profile).unwrap());
}

Вывод:

Аня Смирнова (id 42): аккаунт premium
{"userId":42,"displayName":"Аня Смирнова","isVerified":true,"type":"premium"}
▶ Попробуй сам

По умолчанию serde просто игнорирует ключи JSON, которых нет в твоей структуре. Так что описывать нужно только те поля, которые тебе важны, даже если API присылает пятьдесят.

Массив JSON превращается в Vec. Структуры могут содержать другие структуры и векторы, так что глубоко вложенный JSON описывается просто вложением типов:

use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Task {
title: String,
done: bool,
}
fn main() {
let text = r#"[
{"title": "Изучить serde", "done": true},
{"title": "Написать файл настроек", "done": false},
{"title": "Вызвать API", "done": false}
]"#;
let tasks: Vec<Task> = serde_json::from_str(text).unwrap();
for task in &tasks {
let mark = if task.done { "x" } else { " " };
println!("[{}] {}", mark, task.title);
}
let left = tasks.iter().filter(|t| !t.done).count();
println!("Осталось задач: {}", left);
}

Вывод:

[x] Изучить serde
[ ] Написать файл настроек
[ ] Вызвать API
Осталось задач: 2
▶ Попробуй сам

Иногда форма JSON заранее неизвестна, или тебе нужен только один маленький кусочек. Для этого есть serde_json::Value. В нём может лежать любой JSON: объект, массив, строка, число, bool или null. Макрос json! собирает Value с синтаксисом, похожим на JSON:

use serde_json::{json, Value};
fn main() {
let data: Value = json!({
"city": "Лиссабон",
"temps": [18, 21, 19],
"sunny": true
});
println!("Город: {}", data["city"]);
println!("Первая температура: {}", data["temps"][0]);
// as_str() даёт обычный &str вместо строки JSON
if let Some(city) = data["city"].as_str() {
println!("Просто текст: {}", city);
}
// отсутствующий ключ даёт Null, а не падение
println!("Ветер: {}", data["wind"]);
}

Вывод:

Город: "Лиссабон"
Первая температура: 18
Просто текст: Лиссабон
Ветер: null
▶ Попробуй сам

Заметь, что data["city"] выводится в кавычках: это всё ещё строка JSON. Чтобы получить обычные значения Rust, используй .as_str(), .as_i64() или .as_bool(). Каждый из них возвращает Option, потому что значение может оказаться другого типа.

Value гибкий, но с ним ты теряешь помощь компилятора. Если форма данных известна, лучше бери структуру.

Вот пример из жизни: сохраняем настройки приложения в config.json и загружаем их снова. Здесь эта страница соединяется с уроком Работа с файлами. std::fs::write сохраняет текст, а std::fs::read_to_string загружает его обратно:

use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fs;
#[derive(Serialize, Deserialize, Debug)]
struct Config {
username: String,
theme: String,
font_size: u32,
}
fn main() -> Result<(), Box<dyn Error>> {
let config = Config {
username: String::from("ana"),
theme: String::from("dark"),
font_size: 14,
};
// сохраняем
let text = serde_json::to_string_pretty(&config)?;
fs::write("config.json", text)?;
// загружаем
let loaded_text = fs::read_to_string("config.json")?;
let loaded: Config = serde_json::from_str(&loaded_text)?;
println!("Загружено: {:?}", loaded);
Ok(())
}

Вывод:

Загружено: Config { username: "ana", theme: "dark", font_size: 14 }
▶ Попробуй сам

Почему Box<dyn Error>? Этот main может завершиться ошибкой двумя разными способами: ошибкой файла (std::io::Error) или ошибкой JSON (serde_json::Error). Box<dyn Error> значит «ошибка любого вида», поэтому ? работает для обеих. Подробнее об этом — на странице Свои типы ошибок.

Забыл derive Deserialize. Если структура выводит только Serialize, писать JSON можно, а читать — нет. Ошибка длинная, но важные строки такие:

error[E0277]: the trait bound `User: serde::Deserialize<'de>` is not satisfied
|
10 | let user: User = serde_json::from_str(r#"{"name":"Ana","age":28}"#).unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
|
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `User` type

Компилятор говорит, что User не реализует serde::Deserialize, и советует добавить #[derive(serde::Deserialize)]. Исправление: #[derive(Serialize, Deserialize)]. А если видишь «cannot find derive macro Serialize», значит, ты добавил serde без фичи derive. Выполни cargo add serde --features derive.

.unwrap() на данных, которые ты не контролируешь. В маленьких примерах это нормально, но JSON от пользователя или с сайта может быть сломан. Плохой файл обрушит программу с паникой. Используй ? или match, как показано выше.

Ждёшь, что data["name"] выведется без кавычек. Value выводится как JSON. Вызови .as_str(), чтобы получить обычный текст.

Создай структуру Item с полями name (String) и qty (u32). Положи два товара в Vec, преврати весь вектор в JSON одним вызовом и выведи результат.

Подсказка: #[derive(Serialize)] нужен только структуре. Vec из сериализуемых значений тоже сериализуемый и превращается в массив JSON.

Показать решение
use serde::Serialize;
#[derive(Serialize)]
struct Item {
name: String,
qty: u32,
}
fn main() {
let list = vec![
Item { name: String::from("яблоки"), qty: 3 },
Item { name: String::from("хлеб"), qty: 1 },
];
let json = serde_json::to_string(&list).unwrap();
println!("{}", json);
}

Вывод:

[{"name":"яблоки","qty":3},{"name":"хлеб","qty":1}]
▶ Попробуй сам

serde уже умеет обращаться с Vec, String и числами, так что достаточно вывести Serialize для Item. Попробуй to_string_pretty, чтобы увидеть тот же список на нескольких строках.

Прочитай этот JSON в структуру Settings: {"siteTitle": "Мой блог", "postsPerPage": 5}. В структуре должны быть site_title, posts_per_page и theme, которого может не быть. Потом прочитай ещё {"siteTitle": "Заметки"}, где posts_per_page должно стать 0. Выведи обе структуры через {:?}.

Подсказка: нужны #[serde(rename_all = "camelCase")] на структуре, Option<String> для theme и #[serde(default)] на posts_per_page.

Показать решение
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Settings {
site_title: String,
#[serde(default)]
posts_per_page: u32,
theme: Option<String>,
}
fn main() {
let full = r#"{"siteTitle": "Мой блог", "postsPerPage": 5}"#;
let short = r#"{"siteTitle": "Заметки"}"#;
let a: Settings = serde_json::from_str(full).unwrap();
let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a);
println!("{:?}", b);
}

Вывод:

Settings { site_title: "Мой блог", posts_per_page: 5, theme: None }
Settings { site_title: "Заметки", posts_per_page: 0, theme: None }
▶ Попробуй сам

rename_all сопоставляет siteTitle с site_title, а postsPerPage — с posts_per_page. Ни в одном JSON нет theme, поэтому оба раза там None, а отсутствующий во втором JSON postsPerPage становится 0. Убери #[serde(default)] — и второй unwrap() запаникует с сообщением «missing field postsPerPage». Здесь .unwrap() допустим, потому что JSON мы написали сами; для настоящего ввода используй ? или match.

  • Добавь serde (с --features derive) и serde_json, а потом #[derive(Serialize, Deserialize)] на свои структуры.
  • serde_json::to_string / to_string_pretty превращают значение в JSON; serde_json::from_str превращает JSON в значение.
  • from_str возвращает Result. Для настоящего ввода обрабатывай его через ? или match, а не .unwrap().
  • Для ключей, которых может не быть, используй Option или #[serde(default)], а чтобы совпасть с именами в JSON — rename / rename_all.
  • Если форма данных неизвестна, бери serde_json::Value и json!. Если известна — лучше структуры.

Проверь себя

1. Какой derive позволяет превратить текст JSON в твою структуру?

2. В объекте JSON может не оказаться ключа bio. Какой тип хорошо подходит для этого поля?

3. В структуре есть поле display_name, а в JSON используется displayName. Какое исправление самое простое для всей структуры?

4. Что serde по умолчанию делает с ключами JSON, которых нет в структуре?

Результат: 0 / 4

  • serde.rs — официальное руководство по Serde (на английском): как устроен serde, с примерами для многих форматов данных.
  • Container attributes — serde.rs (на английском): настройки для всей структуры или перечисления, например rename_all и deny_unknown_fields.
  • Field attributes — serde.rs (на английском): настройки отдельных полей, например rename, default, skip и skip_serializing_if.
  • serde_json — docs.rs (на английском): главная страница крейта с примерами типизированного и нетипизированного JSON и макроса json!.
  • serde_json::Value — docs.rs (на английском): все методы для чтения нетипизированного JSON, например as_str, as_i64 и get.