Skip to content

CrudEndpointsConfigSchema.patterns is an exhaustive enum-keyed z.record — the input type demands all five operations and a partial parses to explicit undefined entries (wants z.partialRecord) #14365

Description

@os-trump

Blocked-by: #14691

Found while implementing #11984 (making RestServer.normalizeConfig parse and consume crud / metadata / batch / routes). Filed, not fixed — the declaration lives in packages/spec, which is the spec seat's surface.

What was measured

On origin/main @ 08e49496f, zod 4.4.3. packages/spec/src/api/rest-server.zod.ts declares:

patterns: z.record(CrudOperation, CrudEndpointPatternSchema.optional()).optional()

zod 4 reads an enum-keyed z.record as an EXHAUSTIVE record, with two consequences that disagree with each other:

  1. The input type demands every operation. z.input of that record is a Record whose keys are all five CRUD operations (values CrudEndpointPattern or undefined), so tsc refuses the natural partial patterns: { list: { method: 'GET', path: '/x' } } — measured as TS2739 ... is missing the following properties ...: update, delete, create, read in RestServer.normalizeConfig still casts crud / metadata / batch / routes instead of parsing them — batch.maxBatchSize bounds and three declared enums never execute #11984's pin file, which had to write the fixture with as never.
  2. The runtime parse accepts the partial, then invents the missing keys. zod's $ZodRecord enum-key branch walks every enum value and writes payload.value[key] = result.value for each, so CrudEndpointsConfigSchema.parse({ patterns: { list: {...} } }).patterns has five keys, four of them explicitly undefined. Object.keys(...) answers five for a config that wrote one.

Same shape one level down, with a non-optional value: RouteGenerationConfigSchema.overrides declares operations: z.record(CrudOperation, z.boolean()), which is exhaustive AND required, so a partial overrides.account.operations: { list: false } is refused with four expected boolean, received undefined issues. Since #11984 executes that schema at RestServer construction, this is a live refusal (pinned there as the declared behaviour; the input TYPE already demanded all five at typed authoring sites).

Why it matters

A partial record is the natural authoring shape for both keys, and an AI-authored config will write the partial. Today the type says "no", the runtime says "yes" (for patterns) or "no" (for overrides.*.operations), and the parsed patterns carries phantom entries any future consumer iterating it would have to skip. Note that nothing in packages/rest reads patterns or overrides today at all — the inert-key half is a separate card (declared-but-unconsumed routes / crud keys, filed alongside this one).

Suggested fix (spec seat)

z.partialRecord(CrudOperation, CrudEndpointPatternSchema) for patterns (optional keys, no phantom entries, input type is a Partial), and a decision for overrides.*.operationsz.partialRecord(CrudOperation, z.boolean()) if "unspecified operation keeps its default" is the intended reading. Both change the JSON schema / authorable-surface artifacts (gen:schema), and packages/rest's pin in rest-sub-config-parse-not-cast.test.ts asserts only the contract-stable half (the written pattern survives, no pattern is invented), so it stays green under either declaration.

Generated by Claude Code


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions