jdiff 1.0.0
jdiff: ^1.0.0 copied to clipboard
A pure Dart implementation of RFC 6902 JSON Patch and RFC 6901 JSON Pointer. Provides diff, patch, and apply operations for JSON-like Dart objects. Ideal for sync apps, offline-first architectures, an [...]
jdiff #
Pure Dart implementation of RFC 6902 JSON Patch and RFC 6901 JSON Pointer.
Send only what changed — not the entire document.
Save up to 99% bandwidth when updating large JSON objects.
Features #
| Feature | Status |
|---|---|
All 6 RFC 6902 operations (add, remove, replace, move, copy, test) |
✅ |
RFC 6901 JSON Pointer navigation (get, set, insert, remove, exists) |
✅ |
Automatic diff algorithm (JsonPatch.diff) |
✅ |
| Serialise / deserialise patches to/from JSON | ✅ |
Optimistic locking via test operation |
✅ |
Pure Dart — zero external dependencies |
✅ |
| Null-safe · Dart 3 · sealed classes | ✅ |
| Immutable — original document is never mutated | ✅ |
| 216 tests incl. full RFC 6902 Appendix A compliance suite | ✅ |
Getting Started #
Add to your pubspec.yaml:
dependencies:
jdiff: ^1.0.0
Then run:
dart pub get
Quick Start #
import 'package:jdiff/jdiff.dart';
void main() {
final oldDoc = {'name': 'Alice', 'age': 30, 'tags': ['dart']};
final newDoc = {'name': 'Alice', 'age': 31, 'tags': ['dart', 'flutter'], 'verified': true};
// ── 1. Compute the diff ────────────────────────────────────────────────
final ops = JsonPatch.diff(oldDoc, newDoc);
// ops = [
// ReplaceOperation(path: '/age', value: 31),
// AddOperation(path: '/tags/-', value: 'flutter'),
// AddOperation(path: '/verified', value: true),
// ]
// ── 2. Serialise for the wire ──────────────────────────────────────────
final raw = JsonPatch.toJsonList(ops);
// [{"op":"replace","path":"/age","value":31}, ...]
// ── 3. Apply the patch ─────────────────────────────────────────────────
final result = JsonPatch.apply(oldDoc, ops);
// {'name': 'Alice', 'age': 31, 'tags': ['dart', 'flutter'], 'verified': true}
// ── 4. Deserialise + apply from a server ───────────────────────────────
final fromServer = JsonPatch.applyJson(oldDoc, raw);
}
API Reference #
JsonPatch — High-level static API #
// Compute diff
List<PatchOperation> JsonPatch.diff(Object? source, Object? target)
// Apply a list of operations
Object? JsonPatch.apply(Object? document, List<PatchOperation> operations)
// Deserialise + apply from raw JSON list
Object? JsonPatch.applyJson(Object? document, List<dynamic> jsonList)
// Diff and apply in one shot
Object? JsonPatch.patch(Object? source, Object? target)
// Serialise operations to JSON
List<Map<String, dynamic>> JsonPatch.toJsonList(List<PatchOperation> operations)
// Deserialise from raw JSON
List<PatchOperation> JsonPatch.fromJsonList(List<dynamic> jsonList)
// Convenience single-operation helpers
Object? JsonPatch.add(Object? doc, String path, Object? value)
Object? JsonPatch.remove(Object? doc, String path)
Object? JsonPatch.replace(Object? doc, String path, Object? value)
Object? JsonPatch.move(Object? doc, {required String from, required String path})
Object? JsonPatch.copy(Object? doc, {required String from, required String path})
Object? JsonPatch.test(Object? doc, String path, Object? value)
JsonPointer — RFC 6901 navigation #
// Parse a pointer string
final ptr = JsonPointer.parse('/user/address/city');
// Construct from tokens
final ptr2 = JsonPointer.fromTokens(['user', 'address', 'city']);
// Build with .child()
final ptr3 = JsonPointer.root.child('user').child('name');
// Navigation
Object? value = ptr.get(document);
bool found = ptr.exists(document);
// Immutable mutation (returns new document)
Object? updated = ptr.set(document, newValue);
Object? inserted = ptr.insert(document, newValue); // array: inserts before
Object? removed = ptr.remove(document);
// Pointer arithmetic
JsonPointer parent = ptr.parent;
JsonPointer child = ptr.child('token');
bool isDesc = ptr.isDescendantOf(other);
Operations #
AddOperation(path: '/key', value: 42)
RemoveOperation(path: '/key')
ReplaceOperation(path: '/key', value: 'new')
MoveOperation(from: '/src', path: '/dst')
CopyOperation(from: '/src', path: '/dst')
TestOperation(path: '/key', value: 'expected')
All operations are const-constructable and support == / hashCode.
Use Cases #
Offline-first sync #
// On device: compute diff and queue it
final patch = JsonPatch.toJsonList(JsonPatch.diff(localDoc, updatedDoc));
queue.add(patch);
// On reconnect: send the patch (tiny payload!)
await api.patch('/documents/42', body: jsonEncode(patch));
Real-time collaboration via WebSocket #
// Sender
channel.sink.add(jsonEncode(JsonPatch.toJsonList(JsonPatch.diff(before, after))));
// Receiver
final ops = jsonDecode(message) as List;
sharedDoc = JsonPatch.applyJson(sharedDoc, ops);
Optimistic locking (MVCC) #
try {
final result = JsonPatch.apply(doc, [
TestOperation(path: '/version', value: currentVersion), // guard
ReplaceOperation(path: '/status', value: 'published'),
ReplaceOperation(path: '/version', value: currentVersion + 1),
]);
// ✅ Applied safely — no concurrent modification
} on JsonPatchTestFailedException {
// ❌ Document was concurrently modified — refetch and retry
}
HTTP PATCH endpoints #
// Standard REST: PATCH /api/users/42
// Content-Type: application/json-patch+json
// Body: [{"op":"replace","path":"/name","value":"Bob"}]
final incoming = jsonDecode(request.body) as List;
final patched = JsonPatch.applyJson(existingUser, incoming);
await db.save(patched);
Bandwidth Savings #
Document size: 10 000 bytes (100 fields)
Changed fields: 2
─────────────────────────────────────────
Full PUT body: 10 000 bytes
PATCH payload: 120 bytes (98.8% saved)
The formula:
$$\text{Savings} = \left(1 - \frac{|\text{Patch}|}{|\text{Document}|}\right) \times 100%$$
Error Handling #
try {
JsonPatch.apply(doc, ops);
} on JsonPatchTestFailedException catch (e) {
// test operation failed — optimistic lock violation
print('Expected: ${e.expected}, got: ${e.actual}');
} on JsonPatchConflictException catch (e) {
// move: path is a descendant of from
} on JsonPatchException catch (e) {
// all other patch errors (missing path, bad index, etc.)
} on JsonPointerException catch (e) {
// invalid pointer syntax
}
RFC 6902 Compliance #
All 17 official Appendix A test cases from the RFC plus an extended community
suite are included in test/rfc6902_compliance_test.dart.
License #
MIT — see LICENSE.