serde: сериализация в Rust
Практически обязательный крейт, поэтому его знание проверяют почти всегда. Спрашивают, что делает 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, остальные разбираются в тренажёре.
- Клиент прислал JSON с лишним неизвестным полем. Что произойдёт по умолчанию?A)Разбор упадёт с ошибкой о неизвестном полеB)Поле будет проигнорировано, разбор пройдёт успешноC)Поле попадёт в скрытую карту дополнительных значенийD)Значение поля будет записано в первое подходящее по типу
показать ответ и разбор
+B)Поле будет проигнорировано, разбор пройдёт успешно// разбор: По умолчанию serde пропускает то, чего нет в структуре, — это удобно для совместимости вперёд, когда отправитель обновился раньше получателя. Если лишние поля должны считаться ошибкой (строгий контракт, защита от опечаток в конфиге), ставят #[serde(deny_unknown_fields)]. А вот отсутствующее обязательное поле — ошибка в любом случае, пока для него не задан default.
- Почему serde считается быстрым, хотя работает с произвольными структурами?A)Он кэширует раскладку типов при первом разбореB)Он разбирает документ лениво, откладывая работу до обращения к полюC)Код обхода генерируется при компиляции, рефлексии нетD)Он читает поля напрямую по смещениям через unsafe
показать ответ и разбор
+C)Код обхода генерируется при компиляции, рефлексии нет// разбор: Динамического разбора структуры типа в рантайме не происходит: derive пишет для каждого поля прямой код записи и чтения, а формат вызывает его через трейты Serializer и Deserializer. Компилятор это инлайнит, и получается почти то же, что рукописный парсер. Оборотная сторона — рост времени компиляции на больших моделях данных.
- Как в структуре конфига пометить поле, которого может не быть во входных данных?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 исключает поле из формата вовсе, а само по себе отсутствие обязательного поля даёт ошибку разбора.
- Что делает #[serde(deny_unknown_fields)] на структуре?A)Запрещает добавлять новые поля в структуру без обновления версии форматаB)Отключает подстановку значений по умолчанию для отсутствующих полейC)Складывает неизвестные поля в отдельную карту, доступную после разбораD)Превращает неизвестное поле во входных данных в ошибку разбора
показать ответ и разбор
+D)Превращает неизвестное поле во входных данных в ошибку разбора// разбор: По умолчанию serde игнорирует лишние поля — это удобно для совместимости вперёд, когда отправитель обновился раньше получателя. Атрибут включает строгий режим: любое неизвестное поле даёт ошибку. Так делают там, где опечатка в конфиге должна быть замечена сразу, а не молча проигнорирована. Собрать неизвестные поля в карту можно другим приёмом — flatten поверх HashMap.
- Как serde представляет enum по умолчанию и что меняет #[serde(tag = "type")]?A)По умолчанию внешне-теговое представление, а атрибут делает его внутренне-теговымB)По умолчанию тег не пишется, а атрибут добавляет имя варианта в выводC)По умолчанию используется числовой индекс варианта, а атрибут заменяет его строкойD)По умолчанию варианты сериализуются как массив, а атрибут превращает их в объект
показать ответ и разбор
+A)По умолчанию внешне-теговое представление, а атрибут делает его внутренне-теговым// разбор: По умолчанию вариант оборачивается в объект с одним ключом-именем: {"Click":{"x":5}}. Атрибут tag переносит имя внутрь: {"type":"click","x":5} — именно такой формат ждут почти все внешние API. Есть ещё смежные варианты: пара tag и content для соседних полей и untagged, где вариант угадывается по структуре данных.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.