Skip to content

fix(codegen-csharp): expose field.string @dbColumnType:jsonb as a parsed JSON value (#98)#105

Merged
dmealing merged 1 commit into
mainfrom
fix/csharp-jsonb-parsed-value-98
Jun 29, 2026
Merged

fix(codegen-csharp): expose field.string @dbColumnType:jsonb as a parsed JSON value (#98)#105
dmealing merged 1 commit into
mainfrom
fix/csharp-jsonb-parsed-value-98

Conversation

@dmealing

Copy link
Copy Markdown
Member

What

A field.string carrying @dbColumnType: jsonb is the sanctioned open JSON bag: the physical column is jsonb (returns a parsed JSON value), but @dbColumnType is a physical override — the logical subtype stays string. The C# entity/DTO emitter typed the property off the logical subtype, so it emitted public string? Payload and 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 Python Any (#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)

  • Npgsql maps jsonbJsonDocument natively — and notably it is in Npgsql's built-in weakly-typed json mapping set, so it needs no EnableDynamicJson (the project never calls it; JsonNode/POCO dynamic mapping would have required it).
  • Round-trips an arbitrary JSON object cleanly with EF Core 8 + Npgsql 8.0.10.
  • System.Text.Json (de)serializes it as a real JSON value at the minimal-API boundary — a POST body deserializes into the property and a read serializes back out as the object, never an escaped string.

@dbColumnType:uuid / timestamp_with_tz are deliberately untouched — they keep their logical CLR type and convert at the DB seam (ADR-0013); only jsonb shifts the CLR type.

Changes

  • CSharpNamingDbColumnTypeClrOverride resolves the jsonb→JsonDocument override at the type-mapping seam; RequiresSystemTextJson drives a conditional using System.Text.Json;. Named constants only (DB_COLUMN_TYPE_JSONB, JsonbClrType) — no inline "jsonb".
  • EntityGeneratorScalarProperty applies 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 a JsonDocument property).
  • Committed integration fixtureAsset.g.cs regenerated via the drift harness (JsonDocument? Payload + the using).
  • Runtime gateRuntimeReturnTypeTests now asserts Asset.Payload is a parsed JsonDocument (object members a/b), not string.
  • Persistence runner seamEntityRow passes the JsonDocument through (not reflected as a POCO); Normalization routes 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/plain field.string stay string?, jsonb still maps to a jsonb column, generated entity compiles.

Evidence (Docker available, Testcontainers round-trip DID run):

  • Full C# suite green — Codegen 247 (+1 skipped regen harness), Conformance 666, Render 290, Cli 38.
  • All 80 Docker/Testcontainers integration tests green, including RuntimeReturnTypeTests (asserts the parsed JsonDocument) and the persistence asset-uuid-roundtrip / normalization-wire-types scenarios (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

…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
dmealing merged commit f1b3bd4 into main Jun 29, 2026
25 of 28 checks passed
@dmealing
dmealing deleted the fix/csharp-jsonb-parsed-value-98 branch June 29, 2026 15:48
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant