Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fallible conversions

Use TryFrom when a migration can reject old values. Keep both derives: mark the family fallible, give the latest schema an error type, and load with try_into_model().

Here, a retry count narrows from u32 to u8. The standard conversion supplies TryFromIntError when the old value exceeds 255:

extern crate serde;
extern crate serde_json;
extern crate thisversion;
use std::num::TryFromIntError;
use serde::{Deserialize, Serialize};
use thisversion::{VersionBoundary, VersionFamily};
use thisversion::traits::{IntoSchema, TryIntoModel};

#[derive(Deserialize, Serialize, VersionFamily)]
#[serde(tag = "version", content = "data")]
#[thisversion(fallible)]
enum SettingsVersions {
    V1(SettingsV1),
    V2(SettingsV2),
}

#[derive(Deserialize, Serialize)]
struct SettingsV1 {
    retries: u32,
}

#[derive(Deserialize, Serialize, VersionBoundary)]
#[thisversion(model = Settings, error = TryFromIntError)]
struct SettingsV2 {
    retries: u8,
}

struct Settings {
    retries: u8,
}

impl TryFrom<SettingsV1> for SettingsV2 {
    type Error = TryFromIntError;

    fn try_from(old: SettingsV1) -> Result<Self, Self::Error> {
        Ok(Self { retries: old.retries.try_into()? })
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let input = r#"{"version":"V1","data":{"retries":3}}"#;
    let stored: SettingsVersions = serde_json::from_str(input)?;
    let settings = stored.try_into_model()?;
    let stored: SettingsVersions = settings.into_schema();
    assert!(matches!(stored, SettingsVersions::V2(_)));
    Ok(())
}

Nested errors

The latest schema chooses the family’s final error type. Errors from migration steps and nested conversions must convert into it through From.

A parent can reuse the child’s error. Add these types to the example above:

#![allow(unused)]
fn main() {
extern crate serde;
extern crate serde_json;
extern crate thisversion;
use serde::Deserialize;
struct Settings {
    retries: u8,
}

#[derive(Deserialize)]
struct SettingsV1 {
    retries: u32,
}

#[derive(VersionBoundary, Deserialize)]
#[thisversion(model = Settings, error = TryFromIntError)]
struct SettingsV2 {
    retries: u8,
}

impl TryFrom<SettingsV1> for SettingsV2 {
    type Error = TryFromIntError;

    fn try_from(old: SettingsV1) -> Result<Self, Self::Error> {
        Ok(Self { retries: old.retries.try_into()? })
    }
}

#[derive(Deserialize, VersionFamily)]
#[serde(tag = "version", content = "data")]
#[thisversion(fallible)]
enum SettingsVersions {
    V1(SettingsV1),
    V2(SettingsV2),
}
use std::num::TryFromIntError;
use thisversion::{VersionBoundary, VersionFamily};
use thisversion::traits::{IntoSchema, TryIntoModel};

struct Job {
    settings: Settings,
}

#[derive(VersionBoundary)]
#[thisversion(model = Job, error = TryFromIntError)]
struct JobV1 {
    #[thisversion(nested)]
    settings: SettingsVersions,
}
}

JobV1::try_into_model() propagates a failed settings migration. If the parent has several error sources, choose a parent error enum and implement From for each source. Mark a containing version family fallible too. Infallible From migrations can remain in a fallible family; their Infallible error must also convert into the final error type.

Validation and error handling

Latest inputs skip historical migrations. Here, V2’s u8 already prevents out-of-range retry counts. Other invariants may need checks during decoding or custom model construction. error = E enables error propagation; it does not add validation to ordinary fields. Check invalid historical and latest inputs, including nested values.

The Serde DeserializeIntoModel adapter combines decoding and migration, turning conversion errors into Serde errors using Display. To retain domain errors for matching, decode first, as above. To return decoding and migration errors through one application error, add:

#![allow(unused)]
fn main() {
extern crate serde;
extern crate serde_json;
extern crate thisversion;
use serde::Deserialize;
struct Settings {
    retries: u8,
}

#[derive(Deserialize)]
struct SettingsV1 {
    retries: u32,
}

#[derive(VersionBoundary, Deserialize)]
#[thisversion(model = Settings, error = TryFromIntError)]
struct SettingsV2 {
    retries: u8,
}

impl TryFrom<SettingsV1> for SettingsV2 {
    type Error = TryFromIntError;

    fn try_from(old: SettingsV1) -> Result<Self, Self::Error> {
        Ok(Self { retries: old.retries.try_into()? })
    }
}

#[derive(VersionFamily, Deserialize)]
#[serde(tag = "version", content = "data")]
#[thisversion(fallible)]
enum SettingsVersions {
    V1(SettingsV1),
    V2(SettingsV2),
}
use std::num::TryFromIntError;

use thisversion::{VersionBoundary, VersionFamily};
use thisversion::traits::{IntoSchema, TryIntoModel};

#[derive(Debug)]
enum LoadError {
    Decode(serde_json::Error),
    Migration(TryFromIntError),
}

fn load(input: &str) -> Result<Settings, LoadError> {
    let stored: SettingsVersions =
        serde_json::from_str(input).map_err(LoadError::Decode)?;
    stored.try_into_model().map_err(LoadError::Migration)
}
}