Привет Хабр! Меня зовут Алексей, я разработчик Группы "Иннотех" холдинга Т1.

Цель статьи - познакомить читателя с библиотеками для работы с JSON в Rust. Если вы никогда не парсили JSON на языке Rust и ищите с чего начать, то эта статья для вас!

В статье будут разобраны примеры работы со строками и файлами, познакомимся с библиотеками serde и serde_json

Предполагается, что читатель знаком с синтаксисом языка Rust

1. Создаем проект

mascai@MacBook-Air-Aleksei 16_rust_parser % cargo new json_habr
     Created binary (application) `json_habr` package

mascai@MacBook-Air-Aleksei 16_rust_parser % tree json_habr 
json_habr
├── Cargo.toml
└── src
    └── main.rs

Изменим структуру проекта - удалим main.rs файл и создадим папку с кодом из которого будем собирать бинарные файлы. В корне проекта создадим json файл с данными

[
    {
        "name": "Bob",
        "gender": "male",
        "age": 34
    },
    {
        "name": "Alice",
        "gender": "female",
        "age": 32,
        "cars": [
            {
                "id": 1,
                "name": "BMW"
            },
            {
                "id": 2,
                "name": "Tesla Model X"
            }
        ]
    }
]

Получим структуру проекта:

mascai@MacBook-Air-Aleksei 16_rust_parser % tree json_habr
json_habr
├── Cargo.lock
├── Cargo.toml
├── data.json
├── src
│   └── bin
│       ├── 01_untyped_json.rs
│       └── 02_typed_json.rs

Данная структура позволяет удобно компилировать и запускать наши файлы одной cargo-командой.

cargo run --bin  01_untyped_json

Создадим простейшую программу и запустим ее.

// 01_untyped_json.rs


fn main() {
    println!("Hello world");
}

Получаем следующий вывод:

mascai@MacBook-Air-Aleksei json_habr % cargo run --bin  01_untyped_json
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/01_untyped_json`
Hello world

Мы создали каркас нашего проекта, теперь переходим к библиотекам.

2. Введение в serde_json

serde - библиотека (крейт) предназначена для сериализации и десериализации объектов, поддерживает большинство популярных форматов (json, yaml, toml, ...) и является самой популярной с точки зрения количества пользователей (данные взяты с сайта https://crates.io/crates/serde )

serde_json - предназначена для работы с json

Rust - это язык со строгой статической типизацией. Статическая типизация - типы переменных определяются на этапе компиляции. А строгая типизация не допускает неявного преобразования типов, например, код let var: i32 = 12.2; не скомпилируется

Библиотека serde предоставляет два способа парсинга json строк:

  1. Без указания типов данных ( с использованием serde_json::Value)

  2. Со строгой типизации (тип каждого поля json указан в специальной структуре)

2.1 Парсинг json без указания типов данных

Рассмотрим пример.

Подключаем библиотеку в Cargo.toml

[dependencies]
serde_json = {version="1.0.99"}

Рассмотрим пример.

use std::fs;
use serde_json;


fn main() {
    let res: Result<String, std::io::Error> = fs::read_to_string("data.json"); // read file into string
    let s = match res { // get value from Result object
        Ok(s) => s,
        Err(_) => panic!("Can't read file")
    };

    let mut json_data: serde_json::Value = serde_json::from_str(&s)
        .expect("Can't parse json"); // convert string to json object and panic in case of error

    println!("Data: {}", json_data);
    println!("Bob age: {}", json_data[0]["age"]);

    // change values
    json_data[0]["age"] = serde_json::json!(123);
    json_data[0]["name"] = serde_json::json!("mascai");
    println!("Data: {}", json_data);
    
    std::fs::write("output.json", serde_json::to_string_pretty(&json_data).unwrap())
        .expect("Can't write to file");
}

В первых двух строках подключаем библиотеки для работы с файлами и для работы с json

В пятой строке считываем содержимое файла в строку - создаем объект типа Result<String, std::io::Error>

Для преобразования объекта Result в строку можно использовать ключевое слово match (строки 7 - 9) или добавлять .expect()строки (12 - 13), который определяет сообщение об ошибке, если нельзя получить строку.

В строках 12-13 мы парсим json - создаем объект serde_json::Value. Если говорить простым языком, то serde_json::Value - это любой тип. Реализован этот тип через enum:

pub enum Value {
    Null,
    Bool(bool),
    Number(Number),
    String(String),
    Array(Vec<Value>),
    Object(Map<String, Value>),
}

В строках 19-20 модифицируем json объект и в строке 23 записываем результат в файл.

Запустим программу.

mascai@MacBook-Air-Aleksei json_habr % cargo run --bin  01_untyped_json
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/01_untyped_json`
Data: [{"age":34,"gender":"male","name":"Bob"},{"age":32,"cars":[{"id":1,"name":"BMW"},{"id":2,"name":"Tesla Model X"}],"gender":"female","name":"Alice"}]
Bob age: 34
Data: [{"age":123,"gender":"male","name":"mascai"},{"age":32,"cars":[{"id":1,"name":"BMW"},{"id":2,"name":"Tesla Model X"}],"gender":"female","name":"Alice"}]

Отлично! Мы научились читать, изменять и писать в json-файлы.

2.2 Парсинг json с указанием типов данных

Подключаем библиотеку для сериализации и десериализации даныых в Сargo.toml

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

Рассмотрим пример:

use serde::{Deserialize, Serialize};
use serde_json;
use std::fs;


#[derive(Deserialize, Serialize, Debug)]
struct Person {
    name: String,
    gender: String,
    age: i32,

    #[serde(default)] // use default value [], in case of lack of value
    cars: Vec<Car>
}

#[derive(Deserialize, Serialize, Debug)]
struct Car {
    id: i32,
    name: String
}


fn main() {
    
    let mut people = {  // serialize data into struct
        let res = fs::read_to_string("data.json").expect("Can't read file");
        serde_json::from_str::<Vec<Person>>(&res).unwrap()
    };
    
    people[1].name = "Mascai".to_string(); // Change data
    println!("{:?}", people);
    
    fs::write("output2.json", serde_json::to_string_pretty(&people).unwrap()) // save result    
        .expect("Can't write to file");
}

В строках 5 - 19 описываем структуры данных (имена и типы полей, обязательность полей).

Строка вида #[derive(Deserialize, Serialize, Debug)] подключает трейты, которые автоматически генерируют код для десериализации, сериализации и отладочного вывода объектов наших структур.

В строках 25 - 34 читаем json файл, изменяем данные и записываем обновленные данные в новый файл. Главное преимущество второго примера - возможность валидировать входные данные.

3 Полезные замечания

3.1 Есть два способа подключения трейтов Deserialize, Serialize

Первый способ.

# Cargo.toml
[dependencies]
serde = {version="1.0.99"}
serde_derive = {version="1.0.117"}

В этом случае мы импортируем трейты так: use serde_derive::{Deserialize, Serialize};

Второй способ.

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

В втором случае мы импортируем трейты так: use serde::{Deserialize, Serialize};

Всегда используйте второй способ! Во втором способе не нужно волноваться по поводу несовместимости версий (в первом пример специально указал разные версии крейтов)

3.2 Default / Nullable поля

Если мы хотим, чтобы при отсутствии в данных поле создавалось с дефолтным значением, то нужно использовать.

#[serde(default)] // use default value [], in case of lack of value
cars: Vec<Car>

В этом случае мы получим пустой вектор.

Если мы хотим, чтобы при отсутствии данных поле имело значение null, то нужно использовать следующий код.

#[serde(default)] // use null, in case of lack of value
cars: Option<Vec<Car>>

3.3 Переименование полей

При сериализации имя структур имя полей в json файле должно совпадать с именем в структуре. Мы можем "указать" в структуре имя поля из json, например:

#[serde(rename = "nameInJsonFile"]
name_in_struct: i32

Резюме

Мы научились работать с JSON данными, для продакшен кода лучше использовать структуры с проверкой типов данных. Буду рад ответить на ваши вопросы, коллеги!

Комментарии (3)


  1. strelkan
    24.08.2023 02:35

    3.3 поправьте "sered"


    1. mascai Автор
      24.08.2023 02:35

      Спасибо, исправил


  1. constb
    24.08.2023 02:35
    +2

    "познакомить читателя с библиотеками для работы с JSON в Rust"

    Ожидание: автор сделает сравнение serde с serde-lite, miniserde и nanoserde, укажет на преимущества и недостатки
    Реальность: пересказ readme от serde… ????????‍♂️