сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Экосистема Rust

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

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, где вариант угадывается по структуре данных.

дальше

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

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