Работа с JSON в Rust: парсинг и сериализация на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему 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

Блоги

Рекомендаций по книгам