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
Commit 7fab436
Browse filesBrowse the repository at this point in the historyBrowse files
The only generated update handler is a partial PATCH (both PATCH and PUT map to it),
but two ports overwrote every column a PATCH OMITTED — silent data loss on every
partial update, in shipped generated code:
- C# — the handler bound the body into a full `<Entity> input`; a field absent from
the JSON deserialized to its CLR default (0 / null / false / DateTime.MinValue),
and `CurrentValues.SetValues(input)` then wrote those defaults over the live
row. `PATCH {"status":"ACTIVE"}` on a wide entity destroyed every other column.
- Kotlin (base controller) — `it[field] = dto.field` for EVERY field, so an omitted
optional field's null was written over the stored value.
TS (Zod `InsertSchema.partial()`), Java and Python (present-field merges in the
generated repositories / doubles) were already correct — so this is two ports, not one.
Each broken port already had the CORRECT logic in its own TPH branch (C#'s per-subtype
JSON-body enumeration; Kotlin's `if (dto.f != null)` guard); the base handlers just
didn't use it.
Fix:
- C# base + TPH handlers deserialize each PRESENT property from the raw JSON body
(skipping the PK, and — for the base — null values), using the app's CONFIGURED
JsonSerializerOptions. The last point matters: a field.timestamp binds to
DateTimeOffset via a custom converter registered on ConfigureHttpJsonOptions, and
the first cut used default options, which can't read the wire format and 500'd (the
TPH handler had the identical latent bug — also fixed).
- Kotlin base controller null-guards OPTIONAL fields only. Required props are non-null
(Jackson 400s a body missing them before the handler runs), so they stay
unconditional — guarding them would be an always-true warning and break -Werror.
Contract: an omitted field always survives, in every port. Whether an EXPLICIT null
clears a nullable column (the absent-vs-null tristate) is deferred to FR-035.
Gate — the forcing function no existing scenario could provide: every prior update
scenario sent every non-PK column, so there was never an omitted field to clobber.
`fixtures/api-contract-conformance/scenarios/update-partial-omitted-field-survives.yaml`
PATCHes one field, OMITS the optional `bio`, and asserts (via the response AND a fresh
GET) that `bio` keeps its seed value. It runs on all five ports, both lanes (reference +
generated). Verified to FAIL on the pre-fix Kotlin generator with exactly
"expected row.bio=First computer programmer, got null" — and ONLY that scenario — then
pass after. All five ports: C# 22/22 (both lanes), Kotlin 22/22, Java 22/22, Python
44, TS both lanes.
Goldens updated (verified correct, not blind-regenerated): the C# routes unit test
pinned the old `(long id, Subscriber input, ...)` signature; the Kotlin snapshot changed
exactly one line (`it[label] = dto.label` → null-guarded, `label` being optional).
Design context for the broader work (typed patch surface, Kotlin repository generator,
optimistic concurrency) in docs/superpowers/specs/2026-07-13-generated-mutation-surface-design.md.
Its @rowVersion / ADR-0043 proposal is NOT adopted here: @autoset:onUpdate already models
"stamps on every write" and OMDB already derives its optimistic-lock column from it, so a
new attribute would violate ADR-0023 (can't-invent-what's-derivable) — a decision left to
the owner. Corrects GitHub #198; #197's premise was already refuted on that issue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hd5NWZ1rKau63vJzrBSpLb
Copy file name to clipboardExpand all lines: server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinSpringControllerGenerator.kt
+12-1Lines changed: 12 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -359,7 +359,18 @@ open class KotlinSpringControllerGenerator : MultiFileDirectGeneratorBase<MetaOb
359
359
for (field in entity.metaFields) {
360
360
if (field isObjectField|| field isMapField) continue
0 commit comments