You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
cross-port: verify+fix field.string @dbColumnType:jsonb → validator/DTO typing in C#/Python/Java/Kotlin (TS fixed in #97) #98
@dbColumnType: jsonb on a field.string is the sanctioned "open JSON bag" pattern. The physical column is jsonb (returns a parsed JSON value), but @dbColumnType is a physical override — the logical subtype stays string. In TS, the Zod + TS-type emitters therefore typed the field as a string while the column returned an object (fixed in #97 → z.unknown() / unknown).
Ask
The same logical miss likely exists in the other ports' generated validation / DTO code, since each emits per-port-idiomatic validators/DTOs off the logical subtype:
Python — Pydantic model field type for field.string @dbColumnType:jsonb (should be Any / dict | list | ..., not str).
C# — DTO property type + any DataAnnotations (should be a JSON-capable type, not string).
Java — DTO/record field type + Jackson handling.
Kotlin — data-class property type.
This is not yet confirmed per-port — each port's column-mapper + validator/DTO emitter needs to be read to confirm (a) it honors @dbColumnType: jsonb (emits a jsonb/json physical column) and (b) whether its DTO/validator then mistypes the field as a string. Do not assume; verify against the actual emitter + a generated-output sample (the TS investigation found two sibling "bugs" that turned out to be false positives, so confirm before fixing).
Gate
@dbColumnType: jsonb legal-pairing is already registered (DB_COLUMN_TYPE_LEGAL_SUBTYPES[jsonb] = [field.string]), so no registry-conformance change. Add a per-port codegen/validation test asserting the open-JSON (not string) shape — validation/DTO emission is per-port-idiomatic (not byte-identical render-conformance), so each port gates its own.
Surfaced via the metaobjects-audit skill against a real adopter; TS confirmed + fixed in #97.
SpringTypeMapper → String in the generated <Entity>Dto/payload — a client can't POST a JSON object
BUG at the REST-contract layer (SpringTypeMapper is REST-only)
C#
Npgsql → raw JSON text
EF entity prop string? — gated by RuntimeReturnTypeTests as the intended contract
By design (raw text) — not an accidental bug
Kotlin
Exposed jsonb(col,{it},{it}) + JDBC → raw text → entity String is correct
BUT KotlinPayloadGenerator (REST) also types it String
Persistence String correct; REST payload is the open question
The real issue: cross-port API-contract inconsistency
A jsonb column holds an arbitrary JSON value. TS + Python now expose it at the API boundary as a parsed value (unknown/Any). The JVM + C# ports expose it as a raw JSON string — forcing clients to double-encode ("{\"k\":\"v\"}" instead of {"k":"v"}). C#/Kotlin gate the raw-text behavior with runtime tests, so this is a deliberate contract, not a slip.
This needs a cross-port contract decision (tracked here): should a jsonb open-bag be a parsed JSON value or raw text at the API/wire boundary, uniformly? Pending maintainer direction before fixing the JVM/C# ports. Python (a clear runtime-vs-annotation contradiction) is being fixed regardless.
Resolution — uniform parsed JSON value at the API boundary (maintainer decision)
All five ports now expose a field.string @dbColumnType:jsonb open-bag as a parsed JSON value at the API/wire boundary (wire form unchanged — byte-identical {...}):
Kotlin's derived-CRUD controller reuses the Exposed entity data class (bound to the raw-text identity-codec jsonb column) as its request/response DTO, so the derived-CRUD REST contract still exposes the field as String. C# avoided this because Npgsql maps jsonb ↔ JsonDocument natively (one type change covers persistence + API). The equivalent Kotlin move is to decode the Exposed jsonb-bag column to JsonElement (a real codec instead of the {it},{it} identity passthrough), so the entity property — and thus the CRUD contract — becomes parsed too. That changes Kotlin's persistence-layer type (currently String, asserted in KotlinTypeMapperTest) and the persistence-conformance round-trip. Tracked as a follow-up.
#98 is satisfied for the payload/declared-api surface across all ports + the entity-CRUD surface for TS/Python/Java/C#; the Kotlin entity-CRUD parity is the one open item.
All five ports now expose a field.string @dbColumnType:jsonb open-bag as a parsed JSON value at the API boundary (wire form byte-identical): TS z.unknown() (#97), Python Any (#99), Java Object (#103), Kotlin JsonElement (#104), C# JsonDocument (#105) — all merged to main. Closing. (Cross-port api-contract conformance gate tracked separately.)
Activity
dmealing commentedon Jun 29, 2026
Per-port verification results (verify-don't-assume)
The "same logical bug in all ports" assumption was wrong — the behavior is driver- and layer-specific:
z.string()→z.unknown()str(a lie — runtime test assertsdict)type_map.py→Any)SpringTypeMapper→Stringin the generated<Entity>Dto/payload — a client can't POST a JSON objectstring?— gated byRuntimeReturnTypeTestsas the intended contractjsonb(col,{it},{it})+ JDBC → raw text → entityStringis correctKotlinPayloadGenerator(REST) also types itStringThe real issue: cross-port API-contract inconsistency
A jsonb column holds an arbitrary JSON value. TS + Python now expose it at the API boundary as a parsed value (
unknown/Any). The JVM + C# ports expose it as a raw JSON string — forcing clients to double-encode ("{\"k\":\"v\"}"instead of{"k":"v"}). C#/Kotlin gate the raw-text behavior with runtime tests, so this is a deliberate contract, not a slip.This needs a cross-port contract decision (tracked here): should a jsonb open-bag be a parsed JSON value or raw text at the API/wire boundary, uniformly? Pending maintainer direction before fixing the JVM/C# ports. Python (a clear runtime-vs-annotation contradiction) is being fixed regardless.
dmealing commentedon Jun 29, 2026
Resolution — uniform parsed JSON value at the API boundary (maintainer decision)
All five ports now expose a
field.string @dbColumnType:jsonbopen-bag as a parsed JSON value at the API/wire boundary (wire form unchanged — byte-identical{...}):z.unknown()/unknownAny(pg8000 returns dict)ObjectJsonDocument(Npgsql native)kotlinx JsonElementRemaining gap — Kotlin entity-CRUD surface
Kotlin's derived-CRUD controller reuses the Exposed entity data class (bound to the raw-text identity-codec jsonb column) as its request/response DTO, so the derived-CRUD REST contract still exposes the field as
String. C# avoided this because Npgsql mapsjsonb ↔ JsonDocumentnatively (one type change covers persistence + API). The equivalent Kotlin move is to decode the Exposed jsonb-bag column toJsonElement(a real codec instead of the{it},{it}identity passthrough), so the entity property — and thus the CRUD contract — becomes parsed too. That changes Kotlin's persistence-layer type (currentlyString, asserted inKotlinTypeMapperTest) and the persistence-conformance round-trip. Tracked as a follow-up.#98 is satisfied for the payload/declared-api surface across all ports + the entity-CRUD surface for TS/Python/Java/C#; the Kotlin entity-CRUD parity is the one open item.
fix(codegen-kotlin): parse jsonb open-bag to JsonElement at persisten…
fix(codegen-python): emit Any (not str) for field.string @dbColumnTyp…
fix(codegen-spring): expose field.string @dbColumnType:jsonb as a par…
fix(codegen-kotlin): expose field.string @dbColumnType:jsonb as a par…
fix(codegen-csharp): expose field.string @dbColumnType:jsonb as a par…
dmealing commentedon Jun 29, 2026
All five ports now expose a
field.string @dbColumnType:jsonbopen-bag as a parsed JSON value at the API boundary (wire form byte-identical): TSz.unknown()(#97), PythonAny(#99), JavaObject(#103), KotlinJsonElement(#104), C#JsonDocument(#105) — all merged to main. Closing. (Cross-port api-contract conformance gate tracked separately.)docs(changelog): document the wave — jsonb parsed-value, verify stric…
test(api-contract): cross-port gate for jsonb open-bag parsed-value c…
fix(kotlin): jsonb open-bag api-contract gate + runtime-write verific…
test(csharp): jsonb open-bag api-contract gate + runtime-write verifi…
test(csharp): jsonb open-bag api-contract gate + runtime-write verifi…
test(java): jsonb open-bag runtime-write gate + api-contract harness (#…
test(java): jsonb open-bag runtime-write gate + api-contract harness (#…
feat!: dbColumnType slim-and-derive — Phase 1 (derive arrays + text d…