Serialization, collections & events
Serialization
Section titled “Serialization”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())public class User extends Cadence<User> { @Override public Set<String> getHidden() { return Set.of("password"); }
@Override public Set<String> getAppends() { return Set.of("display_name"); }
@Override public Object getAppendedAttribute(String key) { return "display_name".equals(key) ? getAttribute("name") : null; }}
Map<String, Object> 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).
Collections
Section titled “Collections”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.
Lifecycle events
Section titled “Lifecycle events”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()ModelDispatcher.listen(ModelEvent.CREATED, model -> { logger.info("created {}#{}", model.getClass().getSimpleName(), model.getKey());});
ModelDispatcher.withoutEvents(() -> user.save());user.saveQuietly();user.deleteQuietly();Call ModelDispatcher.flush() in tests to clear listeners between cases. Nested withoutEvents blocks nest safely (a depth counter).
Observer classes
Section titled “Observer classes”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())public class UserObserver extends ModelObserver<User> { @Override public void creating(User model) { model.set("email", String.valueOf(model.get("email")).toLowerCase()); }}
Cadence.observe(User.class, new UserObserver());// or User.QUERY.observe(new 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.