Declaring Your Data Class

Every Document<T> is backed by an @Serializable class you write. How you declare that class's properties — nullable or not, with a constructor default or without — decides what happens the first time update() runs against a document that hasn't been written yet. Get this wrong and the failure shows up as a runtime crash on a button tap, not a compile error.

The rule

Documents never invents a value for a field. It only ever uses one of two sources: a value you already stored, or a default you already declared in the constructor. When neither exists, it fails loudly instead of guessing — there is no "correct" placeholder the library could make up for a field like title: String or count: Int.

@Serializable
data class Note(val title: String = "", val count: Int = 0) // safe: every field has a default

Every field in a document type should have either a constructor default or a nullable type (= null). A field with neither is only safe once you're certain the document is always set() before it's ever update()-d.

Why this matters: update() on an absent document

update { current -> ... } needs a baseline to run the builder against. If the document already exists, that baseline is the current value. If it doesn't exist yet — nothing has ever been set() for this key — the baseline comes from decoding the type as if every field were absent, which only works for fields that have somewhere to fall back to:

This is the same rule whether the missing field is a String, an Int, a nested @Serializable type, or an enum — the library has no way to know what value you consider "correct" for a required field, so it never guesses on your behalf.

A first-run crash, concretely

This type looks reasonable and compiles fine:

@Serializable
data class Note(val title: String, val count: Int) // no defaults, both non-null

It works perfectly for set() and get(). But call update() on it before it's ever been written — for example, incrementing a counter on the very first button tap in a fresh app install — and it crashes:

val document = Documents.document<Note>("note")

// Crashes if "note" was never set() before — there's no stored title
// and no default to fall back to.
document.update { it.copy(count = it.count + 1) }
com.nomemmurrakh.documents.DocumentDecodingException: Failed to decode field 'title' of document 'note'
Note

That message is more generic than it should be — it reads identically to a genuinely corrupted field (see Error Handling), even though this case has a distinct, knowable cause: a missing constructor default. A clearer, dedicated message for this specific case is planned; until then, if you see DocumentDecodingException the very first time you call update() on a document, suspect a missing default before suspecting corrupted data.

The fix is either of:

// Give every field a constructor default
data class Note(val title: String = "", val count: Int = 0)

// Or make the field nullable, which defaults to null
data class Note(val title: String?, val count: Int = 0)

// Or guarantee set() runs before update() ever does
document.set(Note(title = "Untitled", count = 0))
document.update { it.copy(count = it.count + 1) } // safe now

What "optional" means here

This has nothing to do with Kotlin's own optional-parameter syntax beyond the fact that it's what triggers it: a field only gets treated as having a fallback when the constructor supplies a default value expression. A type can be non-nullable and still be safe as long as it has a default; a type can be nullable and safe with no default at all, because null itself is a valid fallback. The two are independent — pick whichever reads better for the field's meaning.

See Opening Documents for what else T can and can't be, and Read & Write for the full get/set/update picture.