Blog/The static metamodel

The static metamodel

Every entity you write gets a generated metamodel class, produced at compile time. It is a quiet piece of the design, and it is where type safety and speed turn out to be the same decision.

March 3, 2026Internals4 min read

A generated metamodel class

For an entity like City, ST/ORM generates a small metamodel class, City_, at compile time. There is nothing in it you write by hand. The metamodel does not define the data model; it exposes the database-backed shape of the entity to application code. From that shape it derives one typed handle per field, and one per relationship, standing in for the columns and the paths you can reach from that row. It is a companion to the entity, regenerated whenever the entity changes, so it never drifts from the thing it describes.

Type safety you cannot forget to ask for

Because those handles are real, typed code, the compiler checks every reference you build a query from. User_.city.name eq "Sunnyvale" is not a string that happens to match a column. It is a path the compiler knows, so a renamed field, a broken path, or a wrong type is a build error, not something you find in production. This is the same generated metamodel that lets a relationship become a query across the graph, and the same one that keeps real SQL templates checked against known fields and types. You do not opt in to the safety. It is the only way the references exist.

Generated paths, not reflection

The other half is what does not happen at runtime. Many ORMs discover your fields by reflection: they ask the class, while the program runs, what it has, then read and write it through that reflective handle. It works, but it is slower than plain field access, and it moves a class of errors from the compiler to the first request that reaches the path. Because ST/ORM already holds the metamodel as generated code, none of that is necessary. The paths are known ahead of time, so reading and writing a field is direct, generated access, with no per-row reflective lookup in the way.

Where it pays off: hydration

The clearest place you feel this is hydration, turning a result row into an entity. That happens for every row of every query, so it is the part that has to be fast. With a generated metamodel, hydration is code-generated too: each column is read through a known path and lands in the right constructor argument, without asking the class at runtime what to do with it. There is no per-row reflection to pay for, so the mapping runs close to the cost of the work itself.

Why it matters

The point is that type safety and performance came from one decision, not from two separate features you have to balance. Generate the model's shape once, at compile time, and the compiler can check your queries while the runtime skips reflection entirely. Everything downstream, from a query predicate to change detection, gets to stand on generated paths instead of runtime discovery. It is a small piece of generated code, but almost every part of the data layer gets to lean on it.