JSON и Serde в Rust
JSON повсюду: файлы настроек, веб-API, сохранения в играх. В Rust для работы с ним почти все используют два крейта. serde (сокращение от serialize / deserialize — сериализация и десериализация) умеет превращать значения Rust в данные и обратно. serde_json учит его формату JSON.
Представь serde как переводчика. Ты один раз описываешь свои данные обычной структурой Rust, а serde сам переводит их в JSON и обратно. Тебе не нужно ни писать текст JSON вручную, ни разбирать его по символам.
Подготовка
Заголовок раздела «Подготовка»Добавь в проект оба крейта. Фича derive позволяет serde самому написать код перевода:
cargo add serde --features derivecargo add serde_jsonТеперь в Cargo.toml есть:
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"Если Cargo.toml и cargo add тебе пока незнакомы, сначала прочитай Cargo и крейты. В Rust Playground оба крейта уже есть, поэтому все кнопки «Попробуй сам» на этой странице работают без подготовки.
Из структуры в JSON
Заголовок раздела «Из структуры в JSON»Добавь к структуре #[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 как есть, без экранирования.
Из JSON в структуру
Заголовок раздела «Из 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 неправильный
Заголовок раздела «Когда JSON неправильный»Настоящий 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 и #[serde(default)]
Заголовок раздела «Отсутствующие ключи: Option и #[serde(default)]»А если поле действительно необязательное? Есть два хороших варианта:
- Сделай поле
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 присылает пятьдесят.
Списки: Vec из структур
Заголовок раздела «Списки: Vec из структур»Массив 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Осталось задач: 2JSON без типов: Value и json!
Заголовок раздела «JSON без типов: Value и json!»Иногда форма 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 гибкий, но с ним ты теряешь помощь компилятора. Если форма данных известна, лучше бери структуру.
Читаем и пишем JSON-файл
Заголовок раздела «Читаем и пишем JSON-файл»Вот пример из жизни: сохраняем настройки приложения в 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(), чтобы получить обычный текст.
Практика
Заголовок раздела «Практика»1. Сохраняем список покупок
Заголовок раздела «1. Сохраняем список покупок»Создай структуру 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, чтобы увидеть тот же список на нескольких строках.
2. Настройки со значениями по умолчанию
Заголовок раздела «2. Настройки со значениями по умолчанию»Прочитай этот 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.