Cross-language metadata standard · Apache 2.0 · 7.0.0 on Maven Central + npm

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.

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.

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_atcreatedAt 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.

Language Status Details
TypeScript reference npm @metaobjectsdev/*@7.0.0-rc. Drizzle + Zod + Fastify + Hono codegen, Kysely runtime, meta migrate for Postgres / SQLite / Cloudflare D1. 2,500+ tests across the workspace.
Java 7.0.0 on Central com.metaobjects:metaobjects-*:7.0.0 on Maven Central. Spring REST + DTO + Repository + filter-allowlist codegen via codegen-spring; OMDB persistence engine with diff-and-converge meta migrate; render + payload + verify + output-parser codegen.
Kotlin 7.0.0 on Central com.metaobjects:metaobjects-codegen-kotlin:7.0.0 + metadata-ktx Kotlin facade. KotlinPoet codegen: Exposed Tables, Spring controllers, payload VOs, output parsers, stored-proc helpers. Persistence-conformance against Testcontainers Postgres.
C#.NET full-stack EF Core entities + AppDbContext + ASP.NET minimal-API routes + Postgres DDL (full-CREATE + incremental introspect/diff/migrate via meta migrate --from-db). Render engine + payload-VO codegen + verify ship. meta CLI is the entry point.
Python full-stack 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.

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.

# metaobjects/meta.subscriber.yaml
metadata.root:
  package: acme
  children:
    - object.entity:
        name: Subscriber
        children:
          - source.rdb:      { "@table": subscribers }
          - field.long:      { name: id }
          - field.string:    { name: email, "@maxLength": 320, "@required": true }
          - field.string:    { name: name }
          - field.enum:
              name: status
              "@values": [active, paused, cancelled]
              "@required": true
          - field.timestamp: { name: createdAt, "@autoSet": onCreate }
          - identity.primary: { "@fields": [id], "@generation": increment }
// generated/Subscriber.ts
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
import { z } from "zod";

export const subscribers = sqliteTable("subscribers", {
  id:        integer("id").primaryKey({ autoIncrement: true }),
  email:     text("email", { length: 320 }).notNull(),
  name:      text("name"),
  status:    text("status", { enum: ["active", "paused", "cancelled"] as const }).notNull(),
  createdAt: text("created_at").notNull(),
});

export const SubscriberInsertSchema = z.object({
  email:  z.string().max(320),
  name:   z.string().optional(),
  status: z.enum(["active", "paused", "cancelled"]),
});

export type Subscriber = typeof subscribers.$inferSelect;
export type SubscriberInsert = z.infer<typeof SubscriberInsertSchema>;
// generated/SubscriberDto.java
package 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) { /* ... */ }
}
// generated/Subscriber.kt
package 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.kt
object 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)
}
// generated/Subscriber.cs
namespace 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>();
        });
    }
}
# generated/subscriber.py
from 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
-- 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
);
# 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.

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:

Language Popular alternatives What they do well What MetaObjects adds
TypeScript Prisma, Drizzle, TypeORM, Kysely Mature schema → migration loops. Excellent type-safe SQL ergonomics (Drizzle, Kysely). Mature ecosystem. 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.
Java Hibernate / JPA, jOOQ, Spring Data JPA Industry-standard ORM (Hibernate). Type-safe SQL with code-first generation (jOOQ). Repository abstractions (Spring Data). 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.
Kotlin Exposed, Ktor + jOOQ, kotlinx.serialization Idiomatic Kotlin SQL DSL (Exposed). Solid web stack (Ktor). First-class coroutines. 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.
C#.NET EF Core, Dapper, NHibernate 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.
Python SQLAlchemy, Django ORM, Pydantic + FastAPI, SQLModel Mature ORM (SQLAlchemy). Batteries-included framework (Django). Excellent validation (Pydantic). SQLAlchemy declarative models + Pydantic schemas + FastAPI routers from the same metamodel that drives every other port. Same Pythonic outputs; same drift guarantees.
Schema-only IDLs OpenAPI, Smithy, gRPC / protobuf Strong API-shape governance. Wide client/server tooling. Industry-standard. 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.

Capability MetaObjects Prisma / Drizzle JPA / EF Core / SQLAlchemy OpenAPI / Smithy
Idiomatic per-language codegen 5 languages TS only single-lang type stubs only
Cross-language source of truth five ports, byte-identical no no API shape only
Schema migrations diff + emit + apply yes yes no
Typed validation at the wire boundary Zod / Pydantic / Hibernate / DataAnnotations / kotlinx runtime only annotations schema-only
Runtime metadata (drive behavior dynamically) all 5 ports no reflection no
Drift detection (build-time) meta verify migration diff migration diffno
Typed prompt construction + render 5 ports, conformance-gated no no no
template.output parser-on-receipt 5 ports (Zod / Pydantic / Jackson / kotlinx / STJ) no no no
LLM tool-call envelopes (template.toolcall) cross-port no no no
Hand-edit-preserving regen 3-way merge user-edits adjacent scaffolding only codegen reruns
Machine-readable metadata (grep-friendly) YAML/JSON, named constants DSL or schema.prisma annotations / decorators YAML/JSON
Generated code is plain (no proxy/decorator magic) idiomatic native idiomatic proxy/runtime magictype stubs
Deterministic regen (same in → same out) conformance-gatedyes yes yes
Small surface area for agent context window11 base types + extensions single-lang ORM API large framework surfacesmall 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.

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.

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.

Client-side

Universal across backends -- a Java server serving React uses the same TypeScript packages.