Repository navigation
Docs: no guidance on serializing MetaObjectAware / PojoObject instances — consumers hand-roll Jackson workarounds #273
Description
Problem
The Java runtime ships a metadata-driven JSON layer for MetaObject-backed instances, but none of the generated agent guidance mentions it exists. A consumer (or a coding agent following the shipped skills) hits a serialization failure on a generated VO, finds no guidance, and hand-rolls a Jackson workaround — reimplementing, badly, something the jar already provides.
That happened to us this week, twice, on the same codebase.
What exists but is undocumented
Present in metaobjects-metadata 7.20.11 and metaobjects-codegen-base 7.20.11:
| class | what it is |
|---|---|
com.metaobjects.io.object.json.JsonObjectWriter / JsonObjectReader |
metadata-driven object JSON read/write |
com.metaobjects.io.object.gson.MetaObjectSerializer |
Gson serializer for MetaObject-backed instances |
com.metaobjects.object.MetaObjectAware |
the interface that makes an instance metadata-aware |
com.metaobjects.generator.direct.object.javacode.PojoAwareCodeWriter |
codegen emitting plain POJOs that implement MetaObjectAware |
The documentation gap
Across the generated agent context for a java, kotlin server, react, tanstack project:
.metaobjects/AGENTS.mdand.metaobjects/CLAUDE.md— 0 mentions of any of the above- every
metaobjects-*skill, includingmetaobjects-codegen/references/java.mdandmetaobjects-runtime-ui/references/java.md— 0 mentions
metaobjects-codegen/references/java.md has a generator table listing SpringControllerGenerator, SpringDtoGenerator, SpringRepositoryGenerator, SpringPayloadGenerator, SpringOutputParserGenerator, SpringFilterAllowlistGenerator — but not PojoAwareCodeWriter, and nothing anywhere says how a generated object should be serialized.
Why the gap bites specifically
Extract-route VOs extend PojoObject, whose framework field is a MetaObject back-reference. A default Jackson ObjectMapper walking that reference fails with InaccessibleObjectException under JPMS. The failure surfaces at the consumer's serialization call, far from any metadata concept, so the natural (wrong) reaction is to treat it as a Jackson problem:
- Two of our services shipped with the response-VO capture disabled entirely, each carrying a comment saying the parsed views "carry MetaObjects framework internals" and could not be serialized — i.e. a real feature was switched off because the supported path was undiscoverable.
- The eventual "fix" was a hand-written
@JsonIgnoreTypemixin onMetaObjectplusFIELDvisibility andFAIL_ON_EMPTY_BEANS=false. It works, and it should never have been written.
Both are consumer-side symptoms of one missing paragraph.
Suggested fix
- A serialization section in
references/java.md(and the equivalent per-port fragment): when you have a MetaObject-backed instance and need JSON, useJsonObjectWriter/MetaObjectSerializer— with a snippet. State explicitly that a default Jackson/Gson mapper over aPojoObjectsubtype will fail on theMetaObjectback-reference, and that this is expected rather than a bug to work around. - List
PojoAwareCodeWriterin the generator table, and say when to choose POJO+MetaObjectAwareoutput overPojoObject— a plain POJO that is metadata-aware serializes with an ordinary mapper, which is the simpler answer for consumers who mostly want normal Jackson behaviour. - One line in
AGENTS.mdunder the principles, in the same spirit as "use the generated constants for any string that names metadata": serialize generated objects through the MetaObjects JSON layer, don't hand-configure a mapper around the framework fields. - Optionally a
metaobjects-verify/ adoption-audit check that flags a hand-configuredObjectMappertouching generated types.
Context
Found while adding per-call telemetry capture in a Java/Kotlin consumer on metaobjects 7.20.11. Happy to contribute the references/java.md section if the shape above looks right.
Activity
dmealing commentedon Aug 7, 2026
Scoped this across all five ports before touching anything. The gap is Java-only — but the investigation also turned up a correction to suggested fix #2 that matters, because implementing it as written would make the problem worse.
The back-reference is real, and worse than "a framework field"
PojoObjectholdsprivate MetaObject metaObjectbehind a publicgetMetaData()getter, with no@JsonIgnoreand notransientanywhere. So this isn't Jackson being fussy about a private field — default bean introspection sees a legitimate public property and walks the entire metadata graph from it. That's theInaccessibleObjectExceptionunder JPMS.Worth adding as a softener in the docs: Java's
ValueObjectis aMap, so Jackson map-serializes it fine. The hard failure is specifically the pojoAware bean shape, which is why it looks so arbitrary from the consumer side.PojoAwareCodeWriteremitsextends PojoObject. It is the breaking shape, not an alternative to it. Recommending it as the clean-Jackson option would send consumers straight into the failure this issue is about.The genuine clean-Jackson answer already ships: the
codegen-springrecord surface. Its DTOs and payloads are plain Java records, and the generatedextractLenientmaps the internalValueObjectgraph into those records before returning — so nothing framework-shaped ever reaches the consumer's mapper. That's what the docs should point at.Two producing paths, not one
The issue implies the problem arrives via codegen. It's both, and they need different guidance:
JavaObjectCodeGenerator flavor=pojoAware(codegen-base) — reachable through the standard<generator><classname>mojo config, and documented nowhere outside an in-treeREADME-flavored-objects.md.getObjects→mc.newInstance().metaobjects-runtime-ui/references/java.mdalready tells consumers rows "areValueObjectinstances" and then offers zero JSON guidance, which is exactly where someone hits this and concludes it's a Jackson problem.The
codegen-springpath is clean and should be stated as such, so nobody "fixes" a problem they don't have.Other ports: not affected
MetaObjectAware/ValueObjectexists for the extract engine, but every consumer-facing artifact maps into plain shapes; runtime returns plain rows (ADR-0019)MetaObjectSerializeranalogue ships@Serializableneeding the kotlinx plugin; jsonb via the internal JacksonMetaJsonbMapper) are already documented indocs/ports/kotlin.mdand the Kotlin skill referencesNo port other than Java ships a
JsonObjectWriter/MetaObjectSerializeranalogue, and the 2026-05-31 cross-port extractor design doc rules the flavored-object concept Java-only by design. So this is correctly a Java-scoped documentation fix, not a five-port sweep.Revised fix shape
Java-scoped only:
agent-context/skills/metaobjects-codegen/references/java.md— serialization section (JsonObjectWriterandJsonObjectReader,MetaObjectSerializer), plus aJavaObjectCodeGeneratorrow in the generator table stating thatflavor=pojoAwareproduces the framework-shaped outputagent-context/skills/metaobjects-runtime-ui/references/java.md— the OMDB path, right where it currently says rows areValueObjectinstancesagent-context/skills/metaobjects-prompts/references/java.mddocs/ports/java.mdagent-context/templates/always-on.md.mustache— one principle line, with a caveat: this template is stack-neutral with no per-language conditionals today, so a Java-specific rule needs conditional phrasing or it leaks into every other stack'sAGENTS.mdRelated gap, not in the issue: the flavored-object generator is undocumented wholesale — its only documentation is that in-tree README. The serialization section is a good place to fix both at once.
Full investigation notes are in the working tree at
.superpowers/sdd/.../issue-273-scope.md.docs(plan): #275 DATE-recursion fix + #273 Java JSON docs
docs(changelog): 0.21.1 coordinated PATCH entry (#275, #273)
dmealing commentedon Aug 9, 2026
Closed in 0.21.1 (the docs ship on npm via
@metaobjectsdev/sdk, which bundlesagent-context/).Commits
30e8946cand7138e580.Why it waited
This was deliberately gated on #275. Until that landed, the layer being documented crashed with a
StackOverflowErroron any date or timestamp field — it could not honestly be recommended as the sanctioned path. Documenting it first would have pointed adopters at a broken API.What now exists
Five surfaces — the
metaobjects-codegen,metaobjects-promptsandmetaobjects-runtime-uiJava references, the stack-neutralalways-ontemplate, anddocs/ports/java.md— covering:PojoObjectsubtype fails on theMetaObjectback-reference — expected, not a bug to work aroundJavaObjectCodeGeneratoradded to the generator tables with both flavorsThe issue's own fix #2 was inverted
The report suggested
pojoAwareas the clean-mapper answer. It is the opposite:FLAVOR_POJO_AWAREemitsclass <Name> extends PojoObject, which is precisely the shape that breaks a default mapper. For plain Jackson-friendly types the answer is thecodegen-springrecord surface, neverpojoAware. The docs say so directly.Out of scope
Fix #4 (a
verify/audit check for un-serializable shapes) was not built. The gap is Java-only — TS/C#/Python map to plain shapes per ADR-0019 and Kotlin's nuances were already documented — so no per-port equivalents were added.Incidental fix
Writing this surfaced a false positive in the agent-context vocabulary-drift test: its regex flagged any dotted Java FQN segment (
com.metaobjects.io.object.json) as a bogusobject.*metamodel subtype. An earlier attempt worked around it by removing the package path, which left an uncompilable snippet in a doc aimed at agents writing Java without an IDE. Fixed at the root instead — all four patterns (field./object./source./template.) now carry a negative lookbehind, verified to still catch genuinely bogus subtypes.