feat: typed value-object jsonb columns work end-to-end across all ports#187
Merged
Conversation
…all ports Typed @objectref value-object columns stored as jsonb (field.object @storage:jsonb, single AND @isarray) now write+read correctly through every port's persistence layer, gated by a new cross-port array-of-VO roundtrip scenario. Three genuine, runtime-proven bugs fixed (the array-of-VO path was untested everywhere, which is how they shipped): Kotlin (codegen-kotlin) — REFACTOR to Jackson, drop decorative @serializable. The port stamped kotlinx-serialization @serializable on every entity/value/ projection data class, and the typed jsonb column emitted Json.encodeToString(VO.serializer(), ...) — which requires the kotlinx compiler plugin, and the moment that plugin is enabled EVERY class with a UUID/Instant/BigDecimal/URI/Inet/temporal field fails to compile (kotlinx has no serializer for those java.* types). No consumer ever used kotlinx (all Jackson); @serializable was decorative. Now: entity/value/projection classes are plain data classes (Jackson-compatible); the typed jsonb + field.map columns use a generated per-package MetaJsonbMapper.kt (Jackson ObjectMapper with kotlinModule + JavaTimeModule) via Exposed's serialize/deserialize lambda overload — no compiler plugin, matching Java(Gson)/C#(System.Text.Json)/TS/ Python. @serializable kept only on prompt payloads + enums (which genuinely kotlinx-decode). New gate: a compile-without-the-plugin + Testcontainers-PG roundtrip of a GENERATED typed-jsonb table (the missing gate that let the broken form ship). Supersedes the unmerged fix/kotlin-exposed-jsonb-object-column. C# (MetaObjects.Codegen) — array-of-VO jsonb was BROKEN (EF model finalization threw "ICollection must be a non-interface reference type"). DbContextGenerator now emits .OwnsMany (not .OwnsOne) when field.ResolvedIsArray(); a coupled nullability defect (a non-null List initializer couldn't represent a NULL jsonb column, conflating empty-[] with null) is fixed to mirror single-VO nullability. Java (OMDB) — array-of-VO jsonb was a GAP (read threw "Expected BEGIN_OBJECT but was BEGIN_ARRAY"). GenericSQLDriver.deserializeJsonb now branches on isArray (TypeToken<List<VO>>) and parseField routes through setObjectArray; the integration-tests RoundtripWriter authors a List<ValueObject> insert value. Oracle: fixtures/persistence-conformance — a Label VO + a labels array-of-VO jsonb field on AllTypes (regenerated canonical schema); roundtrip scenarios exercise 2-element / empty-[] / 1-element arrays + the null case. TS was already correct (27/0); it proved the fixture and disproved nothing. All ports green: TS 27/0, C# 1402/0, Java OMDB 48/0 + persistence 24/0, Kotlin 272/0 + integration 97/0 (all Testcontainers PG). Co-Authored-By: Claude <noreply@anthropic.com>
…simplifier) Quality-only follow-up from the 4-angle simplifier pass on the jsonb work (reuse/simplification/efficiency/altitude — efficiency + altitude were clean, C# + Java clean; all findings were Kotlin generator dedup): - Extract emitSupportFile(pkg, outRoot, fileName, imports, block): the jsonb / inet-uri / instant-tz per-package support-file emitters were three byte-identical copies of the buildString + resolve + writeString scaffold. One helper now, each emitter a one-liner. - Use PackageMapping.toKotlin(target.name) instead of hand-splitting splitFqn then rejoining "$p.$s" (byte-identical: toKotlin is ::→. on the whole FQN) at the two new VO-FQN sites (buildObjectColumns decode + mapValueTypeFqn). - Fold entityNeedsJacksonMapper's two field passes into one loop (the flattened sub-field field.map check moves into the ObjectField branch — same behavior). codegen-kotlin 272/0 — snapshots still byte-match (behavior-preserving). Co-Authored-By: Claude <noreply@anthropic.com>
dmealing
added a commit
that referenced
this pull request
Jul 9, 2026
….8 (Maven) Coordinated release across all four registries. Three merged efforts: - #185/#186 — BREAKING: origin.passthrough is type-preserving (+@convert) - #187 — typed value-object jsonb columns all ports (Kotlin→Jackson; C#/Java/Python array-of-VO) - #188/#189 — load-order-independent super-resolution (Node-vs-Bun build portability) npm: 14-package lockstep 0.15.17. PyPI metaobjects 0.15.10. NuGet MetaObjects* 0.15.8. Maven com.metaobjects:* 7.7.8. See CHANGELOG.md [0.15.17]. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Typed value-object jsonb columns (field.object @storage:jsonb, single AND @isarray array-of-VO) work end-to-end across all persistence ports, gated by a new cross-port array-of-VO roundtrip conformance scenario. (Re-run: the prior run's review PASSED and fixed a consumer-doc gap in docs/ports/kotlin.md; its test step's actual work completed green but the flaky zai provider dropped the stream at the very end — pinned to OpenRouter now. Local branch synced to the review-fix commit 883969a.)
Three runtime-PROVEN bugs + one architectural refactor:
DELIBERATE (treat as intended): the persistence-conformance oracle's hand-written Kotlin AllTypesTable.kt keeps a raw-JSON-String identity codec (the runner is port-agnostic String-based); the generated typed Jackson form is proven by GeneratedTypedJsonbRoundTripTest. All ports use the RESOLVING array accessor (ADR-0039). A 4-angle simplifier pass ran (2nd commit: Kotlin support-file-emit + codec-FQN dedup).
All ports green: TS 27/0, C# 1402/0, Java OMDB 48/0 + persistence 24/0, Kotlin 272/0 + integration 97/0 (Testcontainers PG). Maven + NuGet line change; batches with merged-unpublished #185 for release.
What Changed
codegen-kotlinswitched typed jsonb VO columns off kotlinx@Serializable+VO.serializer()(which required the compiler plugin and failed to compile on any class with UUID/Instant/BigDecimal/URI/Inet/temporal fields) onto plain data classes plus a generated per-packageMetaJsonbMapper.ktusing Jackson through Exposed's lambdajsonboverload;@Serializableis retained only on prompt payloads + enums (genuinely kotlinx-decoded by the FR-006 output parser). A newGeneratedTypedJsonbRoundTripTestproves the generated typed-jsonb table round-trips against Testcontainers PG with no kotlinx plugin on the compile path.field.object @storage:jsonb @isArray) round-tripping in three ports: C#DbContextGenerator/EntityGeneratoremit.OwnsMany(...).ToJson(with a coupled empty-[]-vs-null nullability fix) andWriteCoercioncoerces the array; Java OMDBGenericSQLDriver.deserializeJsonbbranches onisArray(TypeToken<List<VO>>) plussetObjectArray, andRoundtripWriterauthors aList<ValueObject>insert; Python_coerce_write_valuejson.dumpss the list before binding (pg8000 binds a native list as a Postgres ARRAY literal the JSONB column rejects).roundtrip-all-typespersistence-conformance scenario: a newAllTypesentity carries one field of every persistablefield.*subtype including the array-of-VOlabelsjsonb column (written as 2-element / empty-[]/ single-element arrays across three rows), gating writes through each port's runtime/ORM write codec — not raw SQL — against Testcontainers Postgres.Risk Assessment
Testing
The Python jsonb array-of-VO write-codec fix is verified working end-to-end: the previously-failing roundtrip-all-types and update-delete-all-types persistence-conformance scenarios now pass against Testcontainers Postgres, the full 24-scenario persistence suite is green, and a runtime evidence script confirms the
labelsarray-of-VO is written through the ObjectManager write codec and stored as a JSON array on a jsonb column (read back identically); a before/after demo shows the raw-list bind failing with 22P02 pre-fix vs json.dumps binding correctly post-fix. The only Python suite failures (2 staleness-nudge tests) are pre-existing and unrelated — only object_manager.py changed between base and target. Working tree left clean.Evidence: Python array-of-VO jsonb runtime roundtrip (evidence script output)
== Field metadata (resolved, ADR-0039) == labels: subtype=object isArray=True storage='jsonb' settings: subtype=object isArray=False storage='jsonb' == Write+read through ObjectManager runtime == written labels = [{'key': 'alpha', 'weight': 10}, {'key': 'beta', 'weight': 20}] read-back labels= [{'key': 'alpha', 'weight': 10}, {'key': 'beta', 'weight': 20}] == Raw JSONB column value in Postgres == pg_typeof(labels) = jsonb raw labels column = [{'key': 'alpha', 'weight': 10}, {'key': 'beta', 'weight': 20}] PASS: array-of-VO jsonb (labels) + single-VO jsonb (settings) round-tripped; labels persisted as a JSON array on a jsonb column.Evidence: Before/after jsonb array-of-VO bind demo
=== BEFORE (pre-fix: native list passed through to jsonb column) === RESULT: raw-list bind FAILED with SQLSTATE 22P02 === AFTER (post-fix: json.dumps(list) -> JSON text bound to jsonb column) === AFTER-RESULT: json.dumps(list) bind OK; stored labels = [{'key': 'alpha', 'weight': 10}, {'key': 'beta', 'weight': 20}] | pg_typeof = jsonbEvidence: Persistence conformance (all-types scenarios) result
Evidence: Evidence script (reproducible)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ No issues found.
server/python/src/metaobjects/runtime/object_manager.py:696- Python port fails the new cross-port array-of-VO jsonb roundtrip gate (roundtrip-all-types). Thelabelsfield (field.object @storage:jsonb isArray:true, JSONB column) was added to the shared scenario in this change; Python's_coerce_write_valuepasses a field.object list through expecting pg8000 to 'bind a dict/list to the jsonb column natively.' That holds for a dict (single-VOsettingsworks) but NOT for a list: pg8000 adapts a Python list as a Postgres ARRAY literal{...,...}(each element json-stringified), which the JSONB column rejects with 22P02 'invalid input syntax for type json' on parameter $19. There is no isArray branch for jsonb objects (only for native uuid[] columns). The Java OMDB and C# ports received the analogous array-of-VO jsonb fix in THIS change (GenericSQLDriver.deserializeJsonb isArray->List<VO> + setObjectArray; DbContextGenerator .OwnsMany(...).ToJson); Python got no such fix and the scenario is not deferred (EXPECTED_FAILURES = {}), so Python's persistence-conformance is red on roundtrip-all-types — contradicting the change's 'across all persistence ports' headline. Likely fix: for an isArray field.object @storage:jsonb, json.dumps(value) to a JSON array string and bind that.mvn -pl codegen-kotlin -am install -DskipTests (install Kotlin codegen + upstream deps to .m2)cd server/java/integration-tests-kotlin && mvn test -Dtest='GeneratedTypedJsonbRoundTripTest' (headline Kotlin gate: generated typed jsonb, no kotlinx plugin, Testcontainers PG) — 1/0cd server/java/integration-tests-kotlin && mvn test (full Kotlin integration suite) — 97/0cd server/java && mvn -pl codegen-kotlin test (Kotlin codegen unit/snapshot tests) — 272/0cd server/java && mvn -pl omdb test (Java OMDB suite incl testTypedJsonbArrayRoundTrip + testJsonbEmptyAndSingleArrayRoundTrip) — 48/0cd server/java/integration-tests && mvn test -Dtest='QueryScenarioTests' (Java persistence-conformance incl roundtrip-all-types, Testcontainers PG) — 24/0cd server/csharp && dotnet build MetaObjects.slncd server/csharp && dotnet test MetaObjects.Codegen.Tests --filter FullyQualifiedName~ObjectFieldCodegenTests (C# OwnsMany codegen regression) — 5/0cd server/csharp && dotnet test MetaObjects.IntegrationTests --filter FullyQualifiedName~QueryScenarioTests (C# conformance incl roundtrip-all-types, Testcontainers PG) — 24/0cd repo-root && bun install; cd server/typescript/packages/integration-tests && bun test --test-name-pattern roundtrip-all-types test/query.test.ts (TS conformance, Testcontainers PG) — 1/0 (23 filtered)cd server/python && uv sync --group dev --extra integration; uv run pytest tests/integration/test_query_scenarios.py -k roundtrip-all-types (Python conformance, Testcontainers PG) — 1 FAILED🔧 Fix: fix Python jsonb write codec for array-of-VO roundtrip
1 info still open:
server/python/tests/codegen/test_cli_staleness_nudge.py:68- Two Python tests (test_gen_nudges_on_stale_manifest, test_verify_nudges_on_stale_manifest) fail, but this is pre-existing and unrelated to the jsonb change under test. The CLI source emitsnpx meta agent-docs --server pythonwhile the tests assert the stringmetaobjects agent-docs. Between base commit 644d27f and target 1de6b67, the ONLY file changed under server/python is src/metaobjects/runtime/object_manager.py (the jsonb codec); cli.py, agent_context/, and test_cli_staleness_nudge.py are byte-identical across base→target, so these failures exist identically on the base commit and are not introduced or affected by this change.git show 1de6b676 — reviewed the fix: addsimport jsonand the jsonb-storage field.object/field.map serialization branch in server/python/src/metaobjects/runtime/object_manager.py_coerce_write_value(~line 711)uv run pytest tests/integration/test_query_scenarios.py -k 'roundtrip-all-types or update-delete-all-types' -v — both array-of-VO/all-types scenarios PASSuv run pytest tests/integration/test_query_scenarios.py -v — full 24-scenario persistence-conformance suite, all PASS (119s)uv run pytest tests/ — full Python suite: 1405 passed, 2 failed (pre-existing staleness-nudge CLI message mismatch, unrelated to the jsonb fix)git diff 644d27f0 1de6b676 --name-only -- server/python/ — confirmed ONLY object_manager.py changed; cli.py/agent_context/test_cli_staleness_nudge.py byte-identical base→target, so the 2 failures are pre-existingEvidence script (python_array_of_vo_jsonb_evidence.py): boots Testcontainers PG, applies committed schema, writes an AllTypes row withlabels(field.object @storage:jsonb isArray:true array-of-VO) +settings(single-VO jsonb) through ObjectManager.create(), reads back via find_by_id, and inspects the raw JSONB column — labels stored as a 2-element JSON array on a jsonb column, round-trips exactlyBefore/after raw-bind demo: native Python list -> jsonb column fails with SQLSTATE 22P02 'invalid input syntax for type json' (pre-fix); json.dumps(list) -> jsonb column stores [{key:alpha,weight:10},{key:beta,weight:20}] as jsonb (post-fix)🔧 **Document** - 1 issue found → auto-fixed ✅
server/java/codegen-kotlin/README.md:199- README states "75 tests in this module (mvn -pl codegen-kotlin test)"; this branch added at least one new test (emits plain data class without Serializable annotationin KotlinEntityGeneratorTest), so the count is now stale. Could not verify the exact current total without running the Maven suite — a human should runmvn -pl codegen-kotlin testand update the number.🔧 Fix: docs(kotlin): fix stale test count and @serializable example
✅ Re-checked - no issues remain.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.