fix(codegen-csharp): expose field.string @dbColumnType:jsonb as a parsed JSON value (#98)#105
Merged
Merged
Conversation
…sed JSON value (#98) A `field.string` carrying `@dbColumnType: jsonb` is the sanctioned "open JSON bag": the physical column is jsonb (returns a parsed JSON value), so the generated EF Core entity property must surface a JSON-value CLR type, not a double-encoded string. This changes the C# contract (previously raw JSON text in a `string` property) to the uniform parsed-value contract — matching TS `z.unknown()` (#97) and Python `Any` (#99). CLR type: `System.Text.Json.JsonDocument`. Npgsql maps jsonb <-> JsonDocument natively (no `EnableDynamicJson` required — the project relies on the built-in weakly-typed json mappings), it round-trips an arbitrary JSON object, and System.Text.Json (de)serializes it as a real JSON value at the minimal-API boundary (POST -> read returns the object, not an escaped string). - CSharpNaming: `DbColumnTypeClrOverride` resolves jsonb -> JsonDocument at the type-mapping seam (uuid/timestamp_with_tz keep their logical CLR type and convert at the DB seam, ADR-0013); `RequiresSystemTextJson` drives a conditional `using System.Text.Json;`. Named constants only (no inline "jsonb"). - EntityGenerator: ScalarProperty applies the override; the three emit paths (entity / abstract shape / value-object POCO) add the using when needed. - DbContext config unchanged (`.HasColumnType("jsonb")` still emitted). - Regenerated the committed integration fixture (Asset.g.cs) via the drift harness. - Updated the runtime gate (RuntimeReturnTypeTests: Asset.Payload is now a parsed JsonDocument, not string) and the persistence runner seam (EntityRow passes the JsonDocument through; Normalization routes its raw JSON through the existing sorted-key jsonb path so the cross-port wire form stays byte-identical). TDD: new JsonbColumnCodegenTests (jsonb -> JsonDocument?, uuid/plain string stay string?, jsonb still maps to a jsonb column, generated entity compiles). Tests: full C# suite green (Codegen 247, Conformance 666, Render 290, Cli 38) + all 80 Docker/Testcontainers integration tests (RuntimeReturnType + persistence query asset-jsonb round-trip) green. 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
…cation (#98) Completes the C# side of the cross-port jsonb open-bag consistency (PR #110 wired TS + Python; Java/Kotlin/C# were deferred). Runtime-write verification (Mandate 1) — CLEAN, no fix needed. C# has no ObjectManager runtime tier (EF Core is the runtime); the generated minimal-API create route binds the POST body straight onto the entity. The #105 codegen types the `field.string @dbColumnType:jsonb` property `System.Text.Json.JsonDocument` (not string), so the generator emits NO string type/length validator ([MaxLength]/[StringLength]/[Required]) on it — a posted JSON object binds and round-trips through jsonb natively. Locked by a new focused unit test (JsonbColumnCodegenTests.Jsonb_field_carries_no_string_validation_attributes) plus the already-shipped RuntimeReturnTypeTests jsonb read-back assertion. API-contract wiring (Mandate 2) — both C# lanes now run the jsonb/ sub-corpus, mirroring the tph/ + m2m/ harnesses: - JsonbReferenceServer (hand-rolled HttpListener over a Postgres testcontainer) - JsonbGeneratedServerFactory (the real MetaObjects.Codegen DocumentRoutes + AppDbContext, Roslyn-compiled and booted full-stack on Kestrel over a Postgres testcontainer) — the artifact that locks #105. - ApiContractJsonbConformanceTest drives both lanes; ApiContractCorpusPaths gains the jsonb/ paths. Evidence: JsonbColumnCodegenTests 6/6 green; ApiContractJsonb both lanes 2/2 green against Testcontainers Postgres. Pre-existing unrelated failure: ApiDocsCrossPortConformanceTests.CsharpApiDocsSurface_MatchesTheSharedManifest (fails on a clean HEAD with these changes stashed). 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
…cation (#98) (#111) Completes the C# side of the cross-port jsonb open-bag consistency (PR #110 wired TS + Python; Java/Kotlin/C# were deferred). Runtime-write verification (Mandate 1) — CLEAN, no fix needed. C# has no ObjectManager runtime tier (EF Core is the runtime); the generated minimal-API create route binds the POST body straight onto the entity. The #105 codegen types the `field.string @dbColumnType:jsonb` property `System.Text.Json.JsonDocument` (not string), so the generator emits NO string type/length validator ([MaxLength]/[StringLength]/[Required]) on it — a posted JSON object binds and round-trips through jsonb natively. Locked by a new focused unit test (JsonbColumnCodegenTests.Jsonb_field_carries_no_string_validation_attributes) plus the already-shipped RuntimeReturnTypeTests jsonb read-back assertion. API-contract wiring (Mandate 2) — both C# lanes now run the jsonb/ sub-corpus, mirroring the tph/ + m2m/ harnesses: - JsonbReferenceServer (hand-rolled HttpListener over a Postgres testcontainer) - JsonbGeneratedServerFactory (the real MetaObjects.Codegen DocumentRoutes + AppDbContext, Roslyn-compiled and booted full-stack on Kestrel over a Postgres testcontainer) — the artifact that locks #105. - ApiContractJsonbConformanceTest drives both lanes; ApiContractCorpusPaths gains the jsonb/ paths. Evidence: JsonbColumnCodegenTests 6/6 green; ApiContractJsonb both lanes 2/2 green against Testcontainers Postgres. Pre-existing unrelated failure: ApiDocsCrossPortConformanceTests.CsharpApiDocsSurface_MatchesTheSharedManifest (fails on a clean HEAD with these changes stashed). 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.
What
A
field.stringcarrying@dbColumnType: jsonbis the sanctioned open JSON bag: the physical column isjsonb(returns a parsed JSON value), but@dbColumnTypeis a physical override — the logical subtype staysstring. The C# entity/DTO emitter typed the property off the logical subtype, so it emittedpublic string? Payloadand Npgsql surfaced raw JSON text.This changes the C# contract to expose a parsed JSON value at the API/wire boundary — uniform with TS
z.unknown()(#97) and PythonAny(#99). The minimal-API now returns/accepts a real JSON object, not a double-encoded string. This is an intended contract change (raw-text → parsed-value), and the gating runtime test was updated to match.CLR type chosen:
System.Text.Json.JsonDocument(+ rationale)jsonb↔JsonDocumentnatively — and notably it is in Npgsql's built-in weakly-typed json mapping set, so it needs noEnableDynamicJson(the project never calls it;JsonNode/POCO dynamic mapping would have required it).@dbColumnType:uuid/timestamp_with_tzare deliberately untouched — they keep their logical CLR type and convert at the DB seam (ADR-0013); onlyjsonbshifts the CLR type.Changes
CSharpNaming—DbColumnTypeClrOverrideresolves the jsonb→JsonDocumentoverride at the type-mapping seam;RequiresSystemTextJsondrives a conditionalusing System.Text.Json;. Named constants only (DB_COLUMN_TYPE_JSONB,JsonbClrType) — no inline"jsonb".EntityGenerator—ScalarPropertyapplies the override; the entity / abstract-shape / value-object emit paths add the using when an own field is jsonb.DbContextGenerator— unchanged; still emits.HasColumnType("jsonb")(valid + explicit for aJsonDocumentproperty).Asset.g.csregenerated via the drift harness (JsonDocument? Payload+ the using).RuntimeReturnTypeTestsnow assertsAsset.Payloadis a parsedJsonDocument(object membersa/b), notstring.EntityRowpasses theJsonDocumentthrough (not reflected as a POCO);Normalizationroutes its raw JSON through the existing sorted-key jsonb path, so the cross-port wire form stays byte-identical (payload: { a: 1, b: 2 }).Tests (TDD)
New
JsonbColumnCodegenTests(RED→GREEN): jsonb→JsonDocument?,@dbColumnType:uuid/plainfield.stringstaystring?, jsonb still maps to a jsonb column, generated entity compiles.Evidence (Docker available, Testcontainers round-trip DID run):
RuntimeReturnTypeTests(asserts the parsedJsonDocument) and the persistenceasset-uuid-roundtrip/normalization-wire-typesscenarios (asset jsonb reads back as the sorted-key object end-to-end through EF Core + Npgsql against real Postgres).Conformance
No registry change (jsonb legal-pairing already registered). api-contract corpus exercises no jsonb field — unaffected. Persistence corpus expected wire form is unchanged (still
{ a: 1, b: 2 }); only the C# runtime CLR type and runner seam changed to feed it.Deferrals
C#-only (this issue's C# slice). Java/Kotlin/Python slices of #98 remain open.
Part of #98
🤖 Generated with Claude Code