kuilt Help

JsonCrdt

A full JSON document that merges. Objects use ORMap (add-wins keys), arrays use Rga (stable insertion order), and scalar values use MVRegister (surfaces conflicts). Concurrent edits at any depth — nested objects, array items, scalar fields — all resolve automatically.

Converges to: the same document on every replica — concurrently-added keys are all preserved (add-wins), array elements keep a stable order by insertion id, and concurrent scalar writes surface together as a multi-value the caller resolves.

Structure: ORMap + RGA + MVRegister

JsonNode = Object(ORMap<String, JsonNode>) // add-wins keys, recursive values | Array(Rga<JsonNode>) // insertion-ordered, stable ids | Leaf(MVRegister<JsonValue>) // scalar, multi-value on conflict

piece recurses: nested objects merge key-by-key, arrays union their operation logs, and leaves merge as multi-value registers. The three lattice laws (idempotent, commutative, associative) hold at every depth.

Add-wins keys and multi-value leaves

A key added concurrently with a remove survives — the add wins. When two replicas write different scalars to the same key concurrently, the leaf becomes a multi-value register holding both, so no write is silently lost; the caller picks a winner.

Code examples

Set and read a scalar:

@Test fun setThenGet() { val doc = JsonCrdt.empty(a).set("name", str("Alice")) assertIs<JsonNode.Leaf>(doc["name"]) assertEquals(setOf(JsonValue.Str("Alice")), (doc["name"] as JsonNode.Leaf).register.values) }

Concurrent edits to a nested object both survive:

/** * Replica A adds "name" to "profile"; replica B concurrently adds "age". * Both diverge from a shared base. After merge both keys should be present. * * Each replica uses its own [ReplicaId] when modifying the inner ORMap so * their dot spaces don't collide. */ @Test fun nestedObjectMerge() { val base = JsonCrdt.empty(a).set("profile", JsonNode.Object(ORMap.empty())) val docA = base.set("profile", obj(a, "name" to str("Alice"))) val docB = base.withReplica(b).set("profile", obj(b, "age" to num(30.0))) val merged = docA.piece(docB) val profile = assertIs<JsonNode.Object>(merged["profile"]) assertEquals(setOf("name", "age"), profile.map.keys) }

A concurrent add wins over a remove:

@Test fun addWinsOverConcurrentRemove() { val base = JsonCrdt.empty(a).set("x", str("hello")) val docA = base.remove("x") val docB = base.withReplica(b).set("x", str("world")) val merged = docA.piece(docB) assertContains(merged.keys, "x") }

Concurrent scalar writes surface as a multi-value:

@Test fun concurrentScalarWritesProduceMultiValue() { val base = JsonCrdt.empty(a) val docA = base.set("flag", JsonNode.Leaf(MVRegister.empty<JsonValue>().set(a, JsonValue.Str("x")))) val docB = base.withReplica(b).set("flag", JsonNode.Leaf(MVRegister.empty<JsonValue>().set(b, JsonValue.Str("y")))) val merged = docA.piece(docB) val leaf = assertIs<JsonNode.Leaf>(merged["flag"]) assertEquals(setOf(JsonValue.Str("x"), JsonValue.Str("y")), leaf.register.values) }

When to use

JsonCrdt fits collaborative JSON documents — config, metadata, document editors — where concurrent edits to nested structure must converge. For a flat key-value map, LWWMap is lighter; for an add-wins set of keys whose values are themselves CRDTs, use ORMap directly.

Last modified: 22 June 2026