| Crates.io | serde-evolve |
| lib.rs | serde-evolve |
| version | 0.1.0 |
| created_at | 2025-10-22 11:34:32.059744+00 |
| updated_at | 2025-10-22 11:34:32.059744+00 |
| description | Type-safe data schema evolution with compile-time verified migrations |
| homepage | |
| repository | https://github.com/danieleades/serde-evolve |
| max_upload_size | |
| id | 1895519 |
| size | 46,985 |
A Rust library for versioning serialised data structures with compile-time verified migrations.
serde-evolve helps you evolve data schemas over time while maintaining backward compatibility with historical data. It separates wire format (serialization) from domain types (application logic), allowing you to deserialise any historical version and migrate it to your current domain model.
Add this to your Cargo.toml:
[dependencies]
serde-evolve = "0.1"
serde = { version = "1.0", features = ["derive"] }
From/TryFrom, no custom APIsuse serde::{Deserialize, Serialize};
use serde_evolve::Versioned;
// Define version DTOs
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserV1 {
pub name: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserV2 {
pub full_name: String,
pub email: Option<String>,
}
// Define migrations
impl From<UserV1> for UserV2 {
fn from(v1: UserV1) -> Self {
Self {
full_name: v1.name,
email: None,
}
}
}
// Define domain type
#[derive(Clone, Debug, Versioned)]
#[versioned(
mode = "infallible",
chain(UserV1, UserV2),
)]
pub struct User {
pub full_name: String,
pub email: Option<String>,
}
// Final migration to domain
impl From<UserV2> for User {
fn from(v2: UserV2) -> Self {
Self {
full_name: v2.full_name,
email: v2.email,
}
}
}
// Serialization (domain → representation)
impl From<&User> for UserV2 {
fn from(user: &User) -> Self {
Self {
full_name: user.full_name.clone(),
email: user.email.clone(),
}
}
}
// Usage:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let json_v1 = r#"{"_version":"1","name":"Alice"}"#;
let rep: UserVersions = serde_json::from_str(json_v1)?;
let user: User = rep.into(); // Automatic migration V1 → V2 → User
Ok(())
}
All migrations guaranteed to succeed:
#[versioned(mode = "infallible", chain(V1, V2))]
Generates: impl From<Representation> for Domain
Migrations can fail (validation, transformation errors):
// `mode = "fallible"` is the default; specify it only when overriding.
#[versioned(error = MyError, chain(V1, V2))]
Generates: impl TryFrom<Representation> for Domain
By default, you work explicitly with the representation enum:
// Default behavior - explicit representation
let rep: UserVersions = serde_json::from_str(json)?;
let user: User = rep.try_into()?;
The transparent = true flag generates custom Serialize/Deserialize implementations that allow direct domain type serialisation:
#[versioned(
mode = "infallible",
chain(V1, V2),
transparent = true // ← Enable transparent serde
)]
pub struct User {
pub name: String,
}
// Now works directly:
let user: User = serde_json::from_str(json)?;
let json = serde_json::to_string(&user)?;
Data is serialised with an embedded _version tag:
{
"_version": "1",
"name": "Alice"
}
Serde's #[serde(tag = "_version")] handles routing to the correct variant.
From/TryInto, not custom APIs┌─────────────────────────────────────────────────┐
│ Historical Data (V1, V2, ...) │
└────────────────┬────────────────────────────────┘
│ Deserialize
▼
┌─────────────────────────────────────────────────┐
│ Representation Enum (auto-generated) │
│ ┌─────────────────────────────────────────┐ │
│ │ enum UserVersions { │ │
│ │ V1(UserV1), │ │
│ │ V2(UserV2), │ │
│ │ } │ │
│ └─────────────────────────────────────────┘ │
└────────────────┬────────────────────────────────┘
│ From/TryFrom (chain migrations)
▼
┌─────────────────────────────────────────────────┐
│ Domain Type (your application logic) │
│ struct User { ... } │
└─────────────────────────────────────────────────┘
The #[derive(Versioned)] macro generates:
From<Representation> for Domain (or TryFrom for fallible)From<&Domain> for Representation (for serialization)version(), is_current(), CURRENT