An entity is a typed, named record shape declared in metadata. It is the unit of
codegen, the unit of persistence, and the unit of API exposure. Every port turns the
same object.entity declaration into idiomatic native code — a TS interface + Zod
schema, a Java POJO, a Kotlin data class, a C# record, a Python @dataclass.
The entity is metadata-first: you don't write the class, the loader reads the declaration, the codegen emits the class, and the runtime layer reads the same declaration to drive CRUD, validation, and relationships. Pattern-derivable code (FK columns, validator chains, type-safe finders) is generated from the metadata; business logic stays hand-written.
{
"metadata.root": {
"package": "acme::blog",
"children": [
{
"object.entity": {
"name": "Author",
"children": [
{ "source.rdb": { "@table": "authors" } },
{ "field.long": { "name": "id" } },
{ "field.string": { "name": "name", "@required": true, "@maxLength": 200 } },
{ "field.string": { "name": "bio", "@maxLength": 2000 } },
{ "identity.primary": { "@fields": "id", "@generation": "increment" } }
]
}
}
]
}
}metadata:
package: acme::blog
children:
- object.entity:
name: Author
children:
- source.rdb:
table: authors
- field.long:
name: id
- field.string:
name: name
required: true
maxLength: 200
- field.string:
name: bio
maxLength: 2000
- identity.primary:
fields: id
generation: incrementYAML is desugared to canonical JSON at load time. See yaml-authoring.md for the desugar rules.
| Child node | Purpose | Required |
|---|---|---|
source.rdb |
Declares physical storage (table/view/storedProc). See source-kinds.md. | Yes for persisted entities; abstract entities may omit. |
field.<subtype> |
One field per typed column. See field-types.md. | At least one. |
relationship.composition |
An FK relationship to another entity. See relationships.md. | No. |
identity.primary |
Designates the PK field(s). | Yes for persisted entities. |
identity.secondary |
Unique secondary index. | No. |
identity.reference |
Inbound FK from this entity to another. | No. |
identity.primaryis a singleton — its name is optional. An entity may carry at most oneidentity.primary; the loader names a name-less one"primary"automatically (so{ "identity.primary": { "@fields": "id" } }is the canonical minimal form — no need to invent a name). Declaring two primaries on one entity is anERR_TOO_MANY_OCCURRENCESload error.identity.secondary/identity.referenceare not singletons and DO require an explicitname.
Common base fields (id, createdAt, updatedAt) live on an abstract entity that
concrete entities extend. The loader resolves extends: after all files load, so
the base can live in any file in the corpus.
{
"object.entity": {
"name": "BaseEntity",
"abstract": true,
"children": [
{ "field.long": { "name": "id" } },
{ "field.timestamp": { "name": "createdAt", "@required": true } }
]
}
}{
"object.entity": {
"name": "Author",
"extends": "BaseEntity",
"children": [
{ "field.string": { "name": "name", "@required": true } },
{ "identity.primary": { "@fields": "id" } }
]
}
}@metaobjectsdev/codegen-ts emits one file per entity with a Drizzle table, a Zod
schema, a TS type, and (with queriesFile() + routesFile()) typed finders and
Fastify routes.
// generated/acme/blog/Author.ts
import { pgTable, bigserial, varchar } from "drizzle-orm/pg-core";
import { z } from "zod";
export const author = pgTable("authors", {
id: bigserial("id", { mode: "number" }).primaryKey(),
name: varchar("name", { length: 200 }).notNull(),
bio: varchar("bio", { length: 2000 }),
});
export const AuthorSchema = z.object({
id: z.number(),
name: z.string().max(200),
bio: z.string().max(2000).optional(),
});
export type Author = z.infer<typeof AuthorSchema>;metaobjects-maven-plugin with MetaDataGenerator writes a POJO + an OMDB-backed
descriptor. The OMDB ObjectManager reads the same metadata at runtime to drive
CRUD.
// generated/acme/blog/Author.java
package acme.blog;
public class Author {
private Long id;
private String name; // required, maxLength=200
private String bio; // optional, maxLength=2000
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getBio() { return bio; }
public void setBio(String bio) { this.bio = bio; }
}metaobjects-codegen-kotlin (KotlinEntityGenerator) emits a plain
data class (no @Serializable — entities are Jackson-compatible, not
kotlinx-serialized) and (with KotlinExposedTableGenerator) an Exposed
Table object.
// generated/acme/blog/Author.kt
package acme.blog
data class Author(
val id: Long,
val name: String,
val bio: String? = null,
)// generated/acme/blog/AuthorTable.kt
package acme.blog
import org.jetbrains.exposed.sql.Table
object AuthorTable : Table("authors") {
val id = long("id").autoIncrement()
val name = varchar("name", 200)
val bio = varchar("bio", 2000).nullable()
override val primaryKey = PrimaryKey(id)
}MetaObjects.Codegen (via dotnet meta gen) emits an EF Core entity record, a
DbSet on the generated AppDbContext, and a CREATE TABLE in the migration
emitted by the Node meta migrate (schema is Node-owned, ADR-0015).
// generated/Acme/Blog/Author.cs
namespace Acme.Blog;
public record Author
{
public long Id { get; set; }
public string Name { get; set; } = string.Empty; // required, maxLength=200
public string? Bio { get; set; } // optional, maxLength=2000
}// generated/AppDbContext.cs (excerpt)
public partial class AppDbContext : DbContext
{
public DbSet<Author> Authors => Set<Author>();
}metaobjects.codegen emits a Python @dataclass per entity. Persistence
(SQLAlchemy + FastAPI) is on the roadmap; the runtime tier is in progress.
# generated/acme/blog/author.py
from dataclasses import dataclass
from typing import Optional
@dataclass
class Author:
id: int
name: str # required, max_length=200
bio: Optional[str] = NoneThe following conformance fixtures gate this feature's behavior across ports:
Basic entity loading
fixtures/conformance/smoke-empty-metadata/— empty metadata loads without errorfixtures/conformance/loader-basic-single-entity/— singleobject.entityround-tripfixtures/conformance/subtype-entity-with-identity/—object.entityacceptsidentity.primaryfixtures/conformance/subtype-entity-missing-primary-warning/— warning when an entity has no primary identityfixtures/conformance/subtype-value-without-identity/—object.valuedoes NOT require identity
Inheritance via extends:
fixtures/conformance/extends-single-level/— basicextendsresolutionfixtures/conformance/extends-multi-level/— multi-hop ancestor chainfixtures/conformance/extends-abstract-base/—abstract: trueparents are not instantiablefixtures/conformance/extends-cross-file/— deferred resolution across filesfixtures/conformance/error-extends-nonexistent/—ERR_UNRESOLVED_SUPERwhen parent missing
Identity
fixtures/conformance/identity-primary-and-secondary/— multiple identities on one entityfixtures/conformance/identity-reference-simple/— FK viaidentity.reference
Attributes (@-prefixed)
fixtures/conformance/attr-properties-basic/— typed attr parsingfixtures/conformance/attr-default-polymorphic/—@defaultadopts the field's value typefixtures/conformance/error-attr-missing-required/—ERR_MISSING_REQUIRED_ATTRfixtures/conformance/error-attr-wrong-type/—ERR_BAD_ATTR_VALUEon type mismatchfixtures/conformance/error-attr-bad-allowed-value/— out-of-enumeration attr value rejectedfixtures/conformance/error-reserved-word-as-attr/—ERR_RESERVED_ATTRon@-prefixing a reserved word
Documentation common attrs (description, notes, title, …)
fixtures/conformance/doc-common-attrs-basic/— single-line descriptionfixtures/conformance/doc-common-attrs-multiline/— multi-line description preservedfixtures/conformance/doc-common-attrs-on-all-types/— common attrs accepted on every node kindfixtures/conformance/doc-common-attrs-stringarray-shapes/—seeAlso/aliasesarray shapes
Auto-set timestamps
fixtures/conformance/auto-set-on-create/—@autoSetOnCreatehonoredfixtures/conformance/auto-set-on-update/—@autoSetOnUpdatehonoredfixtures/conformance/auto-set-on-create-and-update/— both flags coexist
Filter / sort / data-grid layout
fixtures/conformance/attr-filter-shorthand/—@filterable: trueshorthandfixtures/conformance/attr-filter-explicit-ops/— explicit op-list formfixtures/conformance/error-attr-filter-bad-field/—@filterableon a non-field rejectedfixtures/conformance/error-attr-filter-bad-op/— unknown operator rejectedfixtures/conformance/error-attr-filter-legacy-string/— legacy string form rejectedfixtures/conformance/loader-filterable-on-indexed-no-warning/— indexed field +@filterableis silentfixtures/conformance/warning-filterable-no-index/—@filterablewithout an index emits a warningfixtures/conformance/layout-data-grid-basic/—layout.dataGridcolumns + sortfixtures/conformance/layout-data-grid-multiple-named/— multiple named grids per entityfixtures/conformance/error-data-grid-bad-sort-field/—@defaultSortFieldmust be a real field
Overlay / merge
fixtures/conformance/overlay-same-object-different-files/— samepackage+namemerge across filesfixtures/conformance/overlay-attr-last-writer-wins/— attr conflict resolutionfixtures/conformance/overlay-merge-flag-explicit/—overlay: trueis explicit-merge-intent
Cross-port runner coverage: TS / Java / Kotlin / C# / Python all execute these
via their respective conformance runners. See docs/CONFORMANCE.md
for the per-port pass/skip ledger.
- field-types.md — all
field.*subtypes and their per-port mappings - source-kinds.md —
source.rdb@kind(table / view / storedProc / tableFunction) - relationships.md — composition + identity.reference for FKs
- migrations-and-drift.md — how the codegen output evolves with metadata