Register the schemas that annotated themselves into nothing - #824
Merged
Conversation
Zod 4's `.meta()` clones the schema and registers the clone, so a bare
`someSchema.meta({ id })` statement registers nothing at all. Thirty-nine of
those across eleven route modules published neither the name nor the
description written for it: the schema inlined anonymously at every use site,
and a client generated from the contract got a duplicated nameless type where
one named model belongs. That last part is the cost that matters, because the
native client is the reason the contract is published.
Bind each annotated clone to a const and point the route table at that const.
Twenty-nine ids now emit under `components.schemas`, referenced from their
thirty use sites. Seven stay absent by design: they annotate
`requestParams.query` objects, which the emitter expands into inline
`parameters` rather than a component. `MoodTagLayout` stays absent too, for
the reason `Medication` and `CoachPrefs` already carry a forced registration
in the components block: its only consumer is an `.extend()`, and extending
builds a new object rather than referencing the base.
Two annotations in the illness module named ids that no route ever referenced,
`GetIllnessDayLogQuery` and `ListIllnessDayLogsQuery` — the day-log path
publishes the merged `GetIllnessDayLogsQuery` instead. They were dead twice
over, so they go rather than get wired to nothing.
`docs/api/openapi.yaml` grows the twenty-nine components and their refs. The
only other movement in it is thirty descriptions appearing where the text had
never reached the document. No request or response shape changes.
The annotation was already in the assigning form and still never reached the document, because both consumers extend the base rather than reference it and .extend() builds a new object. Same shape as Medication and CoachPrefs, same remedy: name it in the forced-registration block.
`.meta()` clones the schema in Zod 4 and registers the clone, so a bare
`schema.meta({ id, description })` statement registers nothing at all.
Forty-one of them sat across nine route modules. The ids they named —
`MeasurementType`, `MeasurementSource`, `LoginPasswordRequest`,
`CreateMeasurementRequest`, `CreateBatchWorkoutRequest` and thirty more —
reached `components.schemas` zero times, the descriptions written for them
were published nowhere, and every use site inlined an anonymous copy. A client
generated from the contract got duplicated nameless types where one named
model belongs, which is the part that matters, because the iOS client is why
the contract is published.
Forty of them now bind their clone, and every use site points at it. Where the
schema comes from `src/lib/validations/*` the import is aliased and the clone
takes the name the use sites already spell, so nothing mutates the runtime
validation schema. The seven that cross module boundaries are exported from
`./shared` and the eight consumers import them from there; importing the raw
schema from the validation module instead is exactly what inlined them.
Thirty-four named components appear where inline copies stood. Fully
dereferenced, the document differs by eighty-six added descriptions and
nothing else: no operation's request or response shape moves. `CoachPrefs`
was already force-registered in the index and finally carries its description.
Five ids legitimately stay out of `components.schemas` —
`ListMeasurementsQuery`, `ListAllergiesQuery`, `ListEncountersQuery`,
`SuggestEncounterQuery` and `ListPractitionersQuery`. The emitter expands a
`requestParams.query` object into inline `parameters`, so a query id has
nowhere to land; they stay in the assigning form for consistency.
Three findings the wiring turned up:
`CreateShareLinkRequest` and `HealthRecordExportRequest` were annotated in the
Coach module, which imported the two schemas for no other reason and never
referenced them. Both operations that send these bodies live in the
health-record module, so the annotations move there. Both descriptions
described a `sections` field: the export body has no such field and rejects it
with a 422, and the share-link body freezes scope through `selection` plus
`documentIds` / `documentOnly`. Publishing either sentence verbatim would have
published a lie, so both are corrected to the shape the routes actually take.
`CoachPlansListQuery` — the forty-first — named a component that nothing
referenced and that a query object could never reach. The operation publishes
its two parameters by hand, because the schema carries a cross-field
refinement the expansion cannot express, and every clause of the annotation is
already published on the operation and on both parameters. It is deleted
rather than left registering nothing.
The generated document is the whole visible effect of both halves, so it conflicted line for line. Regenerated from the merged registry rather than resolved by hand.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sixty-four annotations in the OpenAPI registry named a schema and registered nothing.
In Zod 4
.meta()clones the schema and registers the clone, so a bare statement that discards the return value is a no-op:The failure is invisible from the registry side, because the annotation looks correct and the document still builds. It shows only in the emitted file:
MeasurementType,MeasurementSource,LoginPasswordRequest,CreateMeasurementRequest,ListMeasurementsQueryandCreateBatchWorkoutRequestappeared zero times undercomponents.schemas, while the one schema that was force-registered by hand appeared once.What it cost: the schemas inlined anonymously at every use site, and the descriptions written for them were never published at all. A client generated from this contract got a nameless duplicate type per operation instead of one named model, which is the opposite of why the contract is published.
What changed
Seventy-seven calls converted to the assigning form, use sites repointed. Where a schema comes from
src/lib/validations/*the import is untouched and the annotated clone takes a new name, so the runtime validator is never mutated.The seven in
shared.tshad no use site inshared.tsat all: their consumers are eight other modules, which now import the annotated clones. Without that step none of them could ever have registered.Sixty-three new named components in the emitted document, none removed.
MoodTagLayoutneeded the same forced registration asMedicationandCoachPrefs— its annotation was already correct, but both consumers.extend()the base, and.extend()builds a new object rather than a reference.Verified rather than eyeballed
The document diff is three thousand lines, so both halves were checked by fully dereferencing every
$refin the baseline and in the regenerated file and deep-comparing the resolvedpathstrees.The result across every affected path: 116 added
descriptionstrings and nothing else. No property, type, enum,required,formator array-length difference anywhere, no path added or removed, no pre-existing component removed. Those descriptions are exactly the text the bare calls had been throwing away.Findings
Two published descriptions were factually wrong.
HealthRecordExportRequestandCreateShareLinkRequestboth described asectionsfield that neither schema has — the export route refuses it explicitly, and the share link freezes scope throughselectionplusdocumentIds/documentOnly. Wiring them as written would have published a lie, so both are corrected to what the routes take. Both were also annotated in a module that imported the schemas for no other purpose and never referenced them; they now live beside the operations that send them.Six apparent hits were not defects. The
record-settings.tscalls sit inside az.union([...])argument list, where the returned clone is the union member, so they always worked. Verified against the committed spec: all six ids were already published.Three annotations were dead twice. Two named ids for a query the route does not publish (it merges both modes into one hand-written query), and one named a query whose parameters are hand-written because it carries a cross-field
.refine()the expansion cannot express. No use site to wire, and keeping them as unused exports turnedknipred. Every clause of their text is already published on the operations, so nothing left the contract.Twelve ids legitimately never reach
components.schemasand are left in the assigning form for consistency: they annotaterequestParams.queryobjects, which the emitter expands into inlineparameters. Every pre-existing query id behaves the same way.Gates: typecheck, lint,
format:check,knip,openapi:check, 21,551 unit tests and the production build, all green.