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 718ce6f
Browse filesBrowse the repository at this point in the historyBrowse files
fix(codegen): payload optionality comes from @required, in every port (#309)
C# emitted `public required` on EVERY payload property regardless of `@required`;
Kotlin emitted every property non-null for the same reason. A `template.output`
payload is the type an LLM response deserializes into, so neither could represent
a response omitting a field the metadata never marked required. The generated C#
parser's own doc comment says it throws when the JSON is "missing a `required`
property" — the adopter's report, verbatim, from the code.
THE RULE: absent `@required` ⇒ optional. `spec/metamodel/field.json` types the attr
as an optional boolean defaulting to absent. The DECISION is one cross-language
contract; the RENDERING stays idiomatic — C# `T?`, Kotlin `T? = null`, TS `name?:
T`, Python `T | None = None`.
Only C# and Kotlin changed. TS and Python already read it. Java needed nothing:
SpringTypeMapper already emits boxed Integer/Long/Boolean, so every record
component is nullable by construction and absent ⇒ optional already held. Getting
that right meant checking the type mapper rather than trusting "Java records can't
express optional."
C# was contradicting ITSELF, not merely the spec: its FR-010 extractor derives
required-ness from `@required` and treats an absent optional as benign
LOST_OPTIONAL, and `ExtractorGenerator.cs` documents in prose that PayloadCodegen
"does not honor @required". One port, two tiers, opposite predicates.
Kotlin reuses `KotlinGenUtil.isRequiredField` rather than adding a second
predicate. It accepts boolean `true` or the string `"true"`, matching what
`KotlinExtractSchemaEmitter` already accepts, so the payload type and the mapper
populating it cannot disagree — the lockstep Python's `is_field_required` docstring
protects. Python holds the tighter boolean-only threshold on both its tiers, Kotlin
the looser one on both. Whether `@required: "true"` is legal metadata at all is a
LOADER question and is left open rather than half-decided here.
WHY IT SURVIVED FOUR RELEASES: the tests pinned it. C#'s payload suite asserted
`public required` on fixtures where NO field carried `@required`, so it could not
distinguish "reads the attr" from "hardcodes it" — it would have failed under a
correct implementation. A demo test asserted that omitting an unmarked field "fails
to compile" while calling them "required members"; it now declares what it asserts
and gained the arm nobody had, that omitting an OPTIONAL member must compile.
Kotlin's #270 test used non-null as a proxy for "origins don't decide nullability",
which the all-non-null emitter also satisfied; it now carries both directions.
Every flipped assertion was checked against its own fixture rather than
regenerated. That caught a real error mid-fix: `alphaText`/`betaText` in the SHARED
corpus ARE `@required: true`, so blanket-flipping them would have replaced one
wrong golden with another. Which is the finding — the shared xpkg-collision corpus
already carried both arms, and the ports were asserting contradictory output for
one shared model. It is now the payload tier's optionality oracle too.
Adopter-visible in C# and Kotlin: a payload property that was non-null becomes
nullable unless its metadata declares `@required`.
Verified: C# 1592 (Codegen 354, +1 new arm) · Kotlin 315 · Java reactor BUILD
SUCCESS · codegen-ts 1247 · python codegen 415 · gates 10/10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
Copy file name to clipboardExpand all lines: CHANGELOG.md
+50Lines changed: 50 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -352,6 +352,56 @@ command whose whole job is the drift verdict. Pinned by a regression test on the
352
352
which needs no Postgres container and covers the silent half: before the fix its failure
353
353
output *is* the bug, verbatim.
354
354
355
+
### Fixed — a payload field's optionality now comes from `@required` in every port ([#309](https://github.com/metaobjectsdev/metaobjects/issues/309), NuGet/Maven)
356
+
357
+
C# emitted `public required` on **every** generated payload property regardless of
358
+
`@required`, and Kotlin emitted every property non-null for the same reason. So a
359
+
`template.output` payload — the type an LLM response is deserialized into — could not
360
+
represent a response that omits a field the metadata never marked required. The generated
361
+
C# parser's own doc comment says it throws when the JSON is *"missing a `required`
362
+
property"*, which is #309 exactly: adopting the generator meant rejecting real responses
363
+
that a hand-written record accepted.
364
+
365
+
**The rule, now uniform:** a payload field is optional unless it declares `@required`.
366
+
`spec/metamodel/field.json` documents `@required` as an optional boolean defaulting to
367
+
absent, so absent ⇒ optional. The **decision** is one cross-language contract; the
**Only C# and Kotlin changed.** TypeScript and Python already read `@required`. Java needed
372
+
no change and got none: `SpringTypeMapper` already emits boxed `Integer`/`Long`/`Boolean`,
373
+
so every record component is nullable by construction and absent ⇒ optional already held.
374
+
375
+
**C# was contradicting itself, not just the spec.** Its own FR-010 extractor derives
376
+
per-field required-ness from `@required` and classifies an absent optional as benign
377
+
`LOST_OPTIONAL`; `ExtractorGenerator.cs` even documents that PayloadCodegen *"does not
378
+
honor @required"*. One port, two tiers, opposite predicates.
379
+
380
+
**Kotlin uses that port's existing `KotlinGenUtil.isRequiredField`**, which accepts the
381
+
boolean `true` or the string `"true"` — matching what `KotlinExtractSchemaEmitter` already
382
+
accepts, so the payload type and the mapper that populates it cannot disagree. That lockstep
383
+
is the property Python's `is_field_required` docstring protects; Python holds the tighter
384
+
boolean-only threshold on both of its tiers, Kotlin the looser one on both. Whether
385
+
`@required: "true"` should be legal metadata at all is a loader question, left open.
386
+
387
+
**What made this survivable for four releases: the tests pinned it.** C#'s payload suite
388
+
asserted `public required` on fixtures where **no field carried `@required`**, so it could
389
+
never distinguish "reads the attr" from "hardcodes it" — and one demo test asserted that
390
+
omitting an unmarked field "fails to compile" while its comment called them "required
391
+
members". That test now declares what it asserts, and gained the arm nobody had: omitting an
392
+
**optional** member must compile. Kotlin's `#270` test asserted non-null as a proxy for
393
+
"origins don't decide nullability", which the all-non-null emitter also satisfied; it now
394
+
carries both arms, so a `@required` field with an `origin.first` stays non-null while an
395
+
unmarked sibling with the same origin kind goes nullable.
396
+
397
+
The shared `template-output-render-conformance/xpkg-collision` corpus turns out to carry
398
+
both arms already — `alphaText`/`betaText` are `@required: true` while `fromAlpha`/`fromBeta`
399
+
are not — so the ports were asserting **contradictory output for one shared model**. It is
400
+
now the payload tier's optionality oracle as well as its collision oracle.
401
+
402
+
Adopter-visible in C# and Kotlin: a payload property that was non-null becomes nullable
403
+
unless its metadata declares `@required`. Mark the fields you genuinely require.
404
+
355
405
### Fixed — a projection could not borrow an entity's alternate key ([#310](https://github.com/metaobjectsdev/metaobjects/issues/310), all four loaders)
356
406
357
407
A `object.projection` borrows its key rather than declaring one:
0 commit comments