Skip to content

Docs: no guidance on serializing MetaObjectAware / PojoObject instances — consumers hand-roll Jackson workarounds #273

Closed

Description

@dmealing

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.md and .metaobjects/CLAUDE.md — 0 mentions of any of the above
  • every metaobjects-* skill, including metaobjects-codegen/references/java.md and metaobjects-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 @JsonIgnoreType mixin on MetaObject plus FIELD visibility and FAIL_ON_EMPTY_BEANS=false. It works, and it should never have been written.

Both are consumer-side symptoms of one missing paragraph.

Suggested fix

  1. A serialization section in references/java.md (and the equivalent per-port fragment): when you have a MetaObject-backed instance and need JSON, use JsonObjectWriter/MetaObjectSerializer — with a snippet. State explicitly that a default Jackson/Gson mapper over a PojoObject subtype will fail on the MetaObject back-reference, and that this is expected rather than a bug to work around.
  2. List PojoAwareCodeWriter in the generator table, and say when to choose POJO+MetaObjectAware output over PojoObject — 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.
  3. One line in AGENTS.md under 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.
  4. Optionally a metaobjects-verify / adoption-audit check that flags a hand-configured ObjectMapper touching 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

  1. dmealing commented on Aug 7, 2026

    @dmealing
    MemberAuthor

    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"

    PojoObject holds private MetaObject metaObject behind a public getMetaData() getter, with no @JsonIgnore and no transient anywhere. 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 the InaccessibleObjectException under JPMS.

    Worth adding as a softener in the docs: Java's ValueObject is a Map, 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.

    ⚠️ Suggested fix #2 is inverted

    "List PojoAwareCodeWriter in the generator table, and say when to choose POJO+MetaObjectAware output over PojoObject — a plain POJO that is metadata-aware serializes with an ordinary mapper"

    PojoAwareCodeWriter emits extends 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-spring record surface. Its DTOs and payloads are plain Java records, and the generated extractLenient maps the internal ValueObject graph 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:

    1. JavaObjectCodeGenerator flavor=pojoAware (codegen-base) — reachable through the standard <generator><classname> mojo config, and documented nowhere outside an in-tree README-flavored-objects.md.
    2. The om/omdb runtime — getObjects → mc.newInstance(). metaobjects-runtime-ui/references/java.md already tells consumers rows "are ValueObject instances" and then offers zero JSON guidance, which is exactly where someone hits this and concludes it's a Jackson problem.

    The codegen-spring path is clean and should be stated as such, so nobody "fixes" a problem they don't have.

    Other ports: not affected

    Port Verdict Why
    TypeScript Not affected Internal MetaObjectAware/ValueObject exists for the extract engine, but every consumer-facing artifact maps into plain shapes; runtime returns plain rows (ADR-0019)
    C# Not affected Same — records at the boundary; no MetaObjectSerializer analogue ships
    Python Not affected Pydantic models at the boundary; ObjectManager returns dicts
    Kotlin Not affected Generated data classes are plain. Both "which serializer" nuances (payload @Serializable needing the kotlinx plugin; jsonb via the internal Jackson MetaJsonbMapper) are already documented in docs/ports/kotlin.md and the Kotlin skill references

    No port other than Java ships a JsonObjectWriter/MetaObjectSerializer analogue, 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 (JsonObjectWriter and JsonObjectReader, MetaObjectSerializer), plus a JavaObjectCodeGenerator row in the generator table stating that flavor=pojoAware produces the framework-shaped output
    • agent-context/skills/metaobjects-runtime-ui/references/java.md — the OMDB path, right where it currently says rows are ValueObject instances
    • agent-context/skills/metaobjects-prompts/references/java.md
    • docs/ports/java.md
    • agent-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's AGENTS.md

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

  2. added a commit that references this issue on Aug 8, 2026
  3. dmealing commented on Aug 9, 2026

    @dmealing
    MemberAuthor

    Closed in 0.21.1 (the docs ship on npm via @metaobjectsdev/sdk, which bundles agent-context/).

    Commits 30e8946c and 7138e580.

    Why it waited

    This was deliberately gated on #275. Until that landed, the layer being documented crashed with a StackOverflowError on 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-prompts and metaobjects-runtime-ui Java references, the stack-neutral always-on template, and docs/ports/java.md — covering:

    • a write and read snippet in the same block, against the real signatures
    • the wire form as shipped, including the note that readers still accept legacy epoch-millis
    • the explicit sentence that a default Jackson/Gson mapper over a PojoObject subtype fails on the MetaObject back-reference — expected, not a bug to work around
    • both producing paths: flavored codegen and the om/omdb runtime
    • JavaObjectCodeGenerator added to the generator tables with both flavors

    The issue's own fix #2 was inverted

    The report suggested pojoAware as the clean-mapper answer. It is the opposite: FLAVOR_POJO_AWARE emits class <Name> extends PojoObject, which is precisely the shape that breaks a default mapper. For plain Jackson-friendly types the answer is the codegen-spring record surface, never pojoAware. 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 bogus object.* 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions