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

Manual conversions

Use VersionBoundary when matching schema and model fields can move directly or convert through nested. For custom mapping or model validation, implement IntoModel or TryIntoModel on the latest schema yourself. VersionFamily still connects the history. Ordinary From and TryFrom migrations remain part of the derive workflow described earlier.

Custom model mapping

Here the schema’s display_name becomes the model’s name, which must be nonempty when loaded. The manual conversion replaces VersionBoundary:

#![allow(unused)]
fn main() {
extern crate thisversion;
use thisversion::VersionFamily;
use thisversion::traits::{IntoModel, IntoSchema, TryIntoModel};

#[derive(VersionFamily)]
#[thisversion(fallible)]
enum ContactVersions {
    V1(ContactV1),
}

struct ContactV1 {
    display_name: String,
}

struct Contact {
    name: String,
}

#[derive(Debug)]
struct EmptyName;

impl TryIntoModel for ContactV1 {
    type Model = Contact;
    type Error = EmptyName;

    fn try_into_model(self) -> Result<Contact, EmptyName> {
        if self.display_name.trim().is_empty() {
            return Err(EmptyName);
        }
        Ok(Contact { name: self.display_name })
    }
}

impl IntoSchema<ContactVersions> for Contact {
    fn into_schema(self) -> ContactVersions {
        ContactVersions::V1(ContactV1 { display_name: self.name })
    }
}
}

The validation runs for every version that reaches this latest schema, including V1 itself. Saving is implemented separately because replacing the derive also removes its generated output conversion. For an infallible mapping, implement IntoModel instead and use an infallible family. A manual schema needs its own TryIntoModel implementation if used directly with the Serde adapter, which requires that trait.

Several sources, one model

Another family can feed the same Contact. Add these definitions:

#![allow(unused)]
fn main() {
extern crate thisversion;
struct Contact {
    name: String,
}
use thisversion::traits::IntoModel;
use thisversion::VersionFamily;

#[derive(VersionFamily)]
enum DirectoryVersions {
    V1(DirectoryV1),
}

struct DirectoryV1 {
    first_name: String,
    last_name: String,
}

impl IntoModel for DirectoryV1 {
    type Model = Contact;

    fn into_model(self) -> Contact {
        Contact { name: format!("{} {}", self.first_name, self.last_name) }
    }
}
}

Assuming directory names are already valid, each family now follows its own history into Contact. The directory mapping discards the original name parts, so it deliberately has no reverse conversion. If export must preserve them, retain them in the model or elsewhere in the application. Supporting an input format does not require supporting it as an output format.