← вся теориятеория к собесу · Экосистема Rust

serde: сериализация в Rust

serde разводит две вещи: модель данных и формат. derive генерирует код обхода полей твоей структуры, а json или другой формат подключается отдельным крейтом. Расхождение имён с чужой схемой лечится атрибутами — переименованием отдельного поля или сменой стиля для всей структуры сразу.

разбор писал Даниил, автор Сеньорчика

serde

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

Стержень: serde отделяет модель данных от формата; derive генерирует обход полей, а формат подключается отдельным крейтом.

// Формулировки: «что делает derive(Serialize)?», «как переименовать поле?», «что с неизвестным полем в JSON?»

Модель и формат порознь

derive(Serialize, Deserialize) пишет для типа код обхода полей - какой именно формат, ему всё равно. Формат подключается отдельно: serde_json, bincode, serde_yaml, postcard. Поэтому одна структура сериализуется куда угодно без изменений.

Отсюда и скорость: рефлексии нет, для каждого поля генерируется прямой код записи и чтения, который компилятор инлайнит. Результат близок к рукописному парсеру. Плата - время компиляции на больших моделях данных.

#[derive(Serialize, Deserialize)]
struct User { id: u64, name: String }
Serialize/Deserialize
трейты обхода полей, генерируемые derive
формат
отдельный крейт: json, yaml, bincode и другие

Атрибуты закрывают расхождения

Внешний формат редко совпадает с внутренней моделью, и почти всё решается атрибутами: rename и rename_all для имён, default для отсутствующих полей, skip_serializing_if для пустых, flatten для вложенности, with для своих преобразований вроде нестандартного формата даты. Ручная реализация Deserialize нужна редко.

Неизвестные поля по умолчанию игнорируются это совместимость вперёд: отправитель обновился раньше получателя, и ничего не сломалось. Если нужен строгий контракт (конфиг, защита от опечаток), включают deny_unknown_fields. А вот отсутствующее обязательное поле - ошибка всегда, пока для него не задан default или Option.

// Для необязательности выбирают между Option<T> и default: первый честно говорит «значения может не быть», второй подставляет разумное значение и упрощает код читателя.

rename_all
переименование полей под соглашение формата
deny_unknown_fields
строгий разбор: лишнее поле - ошибка

Как отвечать: «Что произойдёт с неизвестным полем при разборе JSON?»

По умолчанию serde его просто проигнорирует, и разбор пройдёт успешно. Это сознательное решение ради совместимости вперёд: отправитель может добавить поле в новой версии, а старый получатель продолжит работать. Если нужен строгий контракт - например, в конфиге, где опечатка в имени параметра должна быть заметна, ставлю deny_unknown_fields, и лишнее поле станет ошибкой. А вот отсутствующее обязательное поле - ошибка в любом случае: чтобы его можно было пропустить, поле объявляют как Option или помечают атрибутом default.

Ты знаешь поведение по умолчанию, знаешь как его поменять и понимаешь, зачем оно такое. Плюс закрываешь симметричный вопрос про отсутствующее поле.

На чём валятся

  • Переименовывают поля структуры под JSON вместо атрибута rename.
  • Включают deny_unknown_fields в публичном API и ломают совместимость с новыми клиентами.
  • Ждут, что отсутствующее поле заполнится нулём без default.
  • Пишут ручную реализацию Deserialize там, где хватило бы атрибута with.
  • Считают, что serde работает через рефлексию.

Проверь себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_serde1 / 5
    Клиент прислал JSON с лишним неизвестным полем. Что произойдёт по умолчанию?
    A)Разбор упадёт с ошибкой о неизвестном поле
    B)Поле будет проигнорировано, разбор пройдёт успешно
    C)Поле попадёт в скрытую карту дополнительных значений
    D)Значение поля будет записано в первое подходящее по типу
    показать ответ и разбор
    +B)Поле будет проигнорировано, разбор пройдёт успешно

    // разбор: По умолчанию serde пропускает то, чего нет в структуре, — это удобно для совместимости вперёд, когда отправитель обновился раньше получателя. Если лишние поля должны считаться ошибкой (строгий контракт, защита от опечаток в конфиге), ставят #[serde(deny_unknown_fields)]. А вот отсутствующее обязательное поле — ошибка в любом случае, пока для него не задан default.

  2. #rs_serde2 / 5
    Почему serde считается быстрым, хотя работает с произвольными структурами?
    A)Он кэширует раскладку типов при первом разборе
    B)Он разбирает документ лениво, откладывая работу до обращения к полю
    C)Код обхода генерируется при компиляции, рефлексии нет
    D)Он читает поля напрямую по смещениям через unsafe
    показать ответ и разбор
    +C)Код обхода генерируется при компиляции, рефлексии нет

    // разбор: Динамического разбора структуры типа в рантайме не происходит: derive пишет для каждого поля прямой код записи и чтения, а формат вызывает его через трейты Serializer и Deserializer. Компилятор это инлайнит, и получается почти то же, что рукописный парсер. Оборотная сторона — рост времени компиляции на больших моделях данных.

  3. #rs_serde3 / 5
    Как в структуре конфига пометить поле, которого может не быть во входных данных?
    A)Сделать его Option<T> или добавить #[serde(default)]
    B)Пометить #[serde(skip)] — тогда поле станет необязательным
    C)Обернуть в Result<T, E> — serde подставит Err при отсутствии
    D)Ничего не делать: отсутствующие поля заполняются нулями
    показать ответ и разбор
    +A)Сделать его Option<T> или добавить #[serde(default)]

    // разбор: Option отражает необязательность в типе: None означает «не задано». Атрибут default подставит значение по умолчанию для типа или из указанной функции — это удобнее, когда у настройки есть разумное значение и городить Option не хочется. skip исключает поле из формата вовсе, а само по себе отсутствие обязательного поля даёт ошибку разбора.

  4. #rs_serde4 / 5
    Что делает #[serde(deny_unknown_fields)] на структуре?
    A)Запрещает добавлять новые поля в структуру без обновления версии формата
    B)Отключает подстановку значений по умолчанию для отсутствующих полей
    C)Складывает неизвестные поля в отдельную карту, доступную после разбора
    D)Превращает неизвестное поле во входных данных в ошибку разбора
    показать ответ и разбор
    +D)Превращает неизвестное поле во входных данных в ошибку разбора

    // разбор: По умолчанию serde игнорирует лишние поля — это удобно для совместимости вперёд, когда отправитель обновился раньше получателя. Атрибут включает строгий режим: любое неизвестное поле даёт ошибку. Так делают там, где опечатка в конфиге должна быть замечена сразу, а не молча проигнорирована. Собрать неизвестные поля в карту можно другим приёмом — flatten поверх HashMap.

  5. #rs_serde5 / 5
    Как serde представляет enum по умолчанию и что меняет #[serde(tag = "type")]?
    A)По умолчанию внешне-теговое представление, а атрибут делает его внутренне-теговым
    B)По умолчанию тег не пишется, а атрибут добавляет имя варианта в вывод
    C)По умолчанию используется числовой индекс варианта, а атрибут заменяет его строкой
    D)По умолчанию варианты сериализуются как массив, а атрибут превращает их в объект
    показать ответ и разбор
    +A)По умолчанию внешне-теговое представление, а атрибут делает его внутренне-теговым

    // разбор: По умолчанию вариант оборачивается в объект с одним ключом-именем: {"Click":{"x":5}}. Атрибут tag переносит имя внутрь: {"type":"click","x":5} — именно такой формат ждут почти все внешние API. Есть ещё смежные варианты: пара tag и content для соседних полей и untagged, где вариант угадывается по структуре данных.

дальше

Теорию прочитал. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.