refactor(API): Add a strict option to Z.class (no-changelog) - #36916
refactor(API): Add a strict option to Z.class (no-changelog)#36916uddish wants to merge 4 commits into
Conversation
PR review overviewBased on ownership of the 3 changed files in this PR:
|
Bundle ReportChanges will increase total bundle size by 4.37kB (0.01%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: editor-ui-esmAssets Changed:
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
9b7557e to
aa9d779
Compare
`Z.class` always built a lenient `z.object`, which strips a key it does not
know. An endpoint whose contract is to reject that key had to define a second,
strict schema and override `schema` and `safeParse` to point at it.
`Z.class(shape, { strict: true })` builds the strict object once, so all four
entry points — `schema`, `safeParse`, `parse` and the constructor — agree.
`extend` carries the option, so a child of a strict parent stays strict.
Part of https://linear.app/n8n/issue/API-84
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…option (no-changelog) Replaces the local strict schema and the two static overrides it existed for. No behaviour change: the generated OpenAPI fragment is byte-identical, and the `POST /workflows` integration tests are untouched. Part of https://linear.app/n8n/issue/API-84 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
No issues found across 3 files
Architecture diagram
sequenceDiagram
participant C as Public API Client
participant API as Workflow Controller
participant DTO as CreateWorkflowPublicDto
participant Z as Z.class Factory
participant Zod as Zod Engine
Note over Z,DTO: Definition Phase (Startup)
Z->>Zod: NEW: z.object(shape).strict()
Note right of Z: Triggered by { strict: true } option
Z->>DTO: Generate Class with strict schema
Note over C,Zod: Runtime Request Flow (POST /workflows)
C->>API: Request with payload (e.g. { name: '...', unknown_key: 1 })
alt Validation via safeParse
API->>DTO: CHANGED: safeParse(payload)
DTO->>Zod: Validate against strict schema
Zod-->>DTO: Return ZodError (Unknown key)
DTO-->>API: success: false
else Validation via Constructor
API->>DTO: NEW: new CreateWorkflowPublicDto(payload)
DTO->>Zod: parse(payload)
Zod-->>DTO: Throw ZodError
DTO-->>API: Exception (400 Bad Request)
end
API-->>C: 400 Bad Request
Note over DTO,Zod: Inheritance Behavior
opt Extension via .extend()
DTO->>Z: ChildDto = Parent.extend(newShape)
Z->>Zod: NEW: z.object(combinedShape).strict()
Note right of Z: Options (strict: true) are preserved in child
end
aa9d779 to
b9e36bd
Compare
There was a problem hiding this comment.
No issues found across 3 files
Architecture diagram
sequenceDiagram
participant Client as Public API Client
participant Controller as Workflow Controller
participant DTO as CreateWorkflowPublicDto
participant ZodClass as Z.class Utility
participant Zod as Zod Library
Note over Client,Zod: Runtime Validation Flow (POST /workflows)
Client->>Controller: Request with Payload (e.g., { name: 'wf', extra: 1 })
Controller->>DTO: safeParse(payload)
Note over DTO,ZodClass: CHANGED: DTO no longer overrides static methods. <br/>It uses logic inherited from Z.class(shape, { strict: true })
DTO->>ZodClass: schema.safeParse(payload)
ZodClass->>Zod: Execute validation logic
alt NEW: options.strict is true
Zod->>Zod: Check for unknown keys
alt Unknown keys present
Zod-->>ZodClass: Return ZodError
ZodClass-->>DTO: success: false
DTO-->>Controller: validation error
Controller-->>Client: 400 Bad Request
else Valid payload
Zod-->>ZodClass: Return data
ZodClass-->>DTO: success: true
end
else Default (Lenient)
Zod->>Zod: Strip unknown keys
Zod-->>ZodClass: Return cleaned data
ZodClass-->>DTO: success: true
end
opt Constructor Usage
Controller->>DTO: NEW: new CreateWorkflowPublicDto(data)
DTO->>ZodClass: super(data)
ZodClass->>Zod: schema.parse(data)
Note right of Zod: Strictness check applied in constructor too
end
Controller->>Controller: Process workflow
Controller-->>Client: 201 Created
| export interface ZodClass<T = unknown, Shape extends z.ZodRawShape = z.ZodRawShape> { | ||
| new (data: T): T; | ||
| schema: z.ZodObject<Shape>; | ||
| schema: z.ZodObject<Shape, z.UnknownKeysParam>; |
There was a problem hiding this comment.
What does this addition of z.UnknownKeysParam do? Why is it needed?
There was a problem hiding this comment.
I did a run with claude and it said this could be made explicit like this:
z.UnknownKeysParam is 'passthrough' | 'strict' | 'strip', but Z.class only ever produces two of those: z.object(shape) → 'strip', .strict() → 'strict'. It never passes through. So the annotation was wider than the implementation, and it can't just be dropped either (z.ZodObject defaults to 'strip', which excludes the strict branch).
There was a problem hiding this comment.
Nice just checked and this makes sense.
We do not need to use the entire UnknownKeysParam type but we should narrow it down.
…(no-changelog) `z.UnknownKeysParam` is `'passthrough' | 'strict' | 'strip'`, but `Z.class` only ever builds two of those: `z.object(shape)` is `'strip'` and `.strict()` is `'strict'`. Nothing passes through, so the annotation was wider than the implementation. Part of https://linear.app/n8n/issue/API-84 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ngelog) The factory returned `DtoClass as unknown as ZodClass<...>`, and casting through `unknown` meant nothing verified the declared `schema` type against the object the factory actually builds. Narrowing the type in the previous commit was therefore an unchecked claim. Dropping the `unknown` step makes it an enforced one: declaring a mode `Z.class` does not build now fails at the cast with TS2352. `Z.array` keeps its cast — unrelated to this change. Part of https://linear.app/n8n/issue/API-84 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Z.classalways built a lenientz.object, which strips a key it does not know. An endpoint whose contract is to reject that key had to define a second, strict schema and overrideschemaandsafeParseto point at it — whichCreateWorkflowPublicDtodid.Z.class(shape, { strict: true })builds the strict object once, so all four entry points agree:schema,safeParse,parseand the constructor.CreateWorkflowPublicDtouses it and drops both overrides. No behaviour change: the generated OpenAPI fragment is identical andPOST /workflows's integration tests are untouched.How to test
pnpm test src/__tests__/zod-class.test.tsinpackages/@n8n/api-types. Three new cases cover the default, the strict option, andextend.pnpm test:integration test/integration/public-api/workflows.test.tsinpackages/cli. 209 pass, and the file is not in this diff.pnpm build, thengit status. No generated YAML should appear.Related Linear tickets, Github issues, and Community forum posts
Part of https://linear.app/n8n/issue/API-84
Review / Merge checklist
Backport to Beta,Backport to Stable, orBackport to v1(if the PR is an urgent fix that needs to be backported)🤖 PR Summary generated by AI