···11+---
22+"@hey-api/openapi-ts": minor
33+---
44+55+**plugin(valibot)**: remove `enum.nodes.nullable` resolver node
66+77+### Removed resolver node
88+99+Valibot plugin no longer exposes the `enum.nodes.nullable` node. It was refactored so that nullable values are handled outside of resolvers.
+5
.changeset/stale-falcons-raise.md
···11+---
22+"@hey-api/openapi-ts": patch
33+---
44+55+**plugin(valibot)**: use `.nullable()` and `.nullish()` methods
+6
docs/openapi-ts/migrating.md
···7788While we try to avoid breaking changes, sometimes it's unavoidable in order to offer you the latest features. This page lists changes that require updates to your code. If you run into a problem with migration, please [open an issue](https://github.com/hey-api/openapi-ts/issues).
991010+## v0.93.0
1111+1212+### Removed resolver node
1313+1414+Valibot and Zod plugins no longer expose the `enum.nodes.nullable` node. Both plugins were refactored so that nullable values are handled outside of resolvers.
1515+1016## v0.92.0
11171218### Updated Symbol interface
···11import type { Refs, SymbolMeta } from '@hey-api/codegen-core';
22-import type { IR, SchemaExtractor } from '@hey-api/shared';
22+import type { IR, Walker } from '@hey-api/shared';
33import type ts from 'typescript';
4455import type { ValibotPlugin } from '../types';
66import type { Pipes } from './pipes';
77-import type { ProcessorContext } from './processor';
8798export type Ast = {
109 hasLazyExpression?: boolean;
···1514export type IrSchemaToAstOptions = {
1615 /** The plugin instance. */
1716 plugin: ValibotPlugin['Instance'];
1818- /** Optional schema extractor function. */
1919- schemaExtractor?: SchemaExtractor<ProcessorContext>;
2017 /** The plugin state references. */
2118 state: Refs<PluginState>;
1919+ walk: Walker<Ast, ValibotPlugin['Instance']>;
2220};
23212422export type PluginState = Pick<Required<SymbolMeta>, 'path'> &
···5858 parameterWithPagination,
5959} from './ir/parameter';
6060export { deduplicateSchema } from './ir/schema';
6161+export type {
6262+ SchemaExtractor,
6363+ SchemaProcessor,
6464+ SchemaProcessorContext,
6565+ SchemaProcessorResult,
6666+} from './ir/schema-processor';
6767+export { createSchemaProcessor } from './ir/schema-processor';
6868+export type { SchemaResult, SchemaVisitor, SchemaVisitorContext, Walker } from './ir/schema-walker';
6969+export { childContext, createSchemaWalker } from './ir/schema-walker';
6170export type { IR } from './ir/types';
6271export { addItemsToSchema } from './ir/utils';
6372export { parseOpenApiSpec } from './openApi';
···8796 OpenApiSchemaObject,
8897} from './openApi/types';
8998export type { Hooks } from './parser/hooks';
9090-export type {
9191- SchemaExtractor,
9292- SchemaProcessor,
9393- SchemaProcessorContext,
9494- SchemaProcessorResult,
9595-} from './plugins/schema-processor';
9696-export { createSchemaProcessor } from './plugins/schema-processor';
9799export type { SchemaWithType } from './plugins/shared/types/schema';
98100export { definePluginConfig, mappers } from './plugins/shared/utils/config';
99101export type { PluginInstanceTypes } from './plugins/shared/utils/instance';
+283
packages/shared/src/ir/schema-walker.ts
···11+import type { Ref } from '@hey-api/codegen-core';
22+import { fromRef, ref } from '@hey-api/codegen-core';
33+44+import type { SchemaWithType } from '../plugins/shared/types/schema';
55+import { deduplicateSchema } from './schema';
66+import type { IR } from './types';
77+88+/**
99+ * Result returned by visitor methods. Contains the expression plus metadata
1010+ * needed for modifier application.
1111+ */
1212+export interface SchemaResult<TExpr> {
1313+ /** Default value from schema, if any. */
1414+ default?: unknown;
1515+ /** The core schema expression, WITHOUT optional/nullable/default applied. */
1616+ expression: TExpr;
1717+ /** The original schema format. */
1818+ format?: string;
1919+ /** For libraries that need lazy evaluation. */
2020+ hasLazyExpression?: boolean;
2121+ /** Plugin-specific metadata bucket. */
2222+ meta?: Record<string, unknown>;
2323+ /** Does this schema explicitly allow null? */
2424+ nullable: boolean;
2525+ /** Is this schema read-only? */
2626+ readonly: boolean;
2727+}
2828+2929+/**
3030+ * Context passed to all visitor methods.
3131+ */
3232+export interface SchemaVisitorContext<TPlugin = unknown> {
3333+ /** Current path in the schema tree. */
3434+ path: Ref<ReadonlyArray<string | number>>;
3535+ /** The plugin instance. */
3636+ plugin: TPlugin;
3737+}
3838+3939+/**
4040+ * The walk function signature.
4141+ */
4242+export type Walker<TExpr, TPlugin = unknown> = (
4343+ schema: IR.SchemaObject,
4444+ ctx: SchemaVisitorContext<TPlugin>,
4545+) => SchemaResult<TExpr>;
4646+4747+/**
4848+ * The visitor interface. Plugins implement this to define how schemas
4949+ * are transformed into their target representation.
5050+ */
5151+export interface SchemaVisitor<TExpr, TPlugin = unknown> {
5252+ /**
5353+ * Apply modifiers (optional/nullable/nullish/default) to a schema result.
5454+ */
5555+ applyModifiers(
5656+ result: SchemaResult<TExpr>,
5757+ ctx: SchemaVisitorContext<TPlugin>,
5858+ context?: {
5959+ /** Is this property optional? */
6060+ optional?: boolean;
6161+ },
6262+ ): TExpr;
6363+ array(
6464+ schema: SchemaWithType<'array'>,
6565+ ctx: SchemaVisitorContext<TPlugin>,
6666+ walk: Walker<TExpr, TPlugin>,
6767+ ): SchemaResult<TExpr>;
6868+ boolean(
6969+ schema: SchemaWithType<'boolean'>,
7070+ ctx: SchemaVisitorContext<TPlugin>,
7171+ ): SchemaResult<TExpr>;
7272+ enum(
7373+ schema: SchemaWithType<'enum'>,
7474+ ctx: SchemaVisitorContext<TPlugin>,
7575+ walk: Walker<TExpr, TPlugin>,
7676+ ): SchemaResult<TExpr>;
7777+ integer(
7878+ schema: SchemaWithType<'integer'>,
7979+ ctx: SchemaVisitorContext<TPlugin>,
8080+ ): SchemaResult<TExpr>;
8181+ /**
8282+ * Called before any dispatch logic. Return a result to short-circuit,
8383+ * or undefined to continue normal dispatch.
8484+ */
8585+ intercept?(
8686+ schema: IR.SchemaObject,
8787+ ctx: SchemaVisitorContext<TPlugin>,
8888+ walk: Walker<TExpr, TPlugin>,
8989+ ): SchemaResult<TExpr> | undefined;
9090+ /**
9191+ * Handle intersection types. Receives already-walked child results.
9292+ */
9393+ intersection(
9494+ items: Array<SchemaResult<TExpr>>,
9595+ schemas: ReadonlyArray<IR.SchemaObject>,
9696+ ctx: SchemaVisitorContext<TPlugin>,
9797+ ): SchemaResult<TExpr>;
9898+ never(schema: SchemaWithType<'never'>, ctx: SchemaVisitorContext<TPlugin>): SchemaResult<TExpr>;
9999+ null(schema: SchemaWithType<'null'>, ctx: SchemaVisitorContext<TPlugin>): SchemaResult<TExpr>;
100100+ number(schema: SchemaWithType<'number'>, ctx: SchemaVisitorContext<TPlugin>): SchemaResult<TExpr>;
101101+ object(
102102+ schema: SchemaWithType<'object'>,
103103+ ctx: SchemaVisitorContext<TPlugin>,
104104+ walk: Walker<TExpr, TPlugin>,
105105+ ): SchemaResult<TExpr>;
106106+ /**
107107+ * Called after each typed schema visitor returns.
108108+ */
109109+ postProcess?(
110110+ result: SchemaResult<TExpr>,
111111+ schema: IR.SchemaObject,
112112+ ctx: SchemaVisitorContext<TPlugin>,
113113+ ): SchemaResult<TExpr>;
114114+ /**
115115+ * Handle $ref to another schema.
116116+ */
117117+ reference(
118118+ $ref: string,
119119+ schema: IR.SchemaObject,
120120+ ctx: SchemaVisitorContext<TPlugin>,
121121+ ): SchemaResult<TExpr>;
122122+ string(schema: SchemaWithType<'string'>, ctx: SchemaVisitorContext<TPlugin>): SchemaResult<TExpr>;
123123+ tuple(
124124+ schema: SchemaWithType<'tuple'>,
125125+ ctx: SchemaVisitorContext<TPlugin>,
126126+ walk: Walker<TExpr, TPlugin>,
127127+ ): SchemaResult<TExpr>;
128128+ undefined(
129129+ schema: SchemaWithType<'undefined'>,
130130+ ctx: SchemaVisitorContext<TPlugin>,
131131+ ): SchemaResult<TExpr>;
132132+ /**
133133+ * Handle union types. Receives already-walked child results.
134134+ */
135135+ union(
136136+ items: Array<SchemaResult<TExpr>>,
137137+ schemas: ReadonlyArray<IR.SchemaObject>,
138138+ ctx: SchemaVisitorContext<TPlugin>,
139139+ ): SchemaResult<TExpr>;
140140+ unknown(
141141+ schema: SchemaWithType<'unknown'>,
142142+ ctx: SchemaVisitorContext<TPlugin>,
143143+ ): SchemaResult<TExpr>;
144144+ void(schema: SchemaWithType<'void'>, ctx: SchemaVisitorContext<TPlugin>): SchemaResult<TExpr>;
145145+}
146146+147147+/**
148148+ * Create a schema walker from a visitor.
149149+ *
150150+ * The walker handles:
151151+ * - Dispatch order ($ref → type → items → fallback)
152152+ * - Deduplication of union/intersection schemas
153153+ * - Path tracking for child schemas
154154+ * - Calling the appropriate visitor method
155155+ */
156156+export function createSchemaWalker<TExpr, TPlugin = unknown>(
157157+ visitor: SchemaVisitor<TExpr, TPlugin>,
158158+): Walker<TExpr, TPlugin> {
159159+ const walk: Walker<TExpr, TPlugin> = (schema, ctx) => {
160160+ // escape hatch
161161+ if (visitor.intercept) {
162162+ const intercepted = visitor.intercept(schema, ctx, walk);
163163+ if (intercepted !== undefined) {
164164+ return intercepted;
165165+ }
166166+ }
167167+168168+ const baseResult = {
169169+ default: schema.default,
170170+ readonly: schema.accessScope === 'read',
171171+ };
172172+173173+ if (schema.$ref) {
174174+ const result = visitor.reference(schema.$ref, schema, ctx);
175175+ return {
176176+ ...result,
177177+ default: result.default ?? baseResult.default,
178178+ readonly: result.readonly || baseResult.readonly,
179179+ };
180180+ }
181181+182182+ if (schema.type) {
183183+ let result = visitTyped(schema as SchemaWithType, ctx, visitor, walk);
184184+ if (visitor.postProcess) {
185185+ result = visitor.postProcess(result, schema, ctx);
186186+ }
187187+ return {
188188+ ...result,
189189+ default: result.default ?? baseResult.default,
190190+ readonly: result.readonly || baseResult.readonly,
191191+ };
192192+ }
193193+194194+ if (schema.items) {
195195+ const deduplicated = deduplicateSchema({ schema });
196196+197197+ // deduplication might collapse to a single schema
198198+ if (!deduplicated.items) {
199199+ return walk(deduplicated, ctx);
200200+ }
201201+202202+ const itemResults = deduplicated.items.map((item, index) =>
203203+ walk(item, {
204204+ ...ctx,
205205+ path: ref([...fromRef(ctx.path), 'items', index]),
206206+ }),
207207+ );
208208+209209+ const result =
210210+ deduplicated.logicalOperator === 'and'
211211+ ? visitor.intersection(itemResults, deduplicated.items, ctx)
212212+ : visitor.union(itemResults, deduplicated.items, ctx);
213213+214214+ return {
215215+ ...result,
216216+ default: result.default ?? baseResult.default,
217217+ readonly: result.readonly || baseResult.readonly,
218218+ };
219219+ }
220220+221221+ // fallback
222222+ const result = visitor.unknown({ type: 'unknown' }, ctx);
223223+ return {
224224+ ...result,
225225+ default: result.default ?? baseResult.default,
226226+ readonly: result.readonly || baseResult.readonly,
227227+ };
228228+ };
229229+230230+ return walk;
231231+}
232232+233233+/**
234234+ * Dispatch to the appropriate visitor method based on schema type.
235235+ */
236236+function visitTyped<TExpr, TPlugin>(
237237+ schema: SchemaWithType,
238238+ ctx: SchemaVisitorContext<TPlugin>,
239239+ visitor: SchemaVisitor<TExpr, TPlugin>,
240240+ walk: Walker<TExpr, TPlugin>,
241241+): SchemaResult<TExpr> {
242242+ switch (schema.type) {
243243+ case 'array':
244244+ return visitor.array(schema as SchemaWithType<'array'>, ctx, walk);
245245+ case 'boolean':
246246+ return visitor.boolean(schema as SchemaWithType<'boolean'>, ctx);
247247+ case 'enum':
248248+ return visitor.enum(schema as SchemaWithType<'enum'>, ctx, walk);
249249+ case 'integer':
250250+ return visitor.integer(schema as SchemaWithType<'integer'>, ctx);
251251+ case 'never':
252252+ return visitor.never(schema as SchemaWithType<'never'>, ctx);
253253+ case 'null':
254254+ return visitor.null(schema as SchemaWithType<'null'>, ctx);
255255+ case 'number':
256256+ return visitor.number(schema as SchemaWithType<'number'>, ctx);
257257+ case 'object':
258258+ return visitor.object(schema as SchemaWithType<'object'>, ctx, walk);
259259+ case 'string':
260260+ return visitor.string(schema as SchemaWithType<'string'>, ctx);
261261+ case 'tuple':
262262+ return visitor.tuple(schema as SchemaWithType<'tuple'>, ctx, walk);
263263+ case 'undefined':
264264+ return visitor.undefined(schema as SchemaWithType<'undefined'>, ctx);
265265+ case 'unknown':
266266+ return visitor.unknown(schema as SchemaWithType<'unknown'>, ctx);
267267+ case 'void':
268268+ return visitor.void(schema as SchemaWithType<'void'>, ctx);
269269+ }
270270+}
271271+272272+/**
273273+ * Helper to create a child context with an extended path.
274274+ */
275275+export function childContext<TPlugin>(
276276+ ctx: SchemaVisitorContext<TPlugin>,
277277+ ...segments: ReadonlyArray<string | number>
278278+): SchemaVisitorContext<TPlugin> {
279279+ return {
280280+ ...ctx,
281281+ path: ref([...fromRef(ctx.path), ...segments]),
282282+ };
283283+}
+1-1
packages/shared/src/parser/hooks.ts
···11import type { Node, Symbol, SymbolIn } from '@hey-api/codegen-core';
2233+import type { SchemaProcessorContext } from '../ir/schema-processor';
34import type { IROperationObject } from '../ir/types';
44-import type { SchemaProcessorContext } from '../plugins/schema-processor';
55import type { PluginInstance } from '../plugins/shared/utils/instance';
6677export type Hooks = {
···11-import type { IR } from '../ir/types';
21import { pathToJsonPointer } from '../utils/ref';
22+import type { IR } from './types';
3344export interface SchemaProcessor {
55 /** Current inherited context (set by withContext) */