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:
- Field has a constructor default (
val count: Int = 0) — the default is used. This works. - Field is nullable with no default (
val nickname: String?) — it resolves tonull. This also works; nullability itself is a built-in fallback. - Field is non-null with no default (
val title: String) — there is nothing to fall back to.update()throwsDocumentDecodingException.
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'
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.