Skip to content

refactor(API): Add a strict option to Z.class (no-changelog) - #36916

Open
uddish wants to merge 4 commits into
masterfrom
api-84-zod-class-strict
Open

refactor(API): Add a strict option to Z.class (no-changelog)#36916
uddish wants to merge 4 commits into
masterfrom
api-84-zod-class-strict

Conversation

@uddish

@uddish uddish commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

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 — which CreateWorkflowPublicDto did.

  • Z.class(shape, { strict: true }) builds the strict object once, so all four entry points agree: schema, safeParse, parse and the constructor.
  • CreateWorkflowPublicDto uses it and drops both overrides. No behaviour change: the generated OpenAPI fragment is identical and POST /workflows's integration tests are untouched.

How to test

  • pnpm test src/__tests__/zod-class.test.ts in packages/@n8n/api-types. Three new cases cover the default, the strict option, and extend.
  • pnpm test:integration test/integration/public-api/workflows.test.ts in packages/cli. 209 pass, and the file is not in this diff.
  • pnpm build, then git 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

  • I have seen this code, I have run this code, and I take responsibility for this code.
  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with Backport to Beta, Backport to Stable, or Backport to v1 (if the PR is an urgent fix that needs to be backported)

🤖 PR Summary generated by AI

Review in cubic

@n8n-assistant

n8n-assistant Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

PR review overview

Based on ownership of the 3 changed files in this PR:

Ownership Files owned Share Source code Test files Misc
@n8n-io/catalysts 3 100% +13 / -16 +26 / -0 +0 / -0
Total 3 100% +13 / -16 +26 / -0 +0 / -0

@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 4.37kB (0.01%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
editor-ui-esm 63.05MB 4.37kB (0.01%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: editor-ui-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/src-*.js 1.47kB 3.34MB 0.04%
assets/ParameterInputList-*.js 300 bytes 1.47MB 0.02%
assets/workflows.store-*.js 2.34kB 825.0kB 0.28%
assets/builder.store-*.js 30 bytes 111.75kB 0.03%
assets/CanvasRunWorkflowButton-*.js 222 bytes 97.09kB 0.23%

@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@n8n-assistant n8n-assistant Bot added the n8n team Authored by the n8n team label Aug 24, 2026
@uddish
uddish force-pushed the api-84-zod-class-strict branch from 9b7557e to aa9d779 Compare August 24, 2026 12:04
@uddish
uddish marked this pull request as ready for review August 24, 2026 12:25
uddish and others added 2 commits August 24, 2026 13:26
`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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Loading

Re-trigger cubic

@uddish
uddish force-pushed the api-84-zod-class-strict branch from aa9d779 to b9e36bd Compare August 24, 2026 12:28
@uddish
uddish marked this pull request as draft August 24, 2026 12:28
@uddish
uddish marked this pull request as ready for review August 24, 2026 12:30

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Loading

Re-trigger cubic

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>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this addition of z.UnknownKeysParam do? Why is it needed?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice just checked and this makes sense.
We do not need to use the entire UnknownKeysParam type but we should narrow it down.

uddish and others added 2 commits August 24, 2026 14:25
…(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>

@SamWooler SamWooler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great 👏

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

Labels

cla-signed n8n team Authored by the n8n team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants