Date: 2026-05-31. Status: approved (design). Java only — TS/Python/C#/Kotlin frozen until Java is correct. Builds on Phase A (runtime object model) + Phase B (metadata-driven recover).
Generating objects that contain sub-objects (single instances and arrays-of-objects) partially exists today — codegen-spring's SpringPayloadGenerator emits nested Java records (the prompt-payload wire shape). The goal is a general-purpose flavored object generator, usable for entities, prompt-request VOs, and extraction-target VOs alike, that emits objects in selectable flavors plus a dedicated extractor:
pojoAware— a mutable POJO thatextendsaMetaObjectAwarebase (so the instance carries itsMetaObject).valueObject— a class using the map-backed, extensibleValueObjectas its base (typed accessors over the map; carries metadata-declared values beyond the typed fields).- a generated
<Name>Extractorclass that performs the extraction (recover) into the flavored object.
The flavor is a generator config option (not a metadata attribute) — the same metadata emits any flavor. Generation is direct code-as-code emission (StringBuilder/writer appending source), not templates — the fast approach used in the legacy reference implementation and already used by this repo's SpringPayloadGenerator/SpringOutputParserGenerator/BaseObjectCodeGenerator. The Mustache basic-pojo template lane is explicitly NOT used.
- Runtime (Phase A):
MetaObjectAware(instance→MetaObject back-ref),ValueObject(map-backed + extensible),ObjectClassRegistry+ObjectClassBindingProvider(self-registering FQN→class),MetaObject.newInstance(),MetaFieldget/set-by-name SPI (POJO via the reflective setter pathretrieveSetterMethod;ValueObjectvia the map). - Runtime (Phase B):
MetaObjectRecover.recover(MetaObject, text[, format[, opts]]) → RecoveryResult<Object>(inom) — assembles a typed object graph (nested + arrays) vianewInstance+ the field SPI; never-throws +orThrow; cycle/depth guard. - Codegen home:
codegen-base/.../generator/direct/object/hasBaseObjectCodeGenerator extends MultiFileDirectGeneratorBase<MetaObject>+BaseObjectCodeWriter+ ajavacodesubpackage — the language-agnostic direct object-codegen base. The flavored Java generator lives here. - Record flavor:
SpringPayloadGeneratoralready emits nested-capable records (the Jackson-wire payload). It stays for that use; the new generator adds the two runtime-metadata-aware flavors.
PojoObjectbase — absent in this repo. Port the legacyabstract class PojoObject implements MetaObjectAware(holds theMetaObject, ctor takes it), fixing the legacysetMetaDataself-assignment bug (metaObject = metaObject→this.metaObject = metaObject, and make the field non-final or drop final to allowsetMetaData). Place inmetadata/.../object/pojo/PojoObject.java. (Optionally implementValidatableonly if that interface exists in this repo; otherwise justMetaObjectAware.)
PojoObject (new) — flavor pojoAware's base. Generated POJOs extends PojoObject, calling super(mo) to set the back-ref.
A Java object generator built on BaseObjectCodeGenerator/BaseObjectCodeWriter, with a flavor config option ∈ { pojoAware, valueObject }. Direct emission (the writer appends source — no templates). For each object.* MetaObject:
-
pojoAware→public class <Name> extends PojoObject:- typed fields for every
MetaField: scalars/enums mapped to Java types; a nestedfield.object(@objectRef) → the nested class type<Sub>(single) orjava.util.List<<Sub>>(whenisArrayType()), the nested class generated recursively in the same flavor (deduped per run); - standard getters/setters; a
public <Name>(MetaObject mo){ super(mo); }ctor (+ a no-arg ctor if needed by the field-IO reflective path); - field write/read at runtime uses the existing Phase A reflective setter/getter path.
- typed fields for every
-
valueObject→public class <Name> extends ValueObject— structurally different frompojoAware, and performance-tuned:public <Name>(MetaObject mo){ super(mo); };- NOT naive
get(name)/set(name, v)per call. Each field's value-holder is cached once (at construction) and the typedgetX/setXoperate on the cached holder directly (holder.getValue()/holder.setValue(v)), avoiding a keyed map lookup on every accessor call. This is the perf pattern the legacy reference uses. - the backing map carries everything, so metadata-declared values beyond the typed accessors are retained (extensible);
- nested accessors typed
<Sub>/List<<Sub>>.
Reference + a likely runtime reconciliation: the perf-tuned holder pattern is in the legacy reference's data/managed object bases — a
Map<String, Value>whereValueis a per-field holder exposinggetValue()/setValue(), so a cached holder reference services get/set without re-hashing (the legacyManagedObject/DataObjectBase+ this repo'sValueObjectBase.AttributeEntry). This repo'sValueObjectBasecurrently exposesAttributeEntrybut may re-look-up by name; the plan must study the legacy reference and reconcileValueObjectBaseto expose a cached per-field value-holder primitive (e.g.valueHolder(name)returning a stable holder) that the generated accessors cache + use directly. ThepojoAwareflavor needs no such primitive (plain typed fields). -
Both flavors emit a self-registering
ObjectClassBindingProvider(FQN→generated class) — wired via ServiceLoader — soMetaObject.newInstance()/ extract yield the generated type for that object's FQN.
public final class <Name>Extractor with public static <Name> extract(MetaDataLoader loader, String text) (typed, orThrow) and public static RecoveryResult<<Name>> recover(MetaDataLoader loader, String text) (never-throws): bakes the payload FQN, resolves the MetaObject via loader.getMetaObjectByName(fqn), calls MetaObjectRecover.recover(mo, text, …); because <Name> is registered, newInstance yields it and assemble populates it (nested + arrays recurse). (The generator emits MetaObjectRecover/RecoveryResult by FQN string — codegen-base needs no om compile dep; the test needs om.) As shipped, the method shapes track the runtime MetaObjectRecover/RecoveryResult names (raw <Name> from extract, RecoveryResult<<Name>> from recover); the queued cross-port recover → extract rename will sweep both.
Single + array handled by recursive generation (each nested VO → its own flavored class + provider) + the Phase B runtime assemble recursing. A per-run dedupe set prevents re-emitting a shared nested type; a cycle guard (visited-set / MAX_NEST_DEPTH) bounds self-referential graphs, falling back to a non-recursive reference.
The legacy object-codegen framework is already present in this repo, byte-identical (verified): codegen-base/.../direct/object/ BaseObjectCodeGenerator (255L), BaseObjectCodeWriter (355L), and javacode/ JavaCodeGenerator (74L) + JavaCodeWriter (202L). A lot of design went into it; it is reused as-is. The new work extends it through its existing hooks — it does not reinvent or rewrite it.
- Reuse: the abstract
BaseObjectCodeGenerator(direct,MultiFileDirectGeneratorBase) +BaseObjectCodeWriterframework and its overridable hooks (getLanguageType,getGetterMethodName/getSetterMethodName/getParameterName,writeGetter/writeSetter,writeObjectHeader/writeObjectFooter,createWriter,getFileExtension/getLanguageName,convertToLanguageNaming,GenerationContext). The existingJavaCodeWriteremits the accessor interface; it stays as-is. - Extend, don't fork: the two flavors + the Extractor are NEW writer subclasses of
BaseObjectCodeWriter(concrete-class emission: theextends <base>clause, theMetaObjectctor, field/getter/setter bodies, and — forvalueObject— the cached-holder accessors) plus thinBaseObjectCodeGeneratorsubclasses that select the writer by theflavoroption. Reuse the framework's naming/type/hook methods rather than duplicating them. - Downstream-extensible by design: every new generator + writer is written so a downstream project can subclass and customize — emission steps are
protectedand overridable (e.g.protected void writeField(...),writeGetterBody(...),writeSetterBody(...),writeConstructor(...),writeExtractMethod(...), naming/type hooks), the writer is obtained via the overridablecreateWriterfactory, and the flavor is a config option a downstream generator can also set/extend. Noprivate/finalon the emission seams that a customizer would reasonably need. - Account for every deviation: if any part of the legacy framework is NOT used as-is (changed, bypassed, or replaced), the implementation plan must state what changed and why (e.g. the
ValueObjectBasecached-holder primitive reconciliation, or any hook that's insufficient for concrete-class emission). Default is reuse; deviations are the exception and must be justified in the plan/PR.
The record payload output (SpringPayloadGenerator) is the immutable Jackson-wire shape for the prompt pillar and stays as-is. The new generator adds the two runtime-metadata-aware flavors for general/entity/extraction use (mutable, MetaObject-bearing, registry-bound). They coexist (different use cases); unifying all three under one flavor option is a possible later cleanup, not this scope.
Direct compile-and-run proofs (the gold-standard gate, like the FR-010/Phase-B codegen proofs):
- For each flavor, generate an object with a nested sub-object + an array-of-objects, compile in-memory (javac), and assert: the class extends the right base;
newInstanceyields it with theMetaObjectback-ref set; the generatedExtractor.extract(loader, dirtyText)populates the nested object + array-of-objects (not null) into the flavored instance;pojoAwarefields set via getters,valueObjectvalues present via the map + typed accessors. - A self-registration test: the generated provider registers the FQN→class, so
MetaObject.newInstance()returns the flavored type.
(No cross-port conformance corpus yet — other ports are frozen. A Java conformance fixture can seed a future cross-port corpus once Java is correct.)
- All other ports (TS/Python/C#/Kotlin) — frozen until Java is correct.
- Templates — generation is direct code-as-code only.
- Changing the existing
recordpayload generator. - Strict-
parse()/DDL consumption of these flavors; therecover→extractrename (queued separately).