serde-patch 0.1.0

JSON Merge Patch (RFC 7396) with optional recursive diff generation for Serde-derived structs
Documentation

serde-patch

JSON Merge Patch (RFC 7396) for Serde-derived types.

Provides macros to generate partial patches (diff) and apply them immutably or in-place.

[dependencies]
serde-patch = "0.1.0"

Features

  • diff! – generates a minimal patch containing only changed fields (and optional forced fields).
  • apply! – consumes the current value and returns an updated one.
  • apply_mut! – modifies the current value in-place.

The patch can be any type that implements AsRef<[u8]> (&str, String, Vec<u8>, etc.).

Example

use serde::{Deserialize, Serialize};
use serde_patch::{apply, apply_mut, diff};

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct Profile {
    bio: String,
    avatar_url: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct User {
    id: u32,
    username: String,
    age: u8,
    active: bool,
    profile: Option<Profile>,
}

fn main() {
    let old = User {
        id: 1001,
        username: "alice".to_string(),
        age: 30,
        active: true,
        profile: Some(Profile {
            bio: "Software engineer".to_string(),
            avatar_url: Some("https://example.com/alice-old.jpg".to_string()),
        }),
    };

    let new = User {
        id: 1001,
        username: "alice".to_string(),
        age: 31,
        active: false,
        profile: Some(Profile {
            bio: "Senior software engineer".to_string(),
            avatar_url: None,
        }),
    };

    // Basic diff – only changed fields
    let basic_patch = serde_json::to_string(&diff!(&old, &new).unwrap()).unwrap();
    // → {"active":false,"age":31,"profile":{"avatar_url":null,"bio":"Senior software engineer"}}


    // Diff with forced field – includes "id" amd "profile.bio" even though unchanged
    let forced_patch = serde_json::to_string(&diff!(&old, &new; ["id", "profile.bio"]).unwrap()).unwrap();
    // → {"active":false,"age":31,"id":1001,"profile":{"avatar_url":null,"bio":"Senior software engineer"}}


    // Apply immutably
    let updated = apply!(old.clone(), &basic_patch).unwrap();
    assert_eq!(updated, new);

    // Apply mutably
    let mut current = old;
    apply_mut!(&mut current, &forced_patch).unwrap();
    assert_eq!(current, new);
}

Macros

  • diff!(old, new) – basic diff (only changed fields).
  • diff!(old, new; ["path.to.field", ...]) – include forced fields even if unchanged.
  • apply!(current, patch) – immutable.
  • apply_mut!(&mut current, patch) – mutable.