jdiff 1.0.0 copy "jdiff: ^1.0.0" to clipboard
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 #

pub.dev License: MIT RFC 6902 RFC 6901 Dart 3

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.

0
likes
150
points
92
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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, and bandwidth-efficient APIs.

Topics

#json #patch #diff #rfc6902 #sync

License

MIT (license)

More

Packages that depend on jdiff