Cross-language metadata standard · Apache 2.0 · Installable in 5 languages
Make schema drift a compile-time error.
AI coding agents regenerate thousands of lines a day. Without a shared spine they produce inconsistent boilerplate at every prompt — and the drift surfaces as a production incident, not a compile error. MetaObjects is the spine: define typed entities once, generate idiomatic code across TypeScript, Java, Kotlin, C#.NET, and Python, and every generated stack breaks the type checker the moment the metadata moves underneath it.
meta gen — one metadata model, a full typed stack generated.
Four pillars. All shipping.
Codegen
One metadata model → idiomatic native code in five languages: Drizzle + Zod + Fastify/Hono (TS), Spring REST + DTO + JPA (Java), Exposed + KotlinPoet + Spring (Kotlin), EF Core + ASP.NET + Postgres DDL (C#), Pydantic + FastAPI + SQLAlchemy (Python). Hand-edit-preserving regen via three-way merge.
Runtime metadata
Load metadata at runtime and drive behavior dynamically — CRUD, validation, relationships, dynamic admin UIs, LLM tool registration. Less code in the repo; less surface area for AI agents to drift on.
Drift detection
meta verify validates every generated artifact against the metadata at build time. Renaming a field in one place breaks the type checker everywhere it's used — across services, across languages, across the human-and-AI-generated boundary.
Prompt construction
The prompt is code too — not a string scattered across services. Declare a typed payload as a projection, keep the text external and provider-resolved, render deterministically: snapshot-testable, cache-stable (no stray whitespace silently breaking exact-prefix prompt-cache hits), drift-checked at build time so a renamed field can't quietly degrade a prompt. Plus typed template.output parsers — Zod / Pydantic / Jackson / kotlinx.serialization / System.Text.Json across the ports.
The #1 reason in the AI era: drift becomes impossible.
AI coding agents regenerate thousands of lines a day. A field rename in one file misses three others — types compile, runtime breaks at the boundary, hours to chase. A required field added to an entity is forgotten in the migration, the validator, the form. A prompt template references {{userName}} while the payload type carries user.name. Without a shared spine, every prompt accelerates the entropy. MetaObjects inverts the source of truth: generated code is owned by the codegen, not by humans or agents.
Drift mode
Without MetaObjects
With MetaObjects
Rename created_at → createdAt in the schema; miss the validator + form binding + API doc + tests
Types compile. Runtime breaks at the boundary. Hours to chase.
Edit one YAML field, regen. Six files update consistently. Drift is structurally impossible.
Add a new entity (table + validator + types + queries + routes + barrel = 6 files)
AI misses one. App half-works.
One YAML drop + meta gen.
Prompt template references a field the payload type doesn't carry
Silent runtime failure in production.
meta verify catches at build time. CI fails before merge.
Required field added to entity; forgotten in migration / validator / form
Production NULL error.
CHECK constraint + Zod refinement + form validation regenerate from one declaration.
Enum transition hand-coded as string literals across 5 files; agent invents "completed" instead of "complete"
Silent string mismatch. Workflow stalls.
field.enum @values narrows the literal type. Compile error on first wrong character.
TS service renames a column; the Java consumer's POJO mapping silently goes stale
Boundary drifts. Cross-service schema mismatch in production.
Same metadata regenerates both stacks. Conformance corpus guarantees they agree byte-for-byte.
The win compounds with scale. For a 5k-line app, MetaObjects is overkill. For a 50k-line codebase maintained by humans-plus-agents, drift is the dominant cost of change — and MetaObjects pays back the setup cost within the first month.
Five ports. All installable today.
Language
Status
Details
TypeScript
npm 0.10.0 · reference
npm @metaobjectsdev/*@0.10.0 (the reference implementation). Drizzle + Zod + Fastify + Hono codegen, Kysely runtime, and meta migrate for Postgres / SQLite / Cloudflare D1 — the TS toolchain owns schema migrations. 2,500+ tests across the workspace.
NuGet MetaObjects / MetaObjects.Render / MetaObjects.Codegen + the dotnet meta tool (MetaObjects.Cli), all 0.10.0. EF Core entities + AppDbContext + ASP.NET minimal-API routes codegen; render engine + payload-VO codegen + verify. (Schema migrations are TS-owned.)
Python
PyPI 0.10.0
PyPI metaobjects0.10.0 + the metaobjects CLI. Loader + canonical serializer + render + verify + codegen (Pydantic + FastAPI + SQLAlchemy) + ObjectManager runtime. All five cross-port conformance corpora green.
Five shared conformance corpora at fixtures/ — metamodel (~90 fixtures), render, persistence (12/12 against Testcontainers Postgres), API contract (20/20 cross-port), YAML — validate every implementation byte-identically against shared expectations.
From one schema, five languages.
A typed entity in metadata, side-by-side with what every port actually generates. Each output is idiomatic for that language — Drizzle + Zod for TypeScript, JPA + Spring records for Java, Exposed + KotlinPoet data classes for Kotlin, EF Core + ASP.NET records for C#, SQLAlchemy + Pydantic for Python. Same metadata; five idiomatic outputs; conformance-gated to byte-identical canonical form.
// generated/SubscriberDto.javapackage acme;
public record SubscriberDto(
Long id,
String email,
String name,
SubscriberStatus status,
java.time.Instant createdAt
) {}
public enum SubscriberStatus { ACTIVE, PAUSED, CANCELLED }
// generated/SubscriberController.java (excerpt)
@RestController
@RequestMapping("/api/subscribers")
public class SubscriberController {
@GetMapping("/{id}")
public SubscriberDto findById(@PathVariable Long id) { /* ... */ }
@GetMapping
public Page<SubscriberDto> list(@RequestParam Map<String, String> filter,
Pageable pageable) { /* ... */ }
@PostMapping public SubscriberDto create(@RequestBody SubscriberDto dto) { /* ... */ }
@PutMapping("/{id}") public SubscriberDto update(...) { /* ... */ }
@DeleteMapping("/{id}") public void delete(@PathVariable Long id) { /* ... */ }
}
Kotlin — Exposed + KotlinPoet data class from codegen-kotlin
// generated/Subscriber.ktpackage acme
import java.time.Instant
import kotlinx.serialization.Serializable
@Serializable
public data class Subscriber(
public val id: Long? = null,
public val email: String,
public val name: String? = null,
public val status: SubscriberStatus,
public val createdAt: Instant? = null,
)
@Serializable
public enum class SubscriberStatus { ACTIVE, PAUSED, CANCELLED }
// generated/SubscriberTable.ktobject SubscriberTable : Table("subscribers") {
val id = long("id").autoIncrement()
val email = varchar("email", 320)
val name = varchar("name", 255).nullable()
val status = enumerationByName("status", 20, SubscriberStatus::class)
val createdAt = timestamp("created_at").nullable()
override val primaryKey = PrimaryKey(id)
}
C#.NET — EF Core + ASP.NET from MetaObjects.Codegen
// generated/Subscriber.csnamespace Acme;
public partial class Subscriber {
public long Id { get; set; }
public string Email { get; set; } = null!;
public string? Name { get; set; }
public SubscriberStatus Status { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
public enum SubscriberStatus { Active, Paused, Cancelled }
// generated/AppDbContext.cs (excerpt)public partial class AppDbContext : DbContext {
public DbSet<Subscriber> Subscribers => Set<Subscriber>();
protected override void OnModelCreating(ModelBuilder b) {
b.Entity<Subscriber>(e => {
e.HasKey(s => s.Id);
e.Property(s => s.Email).IsRequired().HasMaxLength(320);
e.Property(s => s.Status).HasConversion<string>();
});
}
}
Python — SQLAlchemy + Pydantic + FastAPI from metaobjects.codegen
# generated/subscriber.pyfrom datetime import datetime
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from .base import Base
class SubscriberStatus(str, Enum):
ACTIVE = "active"
PAUSED = "paused"
CANCELLED = "cancelled"class Subscriber(Base):
__tablename__ = "subscribers"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
email: Mapped[str] = mapped_column(String(320))
name: Mapped[Optional[str]]
status: Mapped[SubscriberStatus]
created_at: Mapped[datetime]
class SubscriberInsert(BaseModel):
email: str = Field(max_length=320)
name: Optional[str] = None
status: SubscriberStatus
Plus a migration — dialect-aware (Postgres / SQLite / Cloudflare D1)
-- migrations/0001_create_subscribers.up.sql (SQLite dialect)CREATE TABLE subscribers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email VARCHAR(320) NOT NULL,
name TEXT,
status VARCHAR(20) NOT NULL CHECK (status IN ('active', 'paused', 'cancelled')),
created_at TIMESTAMP NOT NULL
);
And the three CLI commands you actually run
# Scaffold metaobjects/ + .metaobjects/ + metaobjects.config.ts
$ meta init
# Generate code (entities, validators, queries, routes, payload VOs, output parsers)
$ meta gen
# Diff metadata vs live DB; emit migration SQL (up + down)
$ meta migrate --db file:./dev.db --slug add-subscriber
Conformance fixtures (JSON form): fixtures/conformance/. YAML and JSON are equivalent — pick whichever your stack prefers.
The metamodel goes deep.
Fields aren't just columns. They carry validators, views, and currency formatting as child metadata. Entities derive read-only projections — passthrough fields and aggregates over relationships — that generate as database views, not tables. Everything below is real meta gen output, conformance-gated, not hand-wired annotations.
The source: rich child metadata on fields + a derived projection
Projection → a read-only database view, not a table (generated/AuthorSummary.ts)
// No Insert/Update schema — a projection is read-only by construction.export const authorSummaryView = sqliteView("v_author_summary", {}).existing();
export const AuthorSummarySchema = z.object({
id: z.number().int(),
postCount: z.number().int(), // COUNT(Post.id) via the Author→posts relationship
});
Run it yourself: this exact model loads and generates a full typed stack in the TypeScript reference implementation with zero errors and zero warnings — and every construct it uses (projections, origins, currency views, field-level validators) is conformance-gated across all five ports.
How MetaObjects compares.
For a single-language project that doesn't talk to LLMs at scale, the popular per-language ORMs are excellent and likely enough. MetaObjects adds value when (a) your codebase is touched by AI agents at volume, (b) you ship in more than one language, or (c) you have typed prompts and tool-calls that need governance. Honest comparison, by language:
Same Drizzle + Zod output, plus four other languages from the same source. Plus typed prompt rendering, output parsers, tool-call envelopes, and meta verify drift gates.
The same JPA-ready entities, plus the controller/DTO/repository layer, plus four other languages from the same source. Runtime reflection-free codegen is friendlier to GraalVM native-image and to AI grep.
KotlinPoet-generated @Serializable data classes + Exposed Tables from the same metamodel that drives the Java / TS / Python / C# stacks. Same Kotlin idioms; cross-port lockstep.
Microsoft-supported, deeply integrated with ASP.NET (EF Core). Lightweight micro-ORM speed (Dapper).
EF Core entities + AppDbContext + ASP.NET routes from the metamodel. Plus the cross-port REST API contract — your C# server agrees with the TS / Java / Kotlin / Python services on filter operators, pagination envelopes, and 404 shapes.
SQLAlchemy declarative models + Pydantic schemas + FastAPI routers from the same metamodel that drives every other port. Same Pythonic outputs; same drift guarantees.
Describes the whole stack, not just the API shape: persistence + render + tool-calls + UI bindings + validation, all from one declaration. IDLs give you type stubs; MetaObjects gives you the table, the validator, the route, the prompt template, and the migration.
Capability matrix — including AI-agent suitability.
Where it matters: the right-hand four columns are the AI-coding-agent dimensions. They were the design driver, not an afterthought.
Generated code is plain (no proxy/decorator magic)
idiomatic native
idiomatic
proxy/runtime magic
type stubs
Deterministic regen (same in → same out)
conformance-gated
yes
yes
yes
Small surface area for agent context window
11 base types + extensions
single-lang ORM API
large framework surface
small spec
The honest read: for a 5k-line TS app with no LLM integration, Drizzle is probably enough. For a 50k-line codebase maintained by humans-plus-agents, drift is the dominant cost of change — and MetaObjects' build-time drift gate alone justifies the setup. For a multi-port team (TS + Java, or TS + Python, or all five), no other system in this matrix does cross-language metadata sharing at all.
Built so AI agents work at the metadata layer.
Every contract is grep-friendly (named constants, never magic strings). Generated code is idiomatic per-language, not framework-magic. An llms.txt index ships at the root so any agent fluent in the convention can index the full standard in seconds. The substrate is designed so Claude Code, Cursor, GitHub Copilot, and Windsurf edit metadata files — and the codegen handles every language behind them.
/llms.txt — short index following the Answer.AI llms.txt convention
/llms-full.txt — full Markdown dump of spec + quickstart material
CLAUDE.md in the repo — agent instructions for working in the metaobjects codebase itself
Why Claude Code in particular. Anthropic's official CLI was the day-zero driver for the standard's design choices. The metamodel vocabulary is small enough to fit in a context window; the conformance contract is enforced as compile-time errors rather than runtime drift; and the generated code is the same idiomatic per-language output a senior engineer would write by hand, so an agent's review pass converges fast. A first-party MCP server exposing the spec, conformance fixtures, and codegen tools as agent-callable functions is on the roadmap.