Skip to content
Назад к блогу
Руководства

JSON в структуры Rust: генерация структур serde (гайд 2026)

Правильно преобразуйте JSON в структуры Rust: числовые типы (i64/u64/f64), Option для null, serde rename для camelCase и подводные камни. Бесплатно в браузере.

11 мин чтения

JSON в структуры Rust: генерация структур serde (гайд 2026)

Вставьте свой payload в конвертер JSON в структуры Rust и скопируйте сгенерированную структуру serde. В этом и состоит весь процесс превращения JSON в структуру Rust: ничего не нужно устанавливать, ничего не загружается на сервер, всё происходит в браузере. Это закрывает сценарий «структура нужна прямо сейчас» за секунды.

Но сгенерировать структуру и сгенерировать правильную структуру — это разные задачи. Конвертер может только угадывать. И здесь Rust повышает ставки. В TypeScript неверная догадка вырождается в расплывчатый any, который можно проигнорировать. С serde неверная догадка приводит к сбою при десериализации: float, попавший в i64, неожиданный null в необязательном поле — и serde_json::from_str возвращает Err вместо ваших данных. Вот почему правильная структура в Rust важнее, чем в большинстве других языков.

Ниже разбирается, как на самом деле работает вывод типов, какое правило типизации чисел большинство конвертеров нарушают, когда выбирать #[serde(rename)], а когда #[serde(rename_all)], и как обращаться с датами, динамическими ключами и ключевыми словами Rust, чтобы результат всегда компилировался.

Как конвертировать JSON в Rust

Конвертация JSON в Rust занимает три шага:

  1. Вставьте свой JSON. Поместите объект, массив или сырой ответ API в поле ввода. Конвертация запускается мгновенно и полностью на стороне клиента.
  2. Настройте вывод. Переименуйте корневую структуру, включите или отключите derive-макросы serde, добавьте Debug и Clone или уберите видимость pub, чтобы соответствовать стилю вашего крейта.
  3. Скопируйте или скачайте. Получите сгенерированный Rust одним кликом и вставьте его прямо в свой проект.

На этом транзакционный путь закончен. Если входные данные минифицированы или вы не уверены в их корректности, сначала прогоните их через форматировщик JSON, чтобы конвертер получил чистый, правильно сформированный JSON. Остальная часть гайда объясняет, как читать результат, чтобы исправить случаи, которые инструмент не может вывести самостоятельно.

Как типы JSON отображаются на Rust

У каждого значения JSON есть аналог в Rust, но соответствие не совсем взаимно-однозначное:

Значение JSONТип Rust
"text"String
42i64 (большие значения — u64, ещё больше — f64)
3.14, 2e3f64
true / falsebool
nullOption<T> (или Option<serde_json::Value>)
[1, 2, 3]Vec<T>
{ ... }именованная struct

Основная работа приходится на объекты. Возьмём типичный payload из REST-API:

{
  "id": 101,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "active": true,
  "roles": ["admin", "user"]
}

Конвертер выдаёт готовую для serde структуру:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub id: i64,
    pub name: String,
    pub email: String,
    pub active: bool,
    pub roles: Vec<String>,
}

Каждый скаляр отображается на примитив, а roles становится Vec<String>. Единственный тип JSON, который не отображается чисто, — это number: в JSON один числовой тип, тогда как Rust заставляет выбирать между i64, u64 и f64. Этому выбору посвящён отдельный раздел ниже, потому что именно здесь ошибается большинство генераторов.

Как конвертер выводит структуры

Четыре правила покрывают почти всё, что вы в него подадите: одна структура на форму объекта, слияние массивов ключ за ключом, точная типизация чисел и идиоматичные имена полей.

Структурный вывод: одна именованная структура на объект

Каждая отдельная форма объекта становится собственной именованной структурой. Вложенные объекты не встраиваются: они выносятся в отдельные определения, на которые ссылаются по полю:

{ "repo": "serde", "owner": { "login": "dtolnay", "id": 100 } }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub repo: String,
    pub owner: Owner,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Owner {
    pub login: String,
    pub id: i64,
}

Вложенный owner становится собственной структурой Owner, на которую ссылается поле. Одинаковые формы дедуплицируются, поэтому два поля с одинаковой структурой используют одну структуру, а не порождают копии.

Слияние массивов и поля Option

Когда вы передаёте массив объектов, конвертер сливает их ключ за ключом. Ключ, присутствующий в одних элементах и отсутствующий в других, становится Option:

{ "users": [{ "id": 1, "nick": "x" }, { "id": 2 }] }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub users: Vec<User>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: i64,
    pub nick: Option<String>,
}

id есть в каждом элементе и остаётся обязательным. nick у второго пользователя отсутствует — отсюда Option<String>. Обратите внимание на то, чего здесь нет: нет #[serde(default)]. serde и так считает Option необязательным полем и десериализует отсутствующий ключ в None, так что атрибут был бы избыточным.

Правильная типизация чисел: i64, u64, f64

Именно это правило пропускают конкурирующие инструменты, и именно оно ломается на реальных данных. Генератор применяет три уровня. Целое число отображается на i64. Если значение превышает i64::MAX, оно повышается до u64. За пределами u64::MAX происходит откат к f64. Любой токен, записанный с десятичной точкой или экспонентой, например 1.0 или 2e3, отображается на f64 независимо от величины.

{ "user_id": 12000000000000000000, "balance": 19.99, "retries": 3 }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub user_id: u64,
    pub balance: f64,
    pub retries: i64,
}

Здесь user_id больше i64::MAX, поэтому попадает в u64 и по-прежнему проходит round-trip. balance содержит десятичную точку — значит, f64, а не целое число. Это важно, потому что serde проверяет тип при десериализации. Дайте ему структуру, где snowflake или ID в стиле Twitter типизирован как i32, — и значение переполнится; типизируйте поле с float как i64 — и serde вернёт ошибку, а не молча обрежет значение. Именно трёхуровневое правило позволяет сгенерированной структуре Rust из JSON десериализоваться, а не паниковать на первом же большом ID.

Поля snake_case и #[serde(rename)]

Ключи JSON часто в camelCase; идиоматичные поля Rust — в snake_case. Конвертер переименовывает поле и добавляет #[serde(rename)], который сопоставляет его обратно с точным ключом JSON:

{ "login": "octocat", "publicRepos": 15, "followerCount": 9001, "createdAt": "2011-01-25" }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub login: String,
    #[serde(rename = "publicRepos")]
    pub public_repos: i64,
    #[serde(rename = "followerCount")]
    pub follower_count: i64,
    #[serde(rename = "createdAt")]
    pub created_at: String,
}

Ключ, который уже в snake_case, например login, не переименовывается. Остальные чисто сопоставляются обратно с форматом передачи, а ваш код на Rust читается естественно.

#[serde(rename)] против #[serde(rename_all)]

Инструмент выдаёт #[serde(rename)] для каждого поля, потому что это работает всегда — даже когда один payload смешивает camelCase, snake_case и нерегулярные ключи в одном объекте. Ему не нужно рассуждать об общем соглашении, которого может и не быть.

Когда все поля структуры действительно следуют одному соглашению, эти атрибуты можно свернуть в одно переименование на уровне контейнера. Удалите построчные атрибуты и поставьте один #[serde(rename_all = "camelCase")] на структуру:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
    pub login: String,
    pub public_repos: i64,
    pub follower_count: i64,
    pub created_at: String,
}

Обе версии десериализуют один и тот же JSON. Используйте rename для каждого поля при смешанных или нерегулярных ключах; берите rename_all, когда структура однородна и хочется меньше шума. serde также поставляет другие соглашения для того же атрибута, такие как "snake_case", "kebab-case" и "SCREAMING_SNAKE_CASE".

Ключевые слова Rust и ключи, не являющиеся идентификаторами

Ключи JSON — это просто строки, поэтому ничто не мешает API прислать ключ с именем type или match, а оба они — зарезервированные слова в Rust. Генератор преобразует их в допустимый идентификатор плюс переименование:

{ "type": "user", "match": true, "first-name": "Ada" }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    #[serde(rename = "type")]
    pub type_: String,
    #[serde(rename = "match")]
    pub match_: bool,
    #[serde(rename = "first-name")]
    pub first_name: String,
}

Форма с завершающим подчёркиванием (type_) намеренно выбрана вместо сырого идентификатора вроде r#type. Сырые идентификаторы не способны выразить каждое ключевое слово в любой позиции; в частности, self, crate и super отвергаются как сырые идентификаторы, поэтому подход «преобразование плюс переименование» — единственный, который всегда компилируется. Ключи, не являющиеся идентификаторами, такие как first-name или 2fa с ведущей цифрой, получают то же обращение: корректное имя Rust и переименование обратно к буквальному ключу JSON.

Десериализация с помощью serde_json

Когда структура готова, добавьте зависимости в Cargo.toml:

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

Дальше разбор — это одна строка. Этот пример полный и компилируется как есть:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub id: i64,
    pub name: String,
    pub email: String,
    pub active: bool,
    pub roles: Vec<String>,
}

fn main() -> Result<(), serde_json::Error> {
    let data = r#"{"id":101,"name":"Ada Lovelace","email":"a@ex.com","active":true,"roles":["admin"]}"#;
    let root: Root = serde_json::from_str(data)?;
    println!("{} has {} role(s)", root.name, root.roles.len());
    Ok(())
}

Всю работу делает derive-макрос Deserialize. Используйте serde_json::from_str для строки, from_slice для среза байтов или from_reader для файла либо тела HTTP, а serde_json::to_string — чтобы сериализовать значение обратно в JSON. Это и есть награда за правильные типы: сгенерированный вами serde derive — это валидатор времени выполнения, поэтому вызов десериализации serde_json, возвращающий Ok, — это гарантия того, что данные совпали с вашей структурой, а не просто уверенность компилятора.

Даты, динамические ключи и неизвестные поля

Некоторые формы не отображаются на обычное поле структуры, и генератор даёт им разумное значение по умолчанию, которое вы затем уточняете вручную.

Даты. В JSON нет типа даты, поэтому timestamp в формате ISO или RFC 3339 приходит как обычная String. Для настоящей работы с датами замените поле на тип из chrono и включите feature serde у chrono (chrono = { version = "0.4", features = ["serde"] }). После этого serde автоматически разбирает RFC 3339:

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
    pub name: String,
    pub created_at: DateTime<Utc>,
}

Динамические ключи. Когда ключи меняются во время выполнения, как в отображении ID на значения, фиксированная структура — неподходящая форма. Используйте HashMap с ключом String:

use std::collections::HashMap;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub scores: HashMap<String, i64>,
}

Неизвестные дополнительные поля. Чтобы сохранить типизированную структуру, но всё же захватить ключи, которые вы не смоделировали, добавьте поле #[serde(flatten)] на основе HashMap. Для по-настоящему динамического значения универсальным решением служит serde_json::Value. Когда ваша настоящая задача — проверять структуру, а не генерировать типы, гайд по валидации JSON Schema описывает, как обеспечить соблюдение контракта от начала до конца.

json-to-rust против quicktype, typeshare и ручного подхода

Единственного лучшего способа получить типы Rust из JSON не существует. Всё зависит от того, где находится JSON и в каком направлении вы конвертируете.

ПодходЛучше всего дляПримечание
Онлайн-конвертер (этот инструмент, transform.tools)Разовые конвертации, чувствительные данные, ноль установкиВыводит по образцу, полностью на стороне клиента
quicktypeВывод на несколько языков, кодогенерация в пайплайнеТоже управляется образцом; CLI или веб
typeshareСовместное использование существующих типов Rust с TS, Swift, KotlinОбратное направление: из Rust в другие языки
schemafy / typifyГенерация из JSON SchemaНа входе schema, а не образец
Написание структур вручнуюКрошечные данные, изучение serdeПолный контроль, но муторно и чревато ошибками при масштабировании

Различие, которое стоит усвоить: онлайн-конвертер и quicktype оба выводят типы из образца JSON, тогда как typeshare работает в обратную сторону, превращая аннотированные типы Rust в TypeScript или Swift. schemafy и typify принимают на вход JSON Schema, а не примерные данные. Если ваш код на TypeScript, а не на Rust, тот же подход на основе образца применяется с конвертером JSON в TypeScript, а гайд по интерфейсам JSON в TypeScript подробно раскрывает эту сторону.

Частые ошибки при генерации Rust из JSON

Сгенерированные структуры — это отправная точка. Проверьте следующее, прежде чем доверять результату на живых данных.

  • Один образец редко раскрывает все формы. Опциональные поля можно вывести только из нескольких элементов массива. Вставьте представительный массив, чтобы вывод Option был точным, а не догадкой по одному удачному объекту.
  • Пустые или смешанные массивы откатываются к serde_json::Value. Это честный ответ, когда выводить не из чего. Подайте более богатый образец, чтобы получить конкретный тип элемента.
  • Не сужайте большой ID до i32. Snowflake и подобные ID превышают 2^53 и переполняют 32-битное поле. Оставьте сгенерированный i64 или u64.
  • Всегда-null поле становится Option<serde_json::Value>, а не обычным опциональным полем. Инструменту нечего выводить, поэтому считайте это заглушкой и дайте полю настоящий тип, когда узнаете его.
  • serde_json::Value требует зависимости serde_json. Если в выводе есть Value, а в вашем Cargo.toml нет этого крейта, код не скомпилируется.
  • Дата, оставленная как String, не годится для арифметики с датами. Если вы планируете сравнивать или форматировать значения timestamp, переключитесь на тип из chrono вместо ручного разбора строк.

Часто задаваемые вопросы

Почему serde отклоняет мой JSON, хотя структура выглядит правильной?

Почти всегда это несовпадение типов. Две обычные причины — значение с плавающей точкой, попавшее в поле i64, и ключ, который иногда отсутствует, но не типизирован как Option. serde проверяет тип при десериализации, поэтому исправьте поле на f64 или Option<T> либо перегенерируйте структуру из представительного образца.

В какой числовой тип Rust превращать целое число JSON?

По умолчанию i64. Повышайте до u64, когда значение превышает i64::MAX, и откатывайтесь к f64 за пределами u64::MAX. Любое число, записанное с десятичной точкой или экспонентой, отображается на f64 независимо от размера, потому что serde отвергает float, десериализуемый в целочисленное поле.

Как сделать поле опциональным, если JSON его пропускает?

Типизируйте его как Option<T>. serde автоматически рассматривает Option как необязательное поле и десериализует отсутствующий ключ в None, поэтому #[serde(default)] не нужен. Конвертер помечает поле как Option, когда у части образцовых элементов ключ отсутствует.

Использовать #[serde(rename)] или #[serde(rename_all)]?

Используйте #[serde(rename)] для каждого поля, когда payload смешивает стили именования; это работает всегда. Если все поля структуры следуют одному соглашению, удалите атрибуты для каждого поля и вместо них поставьте один #[serde(rename_all = "camelCase")] на структуру. Оба варианта десериализуют одинаково.

Как обрабатывать ключи JSON, совпадающие с ключевыми словами Rust, например type?

Генератор выдаёт type_ вместе с #[serde(rename = "type")], сопоставляющим его обратно с исходным ключом. Это надёжнее сырого идентификатора вроде r#type, потому что сырые идентификаторы не могут покрыть self, crate и super в любой позиции.

Почему даты JSON типизируются как String, а не как тип даты?

В JSON нет типа даты, поэтому timestamp — это просто строка при передаче, и String — честное значение по умолчанию. Для настоящей работы с датами замените поле на DateTime<Utc> или NaiveDate из chrono и включите feature serde у chrono; после этого serde сам разбирает RFC 3339.

Как десериализовать JSON в сгенерированную структуру?

Добавьте serde_json в Cargo.toml, затем напишите let root: Root = serde_json::from_str(json)?;. Остальное берёт на себя derive-макрос Deserialize. Используйте from_slice для среза байтов и from_reader для файла или тела HTTP.

Приватен ли мой JSON при использовании онлайн-конвертера JSON в структуры Rust?

Да. Конвертация выполняется на 100% в вашем браузере на JavaScript. Ваш JSON — включая token, ID и данные клиентов — никогда не покидает страницу и никогда не отправляется на сервер. Когда будете готовы, попробуйте конвертер JSON в структуры Rust на своём payload.

Теги: rust json serde type-safety developer-tools

Похожие статьи

Все статьи