fix: validators enum type#3884
Conversation
|
|
🦋 Changeset detectedLatest commit: abc8ceb The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Caution
Debug code (console.log plus a hardcoded querySymbol lookup for #/components/schemas/PermissionAction) is left in packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts. Must be removed before this leaves draft — it will execute for every enum schema in every consumer's build.
TL;DR — Extracts the enum AST emitter out of the TypeScript plugin's export.ts into a dedicated enum.ts, simplifies the duplicate-key bookkeeping, and moves the empty-literalMembers short-circuit in the Zod v4 enum resolver into baseNode so custom resolvers also benefit.
Key changes
- Extract
exportEnumAstinto its own module — moves ~185 lines of enum emission out ofshared/export.tsintoshared/enum.ts, leavingexportAstto just dispatch. - Collapse key resolution into a single pre-pass — replaces the two-step
itemsWithAttempts+ per-itemresolveEnumKeycallsite with a singleresolveItemsWithKeysthat returns{ item, key }pairs. - Factor out
createSymbolMeta— removes three copies of the same{ resource: 'definition', tool: 'typescript', ... }literal insidebuildSymbolIncalls. - Move Zod v4 enum empty-literals guard into
baseNode—baseNodenow returnsunknownToAst({ type: 'unknown' })when there are no literal members, so custom~resolvers.enumget the same fallback as the default path. - Default
validatedpreset toenums: 'javascript'—dev/typescript/presets.tsnow passes the typescript plugin as an object so the Zod validator side has matching enum constants to reference.
Summary | 4 files | 1 commit | base: main ← fix/validators-enum-type
Debug code left in the Zod v4 enum resolver
Before:
baseNodecomputeditemsonce and emitted the Zod node.
After:baseNodealso queries a hardcoded resource ID andconsole.logs the result on every call.
The new querySymbol({ resourceId: '#/components/schemas/PermissionAction' }) + console.log(a) block (and the commented-out console.log(ctx.schema) below it) clearly belong to a local debugging session — the schema name is repo-specific to whatever spec was being inspected, the result is never used, and console.log is otherwise absent from this plugin tree. Drop the block entirely before un-drafting; otherwise every generated Zod schema using z.enum will emit a console.log at codegen time.
packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts
TypeScript enum extraction and key resolution
Before:
shared/export.tscarried the full enum emitter inline and computed each item'sduplicateAttemptseparately from its final cased key, re-runningtoCase+ the leading-digit underscore prefix per-iteration.
After:shared/enum.tsowns the emitter end-to-end;resolveItemsWithKeysproduces{ item, key }pairs in one pass;createSymbolMetadeduplicates theresource: 'definition', tool: 'typescript'boilerplate.
The behavior is preserved: resolveEnumKey({ duplicateAttempt: 0 }) deterministically produces the same baseKey that the old inline expression produced, the nameConflictResolver still receives the post-prefix key, and $.member / $.prop consume the resolved key in the same order. The minor cost is calling resolveEnumKey twice for duplicates (once to derive the base, once with the real attempt), which is negligible and worth the readability win.
packages/openapi-ts/src/plugins/@hey-api/typescript/shared/enum.ts · packages/openapi-ts/src/plugins/@hey-api/typescript/shared/export.ts
Empty-literals fallback hoisted into baseNode
Before:
enumToAstshort-circuited tounknownToAstwhenitemsNodeproduced no literal members, andenumResolverseparately returned the untouchedctx.chain.currenton the same condition.
After:baseNodeperforms the check itself and returnsunknownToAst({ type: 'unknown' }), so a user-supplied~resolvers.enumthat callsctx.nodes.base(ctx)gets the same fallback the default path used to get only insideenumToAst.
Worth confirming that no existing custom resolver relied on ctx.nodes.base(ctx) returning an "enum chain shape" specifically (the unknown chain is structurally a Zod expression and should slot into normal chain composition, but it's a behavior change for that surface).
packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts
Claude Opus | 𝕏
| const a = ctx.plugin.querySymbol({ | ||
| resource: 'definition', | ||
| resourceId: '#/components/schemas/PermissionAction', | ||
| }); | ||
| console.log(a); | ||
| // console.log(ctx.schema) |
There was a problem hiding this comment.
Debug leftovers — querySymbol with a hardcoded #/components/schemas/PermissionAction resource ID, an unused a, a console.log(a), and a commented-out console.log(ctx.schema). This will run for every enum schema in every consumer's build. Remove the block before un-drafting.
| const a = ctx.plugin.querySymbol({ | |
| resource: 'definition', | |
| resourceId: '#/components/schemas/PermissionAction', | |
| }); | |
| console.log(a); | |
| // console.log(ctx.schema) | |
| const { symbols } = ctx; | |
| const { z } = symbols; | |
| if (allStrings && enumMembers.length) { |
There was a problem hiding this comment.
Correction: the suggestion above duplicates the symbols/z destructuring already on lines 60-61 — please ignore the suggested block and just delete lines 63-68. Apologies for the noise.
Claude Opus | 𝕏
@hey-api/codegen-core
@hey-api/json-schema-ref-parser
@hey-api/nuxt
@hey-api/openapi-ts
@hey-api/shared
@hey-api/spec-types
@hey-api/types
@hey-api/vite-plugin
commit: |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3884 +/- ##
==========================================
+ Coverage 39.45% 39.48% +0.02%
==========================================
Files 533 534 +1
Lines 19636 19627 -9
Branches 5840 5854 +14
==========================================
+ Hits 7748 7749 +1
+ Misses 9637 9624 -13
- Partials 2251 2254 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
2b0b774 to
51d4a67
Compare
51d4a67 to
e9b1591
Compare
There was a problem hiding this comment.
TL;DR — Teaches the Zod validator plugin (v3, v4, mini) to reuse a TypeScript-emitted enum/const symbol via z.nativeEnum(...) / z.enum(...) when one exists, instead of always inlining literal members. Threads SchemaVisitorContext.path through every toAst entrypoint so the lookup can compute a resourceId, retires the walkerCtx field, and extracts the existing TS enum codegen into its own shared/enum.ts.
Key changes
- Reuse TS enum/const symbols in Zod validators — when
@hey-api/typescriptruns alongside Zod and emits a non-const enum or aconstobject for the same$ref,enumToAstnow emitsz.nativeEnum(Foo)(v3) orz.enum(Foo)(v4/mini) instead of repeating the literals. pathreplaceswalkerCtx— every ZodtoAstfunction and itsPick<...>option type now takespathdirectly, andBaseContextextendsSchemaVisitorContext.walkerCtx: ctxcall sites becomepath: ctx.path.exportEnumAstextracted — the TS enum emitter is split out of@hey-api/typescript/shared/export.tsinto a newshared/enum.ts, withresolveItemsWithKeysconsolidating the per-item duplicate-key bookkeeping into one pass.querySymbols()added toPluginInstance— companion toquerySymbol()returning the full array; the three existing internal call sites switch over.dev:ts/dev:pywatch scopes narrowed —tsx watchnow only re-runs on relevant input/config/preset files instead of the entire monorepo.
Summary | 57 files | 1 commit | base: main ← fix/validators-enum-type
Test coverage gap for the new lookup path
Before: Zod plugin emitted inline
z.enum([...])/z.union([...])literals regardless of co-plugins.
After: Zod plugin emitsz.nativeEnum(TsEnum)(v3) orz.enum(TsConst)(v4/mini) when a TS symbol matches the sameresourceId.
The Zod snapshot matrix under packages/openapi-ts-tests/zod/v{3,4}/__snapshots__/3.{0,1}.x/{mini,v3,v4}/ was not regenerated, and none of the existing scenarios co-load @hey-api/typescript. The new querySymbols branch is therefore unreachable from any test in this PR — existing snapshots stay green because the lookup always returns [] and the inline-literals fallback runs. Consider adding scenarios that combine @hey-api/typescript (with both enums: 'typescript-const' and enums: 'javascript' cells) and zod, so each of the three dialects produces a snapshot proving the z.nativeEnum(...) / z.enum(...) selection, mirroring the matrix symmetry the repo expects.
packages/openapi-ts/src/plugins/zod/v3/toAst/enum.ts · packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts · packages/openapi-ts/src/plugins/zod/mini/toAst/enum.ts
Ordering and SDK-validator path
The lookup assumes @hey-api/typescript has already registered its enum/var symbol by the time the Zod walker reaches the same $ref. With a user-supplied plugins array that lists zod before @hey-api/typescript, querySymbols returns [] and the inline literal fallback runs — silently — so the behavior degrades to today's output without any signal. Separately, createRequestSchemaContext and createResponseValidator* in all three api.ts files seed path: ref([]); nested walks inside an SDK validator layer therefore can never match a definition-scoped TS symbol. Both behaviors look intentional, but neither is documented, so future readers may chase phantom regressions.
packages/openapi-ts/src/plugins/zod/v3/api.ts · packages/openapi-ts/src/plugins/zod/v4/api.ts · packages/openapi-ts/src/plugins/zod/mini/api.ts
Claude Opus | 𝕏
|
|
||
| const def = ctx.plugin | ||
| .querySymbols({ | ||
| resource: 'definition', // maybe we shouldn't hardcode definition |
There was a problem hiding this comment.
Self-doubting TODO comment leaked into production code (same comment is duplicated in v4 and mini enum.ts). Either commit to 'definition' and drop the comment, or thread the resource type through the lookup. The 13-line querySymbols(...).filter(...)[0] block below is also identical across all three dialects — only identifiers.nativeEnum vs identifiers.enum differs — so extracting it to zod/shared/ would let one resolved version replace three copies.
|
|
||
| const def = ctx.plugin | ||
| .querySymbols({ | ||
| resource: 'definition', // maybe we shouldn't hardcode definition |
There was a problem hiding this comment.
Third copy of the // maybe we shouldn't hardcode definition comment and the same lookup block as v3/mini. Worth resolving once in a shared helper.
|
|
||
| const def = ctx.plugin | ||
| .querySymbols({ | ||
| resource: 'definition', // maybe we shouldn't hardcode definition |
There was a problem hiding this comment.
Third copy of the same TODO + lookup block as v3/v4. Extracting it to zod/shared/ would unify the dialect-only-differs-by-identifier logic.
| return this.querySymbols(filter)[0]; | ||
| } | ||
|
|
||
| querySymbols(filter: SymbolMeta): Array<Symbol<ResolvedNode>> { |
There was a problem hiding this comment.
New public method is welcome — a JSDoc clarifying that querySymbols returns symbols in registration (insertion) order would lock that contract in, since callers like the new Zod enum lookup take [0]. Today it relies on gen.symbols.query ordering by convention.
e9b1591 to
9a78bfd
Compare
9a78bfd to
1a078e9
Compare
1a078e9 to
c3a154e
Compare
@volkankalin feel free to try and let me know if there's anything missing! |
c3a154e to
abc8ceb
Compare
|
@mrlubos Thank you! I have tested and worked well, thank you! Although, one issue, when I try to use single output file, somehow can't make it work. I assume this is out of context or maybe I have misunderstood how the config should be set. Here is the config I used: createClient({
input: './enum-inline.yaml', // Example schema from the repo (specs\3.1.x\enum-inline.yaml)
output: {
fileName: 'EnumInlineOutput',
path: './generated',
},
parser: {
transforms: {
enums: 'root'
}
},
plugins: [
{
enums: 'typescript',
name: '@hey-api/typescript',
},
{
name: 'zod',
// 👇 Tried both with and without infer
types: {
infer: true
}
},
],
}); |
|
@volkankalin you can use parser hooks to pipe every symbol into a single file, see https://heyapi.dev/docs/openapi/typescript/configuration/parser#example-alphabetic-sort It's a low-level API so you get full control with it. I plan to abstract it one day, for now this is the only way |
|
@volkankalin ugh, I'm going to have to revert this nice feature – there are too many scenarios to handle and I don't have the time to dedicate to them right now. |
|
@mrlubos No worries at all. The current version we are using satisfies all the scenarios we need. So we will stick with this version for a while in that case. Thank you for the heads-up 🙏 |

No description provided.