Введение: почему JSON важен в Rust?
JSON (JavaScript Object Notation) остаётся одним из самых популярных форматов для обмена данными между сервисами, конфигурационными файлами и API. В экосистеме Rust работа с JSON реализована через мощный крейт serde_json, который в паре с фреймворком serde предоставляет элегантный и безопасный способ преобразования структур данных.
В отличие от динамических языков, Rust требует явного описания типов, что на первый взгляд может показаться сложным. Однако на практике это даёт невероятную надёжность: вы никогда не получите неожиданный null или поле с неверным типом во время выполнения. В этой статье мы разберём парсинг (десериализацию) и сериализацию JSON на конкретных примерах, которые можно сразу применить в реальных проектах.
1. Настройка проекта и установка зависимостей
Прежде чем писать код, добавим необходимые крейты в Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Крейт serde с флагом derive позволяет автоматически генерировать код для сериализации и десериализации наших структур. serde_json отвечает непосредственно за преобразование в/из JSON.
Импортируем всё необходимое в main.rs:
use serde::{Deserialize, Serialize};
use serde_json::Result;
2. Сериализация: превращаем Rust-структуру в JSON
Сериализация — это процесс преобразования объекта Rust в строку JSON. Рассмотрим классический пример с данными пользователя.
Объявление структуры
Добавим аннотации #[derive(Serialize, Deserialize)] к нашей структуре:
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
email: String,
is_active: bool,
tags: Vec<String>,
}
Создание экземпляра и сериализация
Теперь создадим объект и преобразуем его в JSON:
fn main() -> Result<()> {
let user = User {
id: 42,
name: "Алексей".to_string(),
email: "alex@example.com".to_string(),
is_active: true,
tags: vec!["rust".to_string(), "backend".to_string()],
};
// Сериализация в строку JSON
let json_string = serde_json::to_string(&user)?;
println!("{}", json_string);
// Красивая (pretty) сериализация
let pretty_json = serde_json::to_string_pretty(&user)?;
println!("{}", pretty_json);
Ok(())
}
Результат выполнения:
{"id":42,"name":"Алексей","email":"alex@example.com","is_active":true,"tags":["rust","backend"]}
{
"id": 42,
"name": "Алексей",
"email": "alex@example.com",
"is_active": true,
"tags": [
"rust",
"backend"
]
}
Обратите внимание: to_string_pretty добавляет отступы и переносы строк — идеально для конфигурационных файлов или логов.
3. Парсинг (десериализация): из JSON в Rust-структуру
Десериализация — обратный процесс: мы получаем строку JSON и преобразуем её в типизированную структуру Rust.
Парсинг из строки
fn parse_user_from_json(json_str: &str) -> Result<User> {
let user: User = serde_json::from_str(json_str)?;
Ok(user)
}
fn main() -> Result<()> {
let data = r#"
{
"id": 101,
"name": "Мария",
"email": "maria@example.com",
"is_active": false,
"tags": ["python", "data-science"]
}
"#;
let user = parse_user_from_json(data)?;
println!("Пользователь: {:?}", user);
println!("Имя: {}", user.name);
println!("Активен: {}", user.is_active);
Ok(())
}
Если JSON не соответствует структуре (например, отсутствует обязательное поле или неверный тип), программа вернёт ошибку на этапе компиляции или выполнения — это главное преимущество Rust перед динамическими языками.
Работа с неполными данными: Option и значения по умолчанию
В реальных API не все поля могут присутствовать. Используем Option и атрибут #[serde(default)]:
#[derive(Debug, Serialize, Deserialize)]
struct Config {
host: String,
port: u16,
#[serde(default = "default_timeout")]
timeout_secs: u64,
#[serde(default)]
debug: bool,
}
fn default_timeout() -> u64 {
30
}
fn main() -> Result<()> {
let json = r#"{"host": "localhost", "port": 8080}"#;
let config: Config = serde_json::from_str(json)?;
println!("{:?}", config);
// Config { host: "localhost", port: 8080, timeout_secs: 30, debug: false }
Ok(())
}
Парсинг неструктурированного JSON (Value)
Иногда структура JSON заранее неизвестна. Для таких случаев serde_json предоставляет тип Value — аналог словаря:
use serde_json::Value;
fn process_dynamic_json(json_str: &str) -> Result<()> {
let v: Value = serde_json::from_str(json_str)?;
// Доступ к полям через индексацию
if let Some(name) = v.get("name").and_then(|n| n.as_str()) {
println!("Name: {}", name);
}
// Проверка типа
if v.get("age").map_or(false, |a| a.is_number()) {
println!("Age field exists and is a number");
}
// Преобразование в строку
println!("Raw: {}", v.to_string());
Ok(())
}
Value — это перечисление, которое может быть Null, Bool, Number, String, Array или Object. Это удобно для прототипирования, но на продакшене лучше использовать строго типизированные структуры.
4. Продвинутые техники: кастомные имена полей и вложенные структуры
Изменение имени поля через rename
Если JSON использует snake_case