Skip to content

feat!: bump json-schema-to-typescript to v16 in generator - #304

Draft
mfal wants to merge 3 commits into
masterfrom
claude/cool-liskov-a9eb7d
Draft

mfal wants to merge 3 commits into
masterfrom
claude/cool-liskov-a9eb7d

Conversation

@mfal

@mfal mfal commented Sep 4, 2026

Copy link
Copy Markdown
Member

Split out of #303, which bumped the rest of the production dependency majors. json-schema-to-typescript was held back because it is the only bump in that batch that changes the generated client output.

⚠️ This cuts a major release

There is one breaking public type change (below), so this is a feat! with a BREAKING CHANGE: footer. lerna-lite will therefore cut a major: the repo is currently on 4.455.x, so merging this releases 5.0.0. If that is not wanted right now, this PR should wait rather than be downgraded to a non-breaking commit type — the type change is real either way.

The (T | null) | null noise is fixed, not shipped

A naive bump produces ~164 insertions / 132 deletions across the two generated types.ts files, ~120 of which are redundant (T | null) | null nestings. Those are not shipped here.

Root cause: v16 adds native support for the OpenAPI 3.0 nullable: true keyword (bcherny/json-schema-to-typescript#755). The local populateNullableTypes helper rewrote { ..., nullable: true } into anyOf: [{ ..., nullable: true }, { type: "null" }] but left the nullable flag on the inner schema, so v16 appended a second | null.

populateNullableTypes is deleted in favour of the upstream implementation. That is strictly more correct, not just tidier:

  • The helper was only ever applied to components.schemas (Schemas.ts), so nullable: true on inline schemas under paths was silently discarded. v16 honours it everywhere — this is where every remaining diff line comes from.
  • The helper bailed out on $ref schemas; v16 handles nullable next to a $ref too. (No occurrences in the current specs, so no output change from this.)

Evidence that the removal is behaviour-preserving where the helper did apply: regenerating with v16 and no helper leaves the entire components.schemas section byte-identical to the v15 output. The only change in that section is the cosmetic index signature below.

With the fix, the generated diff is 30 lines per file instead of 148.

Classification of every remaining change

Reviewed line by line across both v2/types.ts and v3-next/types.ts (v3-next mirrors v2 exactly).

Widening — safe (request bodies, callers may now also pass null)

Endpoint Fields
PATCH /v2/stacks/{stackId} updateSchedule
PATCH /v2/mail-addresses/{mailAddressId}/autoresponder inline { expiresAt, message, startsAt }
PATCH /v2/contributors/{contributorId} descriptions, deviatingContractOwner, deviatingSupportInformation
PATCH /v2/contributors/{contributorId}/extensions/{extensionId} detailedDescriptions, externalFrontends, frontendFragments, webhookUrls
PUT/PATCH project + server storage-space endpoints notificationThresholdInBytes

Cosmetic — type-equivalent

DomainmigrationDomainNotMigratableValidationError.context:

-          [k: string]: string;
+          [k: string]:
+            | string
+            | MittwaldAPIV2.Components.Schemas.DomainmigrationDomainNotMigratableReason;

From bcherny/json-schema-to-typescript#704 (TS2411 fix — index signatures widened to cover sibling named properties). DomainmigrationDomainNotMigratableReason is a union of string literals and therefore a subtype of string, so the index signature accepts and yields exactly the same set of values. Noisier to read, identical to use.

Narrowing — BREAKING (1 occurrence)

GET /v2/page-insights, 200 response, metrics[]:

-                  score?: number;
+                  score?: number | null;

The spec declares score as {"type": "number", "format": "double", "nullable": true}. v15 dropped that because the field lives in an inline path schema, so the previous type was wrong — the API could always return null here. Under strictNullChecks, consumers reading metrics[].score must now narrow before use. Correct, but breaking, hence the major.

Verification

test:client-generation-clean is not evidence of anything here — it is literally git diff --exit-code, and nothing in CI runs build:client-prod, so generated-output changes are invisible to the PR pipeline. The regeneration was run locally instead; the committed specs under packages/mittwald/spec/ make it deterministic and offline.

  • yarn lint ✅
  • yarn nx run-many -t build --skip-nx-cache ✅
  • yarn nx run-many -t test:compile --skip-nx-cache ✅
  • yarn nx run-many -t test --skip-nx-cache ✅
  • packages/generator unit tests — 23 passed, 7 suites ✅ (the generator's test target is empty, so test:unit was run explicitly)
  • Baseline check before the bump: regenerating on v15 reproduced the committed output exactly, so the diff below is attributable to the bump alone
  • Determinism check after the bump: a second build:client-prod reproduced the committed output exactly

The regeneration is a separate commit (feat!: update generated client) so the generated diff is reviewable on its own. Note that this means the first commit is not green in isolation — the generator changes without the matching regeneration — which is inherent to the split.

.yarnrc.yml sets npmMinimalAgeGate: 10080; json-schema-to-typescript@16.0.0 is past the 7-day window and installs cleanly.

🤖 Generated with Claude Code

mfal and others added 3 commits September 4, 2026 15:53
v16 adds native support for the OpenAPI 3.0 `nullable: true` keyword
(bcherny/json-schema-to-typescript#755), which makes the local
`populateNullableTypes` workaround redundant — and actively harmful:
it rewrites `{ ..., nullable: true }` into
`anyOf: [{ ..., nullable: true }, { type: "null" }]` but leaves the
`nullable` flag on the inner schema, so v16 appends a second `| null`
and emits `(T | null) | null` about 120 times per generated types file.

Dropping the workaround in favour of the upstream implementation is
also strictly more correct. The workaround was only applied to
`components.schemas` (see Schemas.ts), so `nullable: true` on inline
schemas under `paths` was silently discarded; v16 honours it
everywhere. It additionally handles `nullable` next to a `$ref`, which
the workaround bailed out on.

Verified behaviour-preserving for `components.schemas`: regenerating
with v16 and no workaround leaves that whole section byte-identical to
the v15 output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Regenerated with json-schema-to-typescript v16. All changes come from
v16 honouring `nullable: true` on inline schemas under `paths`, which
v15 silently discarded — the specs always declared these fields
nullable.

Request bodies (widening, safe — callers may now also pass `null`):

- PATCH /v2/stacks/{stackId} — `updateSchedule`
- PATCH /v2/mail-addresses/{mailAddressId}/autoresponder — the inline
  `{ expiresAt, message, startsAt }` object
- PATCH /v2/contributors/{contributorId} — `descriptions`,
  `deviatingContractOwner`, `deviatingSupportInformation`
- PATCH /v2/contributors/{contributorId}/extensions/{extensionId} —
  `detailedDescriptions`, `externalFrontends`, `frontendFragments`,
  `webhookUrls`
- PUT and PATCH on the project/server storage-space endpoints —
  `notificationThresholdInBytes`

Cosmetic (type-equivalent):

- `DomainmigrationDomainNotMigratableValidationError.context` index
  signature widened from `string` to
  `string | DomainmigrationDomainNotMigratableReason` by
  bcherny/json-schema-to-typescript#704 (TS2411 fix). `Reason` is a
  union of string literals and therefore a subtype of `string`, so the
  index signature accepts and yields exactly the same values as before.

BREAKING CHANGE: `score` on the `metrics` entries of the
GET /v2/page-insights 200 response is now typed `number | null` instead
of `number`. The spec declares it `nullable: true`, so the API could
always return `null` here and the previous type was wrong. Under
`strictNullChecks`, consumers reading `metrics[].score` must now
narrow it before use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Master's date-time PR (#299) added `RequestSchemas.ts`, modelled on
`Schemas.ts` and therefore still calling the `populateNullableTypes`
workaround this branch deletes — the merge left the generator uncompilable.
The removal is applied there as well; the defensive `cloneDeep` stays.

The committed client was regenerated from the merged state, so the generated
types carry both the v16 `nullable` handling and master's widened date-time
request parameters.

Co-Authored-By: Claude Opus 5 <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