Every port ships a MetaDataLoader (or per-language equivalent) that takes a
metadata source (directory, classpath, raw string, or list of URIs), runs the
shared pipeline (parse → tree-build → super-resolve → validate), and returns a
queryable loader object. The loader API is shaped almost identically across
ports — a fromDirectory / fromResources / fromString family of static
factories — so a snippet in one port reads naturally in another.
The full design is in 2026-05-25-cross-language-loader-architecture-unification.md.
The Author entity declaration (see entities.md) lives in
./metadata/meta.blog.json or ./metadata/meta.blog.yaml. The loader picks up
both formats; mixed-format directories are fine. Load order is a deterministic
ordinal-filename sort (because overlay merge is order-sensitive).
import { MetaDataLoader } from "@metaobjectsdev/metadata";
// From a directory (recursive)
const loader = await MetaDataLoader.fromDirectory("app", "./metadata");
// From an inline string
const inline = await MetaDataLoader.fromString(
"test",
'{ "metadata.root": { "package": "x", "children": [] } }',
);
// Navigation
const author = loader.metaObject("acme::blog::Author");
const nameField = author.field("name"); // throws if absent
const bioField = author.findField("bio"); // null-returningimport com.metaobjects.loader.MetaDataLoader;
import java.nio.file.Path;
import java.util.List;
// From a directory
MetaDataLoader dirLoader = MetaDataLoader.fromDirectory("app", Path.of("./metadata"));
// From classpath resources
MetaDataLoader resourceLoader = MetaDataLoader.fromResources("app", List.of("meta.blog.json"));
// From an inline string (JSON or YAML)
MetaDataLoader inline = MetaDataLoader.fromString(
"test",
"{ \"metadata.root\": { \"package\": \"x\", \"children\": [] } }",
MetaDataSource.MetaDataFormat.JSON);
// Navigation
MetaObject author = dirLoader.getMetaObjectByName("acme::blog::Author");
MetaField name = author.getMetaField("name"); // throws if absentmetaobjects-metadata-ktx wraps the Java loader factories in idiomatic Kotlin
top-level functions and adds null-returning lookups + reified typed-field access.
import com.metaobjects.metadata.ktx.*
import com.metaobjects.field.StringField
import java.nio.file.Path
// From classpath resources
val loader = loadResources("app", listOf("meta.blog.json"))
// From a directory
val dirLoader = loadDirectory("app", Path.of("./metadata"))
// From an inline string (defaults to JSON; pass MetaDataFormat.YAML for YAML)
val inlineLoader = loadString("test", """{ "metadata.root": { "package": "x", "children": [] } }""")
// Null-returning lookup (idiomatic Kotlin)
val author = loader.metaObjectOrNull("acme::blog::Author")
// Reified typed field access
val name: StringField? = author?.field("name") // null if absent or wrong type
val required: StringField = author!!.requireField("name") // throws if absentusing MetaObjects.Loader;
// From a directory
var loader = MetaDataLoader.FromDirectory("app", "./metadata");
// From classpath / file URIs
var uriLoader = MetaDataLoader.FromUris(
"app", new[] { new Uri("file:./metadata/meta.blog.json") });
// From an inline string
var inline = MetaDataLoader.FromString(
"test",
"""{ "metadata.root": { "package": "x", "children": [] } }""");
// Navigation
var author = loader.GetMetaObject("acme::blog::Author");
var nameField = author.GetField("name");from metaobjects.loader import MetaDataLoader
# From a directory
loader = MetaDataLoader.from_directory("app", "./metadata")
# From an inline string
inline = MetaDataLoader.from_string(
"test",
'{ "metadata.root": { "package": "x", "children": [] } }',
)
# Navigation
author = loader.meta_object("acme::blog::Author")
name = author.field("name")- Discover sources. Directory scan picks up
*.json+*.yaml/*.ymlrecursively. Ordinal-filename sort gives a deterministic load order. - Parse. YAML is desugared to canonical JSON first (sigil-free →
@-prefixed). See yaml-authoring.md. JSON is parsed directly. - Tree-build. Each parsed file becomes a tree of
MetaDatanodes. Same shape across ports. - Overlay merge. Files sharing the same
package+ objectnameare merged. Last-writer-wins on attribute conflicts; structural children accumulate. - Super-resolve.
extends:is resolved after all files load (deferred, not eager) so a child can extend a base declared in any other file. Resolution is order-independent — a pure function of the source set — so a dottedextends:to a member the owner reaches through its OWNextends:(e.g.extends: Owner.memberwhereOwnerinheritsmember) resolves regardless of file enumeration order; the owner's chain is wired on demand before its members are read (#188). - Validate. Per-node validation runs; reserved-attr collisions, missing
required attrs, bad enum members all surface here with
ERR_*codes (seefixtures/conformance/ERROR-CODES.json).
| Option | All ports | Purpose |
|---|---|---|
name |
yes | A label for diagnostics ("app", "test", etc.) |
| Type-registration callback | yes | Register custom MetaObject / MetaField subtypes |
| Common-attribute registration | yes | registerCommonAttribute adds documentation attrs (description, notes, etc.) to every node type |
Every port emits the same error codes (ERR_RESERVED_ATTR,
ERR_MISSING_REQUIRED_ATTR, ERR_BAD_ATTR_VALUE, ERR_DUPLICATE_NAME, …) on
identical bad input. See
fixtures/conformance/ERROR-CODES.json
for the full enumeration; every entry has a matching fixtures/conformance/error-*
fixture that pins the cross-port behavior.
The following conformance fixtures gate this feature's behavior across ports:
Basic loader behavior
fixtures/conformance/loader-basic-single-entity/— single entity file round-tripfixtures/conformance/loader-basic-empty-package/— emptypackageallowed (top-level namespace)fixtures/conformance/loader-basic-explicit-subtype/— explicit<type>.<subtype>keyingfixtures/conformance/loader-basic-multi-file-same-package/— multiple files in one packagefixtures/conformance/error-parse-malformed-json/— malformed JSON surfaces a structured parse error
All fixtures/conformance/error-* fixtures double as a loader-error matrix; see the
ERROR-CODES.json enumeration. Each
error code has at least one matching fixture that pins the cross-port behavior.
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.
- yaml-authoring.md — how YAML lowers to canonical JSON
- entities.md — what the loader navigates
- migrations-and-drift.md —
meta:verifyruns the loader at build time - 2026-05-25-cross-language-loader-architecture-unification.md