Комментарии 3
Видимо, проблема в том что отсутствие поля это по сути другая версия апи/формата и должны быть фолбеки/поддержка старых версий апи в явном виде. А default это поганый стиль языка го
Лучше так
fn parse(json: &str) -> Result<Preferences, serde_json::Error> {
// v2 — текущая
if let Ok(v) = serde_json::from_str::<PreferencesV2>(json) {
return Ok(v.into());
}
// v1 — легаси
serde_json::from_str::<PreferencesV1>(json).map(Preferences::from)
}А ещё лучше в явном виде в JSON
#[derive(Deserialize)]
struct Envelope {
version: u32,
#[serde(flatten)]
body: serde_json::Value,
}И в итоге реализация станет такой
use serde::{Deserialize, Serialize};
use thiserror::Error;
const CURRENT_VERSION: u32 = 3;
const FIRST_SUPPORTED_VERSION: u32 = 1;
#[derive(Error, Debug)]
enum PrefsError {
#[error("ожидался объект с полем `version`: {0}")]
Envelope(#[from] serde_json::Error),
#[error("тело формата v{version} не разобрано: {source}")]
Body { version: u32, #[source] source: serde_json::Error },
#[error("файл создан более новой версией (формат v{got}, поддерживается до v{known}); обновите приложение")]
TooNew { got: u32, known: u32 },
#[error("версия формата v{got} не поддерживается (минимальная: v{first})")]
Unsupported { got: u32, first: u32 },
}
// --- версии формата --------------------------------------------------------
/// v1: изначальный формат
#[derive(Deserialize)]
struct PrefsV1 { theme: String }
/// v2: добавили язык
#[derive(Deserialize)]
struct PrefsV2 { theme: String, language: String }
/// v3 (текущая): добавили акцентный цвет
#[derive(Deserialize)]
struct PrefsV3 { theme: String, language: String, accent: String }
/// доменная структура — единственная, с которой живёт остальной код
struct Preferences { theme: String, language: String, accent: String }
// --- явные миграции: каждая имеет имя, комментарий и свой тест --------------
impl PrefsV1 {
/// v1 -> v2: язык отсутствовал, все считались англоязычными.
fn into_v2(self) -> PrefsV2 {
PrefsV2 { theme: self.theme, language: "en".into() }
}
}
impl PrefsV2 {
/// v2 -> v3: акцентного цвета не было — стандартный синий.
fn into_v3(self) -> PrefsV3 {
PrefsV3 { theme: self.theme, language: self.language, accent: "#0066cc".into() }
}
}
impl From<PrefsV3> for Preferences {
fn from(v: PrefsV3) -> Self {
Self { theme: v.theme, language: v.language, accent: v.accent }
}
}
// --- чтение ------------------------------------------------------------------
#[derive(Deserialize)]
struct Envelope {
version: u32,
#[serde(flatten)]
body: serde_json::Value,
}
fn parse(json: &str) -> Result<Preferences, PrefsError> {
let env: Envelope = serde_json::from_str(json)?; // нет `version` -> Envelope-ошибка
match env.version {
1 => Ok(serde_json::from_value::<PrefsV1>(env.body)
.map_err(|e| PrefsError::Body { version: 1, source: e })?
.into_v2().into_v3().into()),
2 => Ok(serde_json::from_value::<PrefsV2>(env.body)
.map_err(|e| PrefsError::Body { version: 2, source: e })?
.into_v3().into()),
CURRENT_VERSION => serde_json::from_value::<PrefsV3>(env.body)
.map_err(|e| PrefsError::Body { version: CURRENT_VERSION, source: e })
.map(Into::into),
v if v > CURRENT_VERSION => Err(PrefsError::TooNew { got: v, known: CURRENT_VERSION }),
v => Err(PrefsError::Unsupported { got: v, first: FIRST_SUPPORTED_VERSION }),
}
}Согласен, отсутствие поля действительно может быть следствием различий между версиями контракта. Но не каждое добавление необязательного поля требует отдельной версии API и полноценной миграции.
Если семантика отсутствующего поля заранее определена, дефолт вполне может быть частью контракта обратной совместимости. В описанном случае ошибка была именно в выборе значения: Default::default() для bool возвращал false, хотя по логике приложения требовался true.
А во втором примере с Option<bool> как раз выяснилось, что никакой булевый дефолт не подходит, поскольку «не проверяли» и «проверили, результата нет» — разные состояния.
Для более серьёзных изменений формата явное версионирование, конечно, предпочтительнее.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Serde в Rust: как отсутствующее поле JSON незаметно отключало настройки приложения