Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions docs/docs/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ console.log('CLI installed at:', cliPath);

## Optional Dependencies

### Zod (Recommended)
### Zod 4+ (Recommended)

For structured output validation, install Zod:
For structured output validation, install Zod 4 or later:

```bash
npm install zod
npm install zod@^4
```

Then use it with the SDK:
Expand Down
6 changes: 3 additions & 3 deletions docs/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ console.log('CLI installed at:', cliPath);

## Optional Dependencies

### Zod (Recommended)
### Zod 4+ (Recommended)

For structured output validation, install Zod:
For structured output validation, install Zod 4 or later:

```bash
npm install zod
npm install zod@^4
```

Then use it with the SDK:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"node": ">=18.0.0"
},
"peerDependencies": {
"zod": "^3.0.0"
"zod": "^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
Expand Down
226 changes: 17 additions & 209 deletions src/schemas/zod-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import type { JsonSchema } from '../types';

/**
* Internal type representing a Zod-like schema structure.
*
* This type allows the SDK to work with Zod schemas without requiring
* Zod as a direct dependency.
* Internal type representing a Zod schema with toJSONSchema method (Zod 4+).
*
* @internal
*/
type ZodTypeLike = {
_def?: {
typeName?: string;
innerType?: ZodTypeLike;
shape?: () => Record<string, ZodTypeLike>;
type?: ZodTypeLike;
values?: readonly unknown[];
checks?: Array<{ kind: string; value?: unknown }>;
options?: ZodTypeLike[];
};
shape?: Record<string, ZodTypeLike>;
type ZodSchemaWithJsonSchema = {
toJSONSchema?: () => JsonSchema;
};

/**
Expand All @@ -27,15 +15,11 @@ type ZodTypeLike = {
* This enables the SDK to accept Zod schemas for structured output
* validation and convert them to JSON Schema format for the CLI.
*
* Supported Zod types:
* - Primitives: string, number, boolean, null, undefined
* - Literals and enums
* - Arrays and objects
* - Optional, nullable, and default
* - Union, intersection, record, tuple
* Requires Zod 4+ which has built-in `toJsonSchema()` support.
*
* @param schema - A Zod schema or Zod-like object
* @param schema - A Zod schema with toJsonSchema method
* @returns Equivalent JSON Schema object
* @throws Error if the schema doesn't have a toJsonSchema method
*
* @example
* ```typescript
Expand All @@ -61,197 +45,21 @@ type ZodTypeLike = {
*
* @category Schema
*/
export function zodToJsonSchema(schema: ZodTypeLike): JsonSchema {
const def = schema._def;
if (!def) {
return {};
}

const typeName = def.typeName;

switch (typeName) {
case 'ZodString':
return buildStringSchema(def);

case 'ZodNumber':
return buildNumberSchema(def);

case 'ZodBoolean':
return { type: 'boolean' };

case 'ZodNull':
return { type: 'null' };

case 'ZodUndefined':
return {};

case 'ZodLiteral':
return { const: def.values };

case 'ZodEnum':
return { type: 'string', enum: def.values as unknown[] };

case 'ZodNativeEnum':
return { type: 'string', enum: Object.values(def.values as object) };

case 'ZodArray':
return {
type: 'array',
items: def.type ? zodToJsonSchema(def.type) : {},
};

case 'ZodObject':
return buildObjectSchema(schema, def);

case 'ZodOptional':
return def.innerType ? zodToJsonSchema(def.innerType) : {};

case 'ZodNullable': {
const inner = def.innerType ? zodToJsonSchema(def.innerType) : {};
return { oneOf: [inner, { type: 'null' }] };
}

case 'ZodDefault':
return def.innerType ? zodToJsonSchema(def.innerType) : {};

case 'ZodUnion':
return {
oneOf: (def.options ?? []).map((opt) => zodToJsonSchema(opt)),
};

case 'ZodIntersection':
return {
allOf: [def.innerType ? zodToJsonSchema(def.innerType) : {}],
};

case 'ZodRecord':
return {
type: 'object',
additionalProperties: def.type ? zodToJsonSchema(def.type) : true,
};

case 'ZodTuple':
return { type: 'array' };

case 'ZodAny':
case 'ZodUnknown':
return {};

default:
return {};
}
}

/**
* Builds a JSON Schema for a Zod string type.
*
* @param def - The Zod definition object
* @returns JSON Schema for a string type
*
* @internal
*/
function buildStringSchema(def: ZodTypeLike['_def']): JsonSchema {
const schema: JsonSchema = { type: 'string' };

for (const check of def?.checks ?? []) {
switch (check.kind) {
case 'min':
schema.minLength = check.value as number;
break;
case 'max':
schema.maxLength = check.value as number;
break;
case 'length':
schema.minLength = check.value as number;
schema.maxLength = check.value as number;
break;
case 'email':
schema.format = 'email';
break;
case 'url':
schema.format = 'uri';
break;
case 'uuid':
schema.format = 'uuid';
break;
case 'regex':
schema.pattern = String(check.value);
break;
}
}

return schema;
}

/**
* Builds a JSON Schema for a Zod number type.
*
* @param def - The Zod definition object
* @returns JSON Schema for a number type
*
* @internal
*/
function buildNumberSchema(def: ZodTypeLike['_def']): JsonSchema {
const schema: JsonSchema = { type: 'number' };

for (const check of def?.checks ?? []) {
switch (check.kind) {
case 'min':
schema.minimum = check.value as number;
break;
case 'max':
schema.maximum = check.value as number;
break;
case 'int':
schema.type = 'integer';
break;
case 'multipleOf':
schema.multipleOf = check.value as number;
break;
}
}

return schema;
}

/**
* Builds a JSON Schema for a Zod object type.
*
* @param schema - The Zod schema object
* @param def - The Zod definition object
* @returns JSON Schema for an object type
*
* @internal
*/
function buildObjectSchema(schema: ZodTypeLike, def: ZodTypeLike['_def']): JsonSchema {
const shape = def?.shape?.() ?? schema.shape ?? {};
const properties: Record<string, JsonSchema> = {};
const required: string[] = [];

for (const [key, value] of Object.entries(shape)) {
properties[key] = zodToJsonSchema(value as ZodTypeLike);

const valueDef = (value as ZodTypeLike)._def;
const isOptional = valueDef?.typeName === 'ZodOptional' || valueDef?.typeName === 'ZodDefault';

if (!isOptional) {
required.push(key);
}
export function zodToJsonSchema(schema: ZodSchemaWithJsonSchema): JsonSchema {
if (typeof schema.toJSONSchema !== 'function') {
throw new Error(
'Schema does not have toJSONSchema method. Ensure you are using Zod 4+ or provide a JSON Schema directly.',
);
}

return {
type: 'object',
properties,
required: required.length > 0 ? required : undefined,
additionalProperties: false,
};
return schema.toJSONSchema();
}

/**
* Type guard to check if a value is a Zod schema.
* Type guard to check if a value is a Zod schema (Zod 4+).
*
* @param value - Value to check
* @returns True if the value appears to be a Zod schema
* @returns True if the value appears to be a Zod schema with toJsonSchema support
*
* @example
* ```typescript
Expand All @@ -266,11 +74,11 @@ function buildObjectSchema(schema: ZodTypeLike, def: ZodTypeLike['_def']): JsonS
*
* @category Schema
*/
export function isZodSchema(value: unknown): value is ZodTypeLike {
export function isZodSchema(value: unknown): value is ZodSchemaWithJsonSchema {
return (
typeof value === 'object' &&
value !== null &&
'_def' in value &&
typeof (value as ZodTypeLike)._def === 'object'
'toJSONSchema' in value &&
typeof (value as ZodSchemaWithJsonSchema).toJSONSchema === 'function'
);
}
Loading