Привет Хабр! Меня зовут Алексей, я разработчик Группы "Иннотех" холдинга Т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 строк:
Без указания типов данных ( с использованием serde_json::Value)
Со строгой типизации (тип каждого поля 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 данными, для продакшен кода лучше использовать структуры с проверкой типов данных. Буду рад ответить на ваши вопросы, коллеги!