Skip to content

feat(emitter): load absent required nullable keys as null with = null - #347

Merged
sethjuarez merged 8 commits into
mainfrom
sethjuarez/nullable-load-leniency-pushback
Oct 16, 2026
Merged

sethjuarez merged 8 commits into
mainfrom
sethjuarez/nullable-load-leniency-pushback

Conversation

@sethjuarez

@sethjuarez sethjuarez commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

What & why

Add the stock TypeSpec spelling x: T | null = null for legacy input that omits a key which canonical output must always write.

Declaration Missing key on load Unset value on save
`x: T null` Reject
`x: T null = null` Load as null
x?: T or `x?: T null` Accept

Swift and Swift/Codable unknown-discriminator carriers also fill absent inherited null-default keys on save while preserving non-null values and unknown payload fields. Required nullable fields without defaults still reject absence.

The authorized release batch also repairs shared enum declarations in Go/Rust/Swift, nullable alias names across targets, and C# Record<string> loads that failed with CS8602. Null string-record entries now report their key path instead of dereferencing null.

Fixes #346.

Reproduction-first checklist

Typra fixes codegen bugs by turning the reproduction into a permanent test. For any change to emitter
behavior or generated output, confirm:

  • A focused feature fixture carries the shape (packages/typra-emitter/fixtures/features/<area>/main.tsp), or an existing one was extended.
  • A test asserts the fix — a validate-fixtures.mjs assertion, a golden under generated/fixtures/<lang>, and/or a unit test that fails on main and passes here.
  • Where behavior is runtime-observable, an executable @vector/@sample encodes input → expected/expectedError.
  • npm test, npm run validate:fixtures, and npm run lint pass.
  • The PR title is a Conventional Commit (type(scope): summary) — it becomes the squash commit and the CHANGELOG entry release-please generates.

Notes

Commands below ran in packages/typra-emitter unless stated otherwise.

  • npm test: passed, 931 tests, 0 failures, 0 skipped, exit 0.
  • npm run lint: passed, exit 0.
  • npm run validate:fixtures: complete rerun passed at 113dc4a, exit 0. All generated-test and executable-conformance stages completed; only the seven declared formatter-idempotency stages below were deferred.
  • npm run pack:dry-run (repository root): passed, exit 0.
  • Native C# build reproduced two CS8602 errors before repair. Generated C# build and executable conformance pass afterward, including null-entry rejection with key paths.
  • The exact Python: inline literal alias named after its field breaks import (mood: mood | None) #346 spec imports and round-trips calm, busy, and null in Python and Pydantic. Permanent nullable fixture tests also cover a nested alias.
  • Native Swift and Swift/Codable unknown-kind regressions failed before the repair and passed afterward on Swift 6.3.3 for Windows.

The earlier Windows gofmt failures came from CRLF checkouts of two unchanged Go adapters. Normalizing those working copies to their indexed LF content removed the failures without a Git diff. No fixture line-ending change is included in the commits. The first full run found a stale nullable IR golden for the new Combo field; 113dc4a corrects it and the complete rerun passes.

Known limitations

BREAKING CHANGE: nullable literal aliases now use their declared type name (for example Mood, not the property-derived mood). Update direct references to old generated symbols when regenerating 3.0.0 output. Rust shared enum declarations may move between model source modules; use the unchanged generated module re-exports rather than an owner-file-specific import.

sethjuarez and others added 8 commits October 16, 2026 00:12
`x: T | null = null` is now accepted on a required nullable property. Load
treats an absent key as null instead of rejecting it, while save still always
writes the key (as null when unset). This models documents written before the
key existed (lenient load, strict save) with stock TypeSpec, no new decorator.

- IR: record `hasNullDefault` on the field and load assignment; a `null`
  default on any other property is still a diagnostic.
- Policy: add `shouldRejectAbsentNullableKey` and route every runtime's
  missing-key guard (TS, Python, Go, C#, Java, Rust, Swift, inherited and
  Swift unknown-discriminator paths) through it; save paths are unchanged.
- Coverage: extend `fixtures/features/nullable` (row scalar/model, inherited
  base field, all-defaulted model), IR golden in validate-fixtures, per-runtime
  codegen assertions, and an executable Python absent-key -> null probe.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 25c59b95-6ae9-4775-bd14-86a11b625d8d
Materialize absent required-nullable base keys without overwriting existing values or unknown payload fields. Keep optional fields and variant-only fields unchanged.

Add executable absent/null/populated conformance across ten runtimes and dedicated Swift/Codable unknown-kind red-green regressions. Scope inherited Rust coverage to typed polymorphic collections; raw single-reference forwarding is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 25c59b95-6ae9-4775-bd14-86a11b625d8d
Keep the shape sample fully materialized for native serde object-equality round trips. The executable conformance input still omits defaulted keys and independently requires explicit nulls on save.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 25c59b95-6ae9-4775-bd14-86a11b625d8d
Keep identical enum definitions in the first declaring file and import them elsewhere. Rust crate re-exports stay unchanged, but the owning source module may move with emission order.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 25c59b95-6ae9-4775-bd14-86a11b625d8d
Recover the alias from the declaring variants, including nested aliases, instead of naming the enum after its property. Pin the Python import failure and alias-sharing behavior in generated output and executable conformance.

Generated symbols for Alias | null now use the declared alias name (for example Mood instead of mood); consumers directly referencing the old property-derived symbol must update it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 25c59b95-6ae9-4775-bd14-86a11b625d8d
Generate a non-null guard with the record key path instead of dereferencing object?. Preserve non-null and empty strings; reject null entries rather than putting null in IDictionary<string, string>.

Reproduced CS8602 for plain and nullable records. Both generated C# build and executable conformance pass after repair; the full emitter suite passes 931 tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 25c59b95-6ae9-4775-bd14-86a11b625d8d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 25c59b95-6ae9-4775-bd14-86a11b625d8d
The full validation run passed runtime gates but found the newly added Combo field missing from the nullable fixture expected flags. Record its optional-nullable shape.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 25c59b95-6ae9-4775-bd14-86a11b625d8d
@sethjuarez
sethjuarez merged commit 3ab2d65 into main Oct 16, 2026
5 checks passed
@sethjuarez
sethjuarez deleted the sethjuarez/nullable-load-leniency-pushback branch October 16, 2026 09:53
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.

Python: inline literal alias named after its field breaks import (mood: mood | None)

1 participant