Обновить

Serde в Rust: как отсутствующее поле JSON незаметно отключало настройки приложения

Уровень сложностиПростой
Время на прочтение8 мин
Охват и читатели5.5K
Рейтинг0
Комментарии3

Комментарии 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> как раз выяснилось, что никакой булевый дефолт не подходит, поскольку «не проверяли» и «проверили, результата нет» — разные состояния.

Для более серьёзных изменений формата явное версионирование, конечно, предпочтительнее.

Но не каждое добавление необязательного поля требует отдельной версии API и полноценной миграции.

Кроилово ведёт к попадалову

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации