Field Decomposition

A document is never stored as one serialized blob. Instead, every top-level field becomes its own MMKV entry, keyed {document}::{field}:

// Note(title = "Pick up milk", body = "2%, not whole", done = false)
// stored as:
note-1::title -> bytes
note-1::body  -> bytes
note-1::done  -> bytes

The :: separator is reserved for this purpose, which is why it's rejected in a document key — see Opening Documents.

Why decompose

Decomposition is what makes field-level reactivity and truly cheap single-field writes possible. update(prop, value) and field delegates each touch exactly one decomposed key — a single putBytes or getBytes call, independent of how many other fields the document has, with no read or re-encoding of anything else.

update { current -> ... } is a different story: it's a full read-modify-write of the whole document — every field is decoded on the read side, and every field is re-encoded and rewritten on the write side, even when the builder only changes one of them. See Read & Write for when to reach for each shape, and Benchmarks for the measured cost difference between the two.

Nested types

A field whose type is itself a @Serializable object — like a nested Player inside a save file, or an address inside a user profile — is stored as a single serialized sub-blob under that one field's key, to any nesting depth. Nested types are not decomposed further in v1; only top-level fields get their own key.

How fields are discovered

The library walks each type's fields using kotlinx.serialization's SerialDescriptor — never reflection, which keeps this working identically on Kotlin/Native (iOS) as it does on the JVM. A custom encoder/decoder writes and reads each element to and from its own {doc}::{field} key instead of a single shared output buffer.