fix(codegen-spring): expose field.string @dbColumnType:jsonb as a parsed JSON value in DTOs/payloads (#98)#103
Merged
Conversation
…sed JSON value in DTOs/payloads (#98) A `field.string @dbColumnType:jsonb` open JSON bag was typed `String` in the generated `<Entity>Dto`/payload records (the REST contract), so Jackson could not bind a posted JSON object and serialised a stored bag as a double-encoded string. SpringTypeMapper.javaTypeName now returns `Object` (dependency-free; Jackson maps arbitrary JSON to Map/List/scalar) when a StringField carries @dbColumnType:jsonb, via a new jsonbOptIn() guard mirroring the existing timestampWithTzOptIn() pattern. This flows uniformly to every REST/contract consumer of the mapper (DTO, payload, output-parser, api-docs); OMDB persistence is dynamic and unaffected. Matches the TS `z.unknown()` (#97) and Python `Any` (#99) cross-port fixes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky
dmealing
added a commit
that referenced
this pull request
Jun 29, 2026
…98) Complete the Java side of the jsonb-open-bag cross-port consistency work. Mandate 1 — OMDB runtime write path audit: OMDB has NO write-path gap for the open bag (`field.string @dbColumnType:jsonb`). Its write path correctly serializes + binds a JSON-text value via isOpenJsonbField -> serializeOpenJsonb -> bindJsonbParameter (Postgres setObject(.., Types.OTHER)). The persistence corpus never exercised this through the runtime write codec (asset-uuid seeds payload via raw SQL and only READS; roundtrip-all-types writes the *typed* @storage:jsonb path), so OpenJsonbWriteRoundtripTest fills the gap: it INSERTs an Asset whose payload is JSON text through ObjectManagerDB.createObject (NOT raw SQL) and reads it back parsed. The apparent "object is rejected" is NOT an OMDB write-path defect: OMDB's ValueObject model is statically typed by field subtype (ADR-0017), so a structured value assigned to a field.string is coerced to its java toString() at SET time (DataObjectBase._setObjectAttribute -> DataConverter.toType(STRING, ..)) — generic field-typing, identical for every string field, by design. (Verified empirically: a Map bound to the bag binds the malformed text "{a=1, b=2}" and Postgres rejects it with "invalid input syntax for type json".) The cross-port "write an OBJECT" half lives at the DTO/serialization boundary (the generated DTO is Object, #103): a consumer serializes the posted object to JSON text before it reaches OMDB's field.string, and the read side re-parses it. No OMDB production change was needed — hence the test() prefix. Mandate 2 — api-contract Java harness for the jsonb sub-corpus, mirroring the single-entity Author and m2m/tph harnesses, both lanes: - JsonbReferenceServer (hand-rolled JDK HttpServer + Postgres testcontainer). - GeneratedJsonbControllerHarness (codegen-spring DocumentController/DocumentDto compiled + booted over MockMvc behind the in-memory DocumentRepository seam). The generated DocumentDto types the bag as Object (#103), so a posted JSON object binds and reads back PARSED, never a JSON-encoded string. This LOCKS the API-boundary contract. Tests (Testcontainers Postgres, mvn -f integration-tests/pom.xml): - OpenJsonbWriteRoundtripTest 1/1 green - JsonbApiContractConformanceTest (reference) 1/1 green - JsonbGeneratedApiContractConformanceTest 1/1 green - existing ApiContract*/M2m*/Tph* 48/48 green (no regression) Deferred: Kotlin + C# api-contract jsonb harnesses (sibling worktrees); a cross-port persistence-conformance open-bag roundtrip scenario (would require all five ports' roundtrip writers, out of this Java-scoped change). Part of #98 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky
dmealing
added a commit
that referenced
this pull request
Jun 29, 2026
…98) (#113) Complete the Java side of the jsonb-open-bag cross-port consistency work. Mandate 1 — OMDB runtime write path audit: OMDB has NO write-path gap for the open bag (`field.string @dbColumnType:jsonb`). Its write path correctly serializes + binds a JSON-text value via isOpenJsonbField -> serializeOpenJsonb -> bindJsonbParameter (Postgres setObject(.., Types.OTHER)). The persistence corpus never exercised this through the runtime write codec (asset-uuid seeds payload via raw SQL and only READS; roundtrip-all-types writes the *typed* @storage:jsonb path), so OpenJsonbWriteRoundtripTest fills the gap: it INSERTs an Asset whose payload is JSON text through ObjectManagerDB.createObject (NOT raw SQL) and reads it back parsed. The apparent "object is rejected" is NOT an OMDB write-path defect: OMDB's ValueObject model is statically typed by field subtype (ADR-0017), so a structured value assigned to a field.string is coerced to its java toString() at SET time (DataObjectBase._setObjectAttribute -> DataConverter.toType(STRING, ..)) — generic field-typing, identical for every string field, by design. (Verified empirically: a Map bound to the bag binds the malformed text "{a=1, b=2}" and Postgres rejects it with "invalid input syntax for type json".) The cross-port "write an OBJECT" half lives at the DTO/serialization boundary (the generated DTO is Object, #103): a consumer serializes the posted object to JSON text before it reaches OMDB's field.string, and the read side re-parses it. No OMDB production change was needed — hence the test() prefix. Mandate 2 — api-contract Java harness for the jsonb sub-corpus, mirroring the single-entity Author and m2m/tph harnesses, both lanes: - JsonbReferenceServer (hand-rolled JDK HttpServer + Postgres testcontainer). - GeneratedJsonbControllerHarness (codegen-spring DocumentController/DocumentDto compiled + booted over MockMvc behind the in-memory DocumentRepository seam). The generated DocumentDto types the bag as Object (#103), so a posted JSON object binds and reads back PARSED, never a JSON-encoded string. This LOCKS the API-boundary contract. Tests (Testcontainers Postgres, mvn -f integration-tests/pom.xml): - OpenJsonbWriteRoundtripTest 1/1 green - JsonbApiContractConformanceTest (reference) 1/1 green - JsonbGeneratedApiContractConformanceTest 1/1 green - existing ApiContract*/M2m*/Tph* 48/48 green (no regression) Deferred: Kotlin + C# api-contract jsonb harnesses (sibling worktrees); a cross-port persistence-conformance open-bag roundtrip scenario (would require all five ports' roundtrip writers, out of this Java-scoped change). Part of #98 Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky Co-authored-by: Claude Opus 4.8 <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.
The bug
SpringTypeMapper.javaTypeNametyped theStringFieldarm unconditionally asString. Afield.string @dbColumnType:jsonbopen JSON bag therefore landed asStringin the generated<Entity>Dtorecord + payload — the REST/API contract. Consequences at the boundary:Stringcomponent.The decision: parsed JSON value at the boundary
Per the maintainer decision on #98, a jsonb open-bag must be exposed at the REST/API boundary as a parsed JSON value, uniformly across ports — matching the TS fix
z.unknown()(#97) and the Python fixAny(#99). For Java the field's DTO/payload type is nowObject:Map/List/scalar against anObjecttarget.StringFieldonly (validated in the loaderValidationPhase), so the guard is safe.Before / after
SpringTypeMapper.javaTypeName(field)for afield.string:field.stringStringStringfield.string @dbColumnType:jsonbStringObjectfield.string @dbColumnType:uuidStringStringThe change is a new
jsonbOptIn(field)guard (mirroring the existingtimestampWithTzOptIn()pattern, keyed offCoreDBMetaDataProvider.DB_COLUMN_TYPE_JSONB— no inline"jsonb"literal). It flows uniformly to every REST/contract consumer of the mapper (SpringDtoGenerator,SpringPayloadGenerator,SpringOutputParserGenerator, api-docsJavaFieldShapes). OMDB persistence is dynamic and does not use the mapper, so it is unaffected — the right, contained layer.Test evidence (TDD)
Added three arms to
SpringTypeMapperTest: jsonb →Object, jsonb case-insensitive →Object, and@dbColumnType:uuidstaysString(plain-string-stays-Stringalready covered).expected:<[Object]> but was:<[String]>; the uuid-stays-Stringarm passed.mvn -pl codegen-spring -am test→ 146/146 pass, includingSpringTypeMapperTest(17) andGeneratedTraceHelperCompileRunTest(which declaresfield.string @dbColumnType:jsonbllmRequest/llmResponseand compile-runs the emitted helper — green unchanged, since the trace helper routes those columns through the OMDB row builder, not the DTO mapper).JVM Testcontainers/integration corpora were not run in this environment (Docker not exercised); the unit + codegen + compile-run gates above all pass.
Part of #98 (C#/Kotlin still pending — issue intentionally left open).
🤖 Generated with Claude Code