rust 113 lines · 3 tabs

Diffing Two Config Snapshots Into a Typed Change List in Rust

Shared by codesnips Aug 2026
3 tabs
use serde::Serialize;
use serde_json::Value;

#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum ChangeKind {
    Added { value: Value },
    Removed { value: Value },
    Modified { from: Value, to: Value },
}

#[derive(Debug, Clone, Serialize)]
pub struct Change {
    pub path: String,
    #[serde(flatten)]
    pub kind: ChangeKind,
}

impl Change {
    pub fn added(path: String, value: Value) -> Self {
        Change { path, kind: ChangeKind::Added { value } }
    }

    pub fn removed(path: String, value: Value) -> Self {
        Change { path, kind: ChangeKind::Removed { value } }
    }

    pub fn modified(path: String, from: Value, to: Value) -> Self {
        Change { path, kind: ChangeKind::Modified { from, to } }
    }
}
3 files · rust Explain with highlit

This snippet shows how to compare two configuration snapshots and produce a flat, machine-readable list of changes rather than a raw textual diff. The core idea is to treat config as a generic tree of serde_json::Value nodes and walk both trees in lockstep, emitting a typed record whenever a leaf appears, disappears, or changes value. This is the pattern that powers config drift detection, audit logs, and "what changed between deploys" tooling.

In change.rs, a Change struct pairs a dotted path (like server.limits.max_connections) with a ChangeKind enum. Modeling the operation as an enum — Added, Removed, Modified — instead of stashing loose strings makes downstream consumers exhaustive: a formatter or an alert rule must handle every variant. The Modified variant carries both from and to so callers can render before/after without re-reading the snapshots. Deriving Serialize means the whole change list can be shipped as JSON to a UI or a webhook unchanged.

In differ.rs, diff is the public entry point that seeds an empty path and delegates to the recursive diff_value. The recursion has three shapes. When both sides are objects, diff_objects collects the union of keys via a BTreeSet — the sorted set guarantees deterministic output, which matters for stable diffs in tests and audit trails — then recurses per key, extending the path segment by segment. When a key exists on only one side it becomes an Added or Removed. For non-object leaves (or type mismatches, like an object becoming a scalar), the values are compared directly and a single Modified is emitted when they differ.

A deliberate trade-off is that arrays are compared by whole-value equality rather than element-wise. Element diffing requires a matching heuristic (by index? by id?) that has no universal right answer, so treating an array as an opaque leaf keeps the algorithm honest and predictable. join_path builds dotted paths and skips the leading separator so the root's children read cleanly.

In main.rs, two inline JSON snapshots are parsed and diffed, and the sorted results are printed. The empty-diff case is handled explicitly so equal snapshots report "no changes" instead of silence. Because the output is pure data, the same Vec<Change> could just as easily be serialized, filtered by path prefix, or fed into a policy engine.


Related snips

Share this code

Here's the card — post it anywhere.

Diffing Two Config Snapshots Into a Typed Change List in Rust — share card
Link copied