ORM Serialization
Introduction
When you build APIs or pass models into views, you often need a plain object instead of a live model instance. models expose toArray(), toJSON(), and toJson() for that conversion. Loaded relations that already sit on the instance are included. For a curated public API shape, prefer API Resources.
import User from "@/Models/User.ts";
const user = await User.with("roles").first();
return user!.toArray();Serializing models and collections
Serializing to arrays
toArray() walks own attributes (skipping functions), drops static hidden keys, runs static appends / instance appends, and converts Date values to ISO-8601 strings:
const user = await User.find(1);
user!.toArray();
// { id: 1, name: "Ada", email: "ada@example.com", created_at: "2026-01-15T12:00:00.000Z", ... }For a list of models, map each instance (or use a resource collection):
const users = await User.query().get();
users.all().map((user) => user.toArray());Serializing to JSON
toJSON() returns the same object as toArray(). toJson() is an alias of toJSON() — both return a plain object; call JSON.stringify when you need a string:
const user = await User.find(1);
user!.toJSON();
JSON.stringify(user); // uses `toJSON` when the runtime serializes the modelRelationships
Only relations that are already present on the instance appear in the serialized output — typically after with(...), load(...), or assignment. Relation method names are not invoked during serialization.
const user = await User.with("posts").find(1);
user!.toArray().posts; // array / collection of related models' attributes when loadedHiding attributes from JSON
List attribute (or relation) names on static hidden. Those keys are omitted from toArray() / toJSON():
import { Model } from "@bunyad/orm";
export default class User extends Model {
static table = "users";
static hidden = ["password", "remember_token"];
declare name: string;
declare email: string;
declare password?: string;
}const user = await User.create({
name: "Ada",
email: "ada@example.com",
password: "secret",
});
user.toArray().password; // undefined
user.toArray().email; // "ada@example.com"Hidden attributes remain available on the live model for application code (user.password). Hiding only affects serialization.
Appending values to JSON
Appended attributes are computed values that are not (or not only) database columns. Define a get{Studly}Attribute method and list the snake_case name on static appends:
import { Model } from "@bunyad/orm";
export default class Shop extends Model {
static table = "shops";
static appends = ["display_name"];
declare name: string;
getDisplayNameAttribute() {
return `Shop: ${this.name}`;
}
}const shop = await Shop.where("name", "Open").first();
shop!.toArray().display_name; // "Shop: Open"Appended keys respect hidden. If a name appears in both lists, it is omitted.
Appending at run time
Add or replace appends on one instance:
user.append("is_admin");
user.append("is_admin", "status");
user.setAppends(["is_admin"]);
user.getAppends(); // ["is_admin"]append merges into the current list (class defaults plus prior instance appends). setAppends replaces the instance list entirely.
Accessors used only through appends are invoked during toArray(). They are not automatically available as live properties unless you also expose them another way (for example an Attribute cast — see Mutators and Casting).
Date serialization
Date attribute values become ISO-8601 strings inside toArray():
static casts() {
return {
birthday: "date" as const,
published_at: "datetime" as const,
};
}
const post = await Post.find(1);
post!.toArray().published_at; // "2026-03-01T08:30:00.000Z"Cast definitions control how dates are stored and hydrated. Serialization always uses Date.prototype.toISOString() for values that are Date instances at serialize time.
Casting and serialization together
Casts run on hydrate and save. Serialization reads the already-cast in-memory values (booleans stay booleans, arrays stay arrays, Collection instances are left as objects unless you map them in a resource).
static casts = {
active: "boolean" as const,
meta: "json" as const,
};
static hidden = ["secret"];
const profile = await Profile.find(1);
profile!.toArray();
// { id: 1, name: "Ada", active: true, meta: { theme: "dark" }, ... }When to use resources instead
| Approach | Use when |
|---|---|
toArray() / toJSON() |
Internal dumps, simple admin JSON, debugging |
JsonResource |
Public APIs, per-endpoint field sets, conditional relations |
Resources can still call model.toArray() inside toArray() when you want most columns plus a few overrides — but most APIs map fields explicitly for stability.