Skip to content
Kona

Serialization, collections & events

Models turn themselves into maps and JSON for APIs and views.

API Role
toArray() / toMap() Cast attributes + loaded relations as nested maps
toJson() JSON string of toMap()
hidden Attribute names omitted from serialization
visible When non-empty, only these attributes are included (minus hidden overrides)
appends Virtual keys filled by getAppendedAttribute
makeHidden / makeVisible / setAppends Per-instance overrides
class User : Cadence<User>() {
override val hidden = setOf("password")
override val appends = setOf("display_name")
override fun getAppendedAttribute(key: String): Any? =
if (key == "display_name") getAttribute("name") else null
}
val payload = user.toMap()
Response.json(user.toMap())

Loaded relations appear under their relation names. Soft-deleted timestamps and other cast columns use their cast Java types in the map (for example Instant).

get() returns a List. Prefer getCollection() when you want Cadence helpers:

val users = User.query().where("admin", true).getCollection()
users.pluck("email")
users.modelKeys()
users.find(1)
users.load("posts")
users.loadMissing("roles")
users.filter { it["admin"] == true }
users.toArray()
users.toMapKeyedById()

CadenceCollection.of(list) / CadenceCollection.make(…) wrap an existing list. Mutations that load relations update the same model instances you already hold.

ModelDispatcher is a process-wide registry for persistence hooks. Events:

Event When
RETRIEVED After a model is hydrated from a row
CREATING / CREATED Before / after insert
UPDATING / UPDATED Before / after update
SAVING / SAVED Around every save (including clean no-op updates)
DELETING / DELETED Around delete
TRASHED Soft-delete completed
RESTORING / RESTORED Soft-delete restore
FORCE_DELETING / FORCE_DELETED Permanent delete on a soft-deleting model
ModelDispatcher.listen(ModelEvent.CREATED) { model ->
logger.info("created {}#{}", model.javaClass.simpleName, model.getKey())
}
ModelDispatcher.withoutEvents {
user.save() // no listeners run
}
user.saveQuietly()
user.deleteQuietly()

Call ModelDispatcher.flush() in tests to clear listeners between cases. Nested withoutEvents blocks nest safely (a depth counter).

For model-scoped hooks, extend ModelObserver and register it once:

class UserObserver : ModelObserver<User>() {
override fun creating(model: User) {
model["email"] = model["email"].toString().lowercase()
}
override fun deleted(model: User) {
logger.info("removed user {}", model.getKey())
}
}
User.observe(UserObserver())
// or Cadence.observe(User::class.java, UserObserver())

Observers only receive instances of their model type. Override any lifecycle method (retrieved, creating, created, updating, updated, saving, saved, deleting, deleted, trashed, restoring, restored, forceDeleting, forceDeleted). Under the hood they register on ModelDispatcher, so flush() / withoutEvents / saveQuietly apply the same way.