From dc3e6adf15b4e7a47e4f585358621c8a6c7677cc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 02:29:11 +0000 Subject: [PATCH 1/2] refactor: simplify zod-adapter to use Zod 4's built-in toJSONSchema Leverage Zod 4's native toJSONSchema() method instead of manually parsing internal _def structure. This reduces adapter complexity from ~250 lines to ~85 lines and improves maintainability. - Update peer dependency from zod ^3.0.0 to ^4.0.0 - Replace manual type inspection with schema.toJSONSchema() call - Update isZodSchema to detect toJSONSchema method presence - Simplify tests to use actual Zod schemas instead of mocks --- bun.lock | 2 +- package.json | 2 +- src/schemas/zod-adapter.ts | 226 +++---------------------------------- tests/zod-adapter.test.ts | 162 ++++++++++---------------- 4 files changed, 79 insertions(+), 313 deletions(-) diff --git a/bun.lock b/bun.lock index bd2e16e..6311bbd 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "zod": "^4.2.1", }, "peerDependencies": { - "zod": "^3.0.0", + "zod": "^4.0.0", }, "optionalPeers": [ "zod", diff --git a/package.json b/package.json index 3c1a8a0..bf761f2 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "node": ">=18.0.0" }, "peerDependencies": { - "zod": "^3.0.0" + "zod": "^4.0.0" }, "peerDependenciesMeta": { "zod": { diff --git a/src/schemas/zod-adapter.ts b/src/schemas/zod-adapter.ts index 227d0c8..0b46f8d 100644 --- a/src/schemas/zod-adapter.ts +++ b/src/schemas/zod-adapter.ts @@ -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; - type?: ZodTypeLike; - values?: readonly unknown[]; - checks?: Array<{ kind: string; value?: unknown }>; - options?: ZodTypeLike[]; - }; - shape?: Record; +type ZodSchemaWithJsonSchema = { + toJSONSchema?: () => JsonSchema; }; /** @@ -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 @@ -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 = {}; - 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 @@ -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' ); } diff --git a/tests/zod-adapter.test.ts b/tests/zod-adapter.test.ts index 0ca3a96..9141194 100644 --- a/tests/zod-adapter.test.ts +++ b/tests/zod-adapter.test.ts @@ -1,102 +1,76 @@ import { describe, expect, it } from 'bun:test'; +import { z } from 'zod'; import { isZodSchema, zodToJsonSchema } from '../src/schemas/zod-adapter'; -const createMockZodString = (checks: Array<{ kind: string; value?: unknown }> = []) => ({ - _def: { typeName: 'ZodString', checks }, -}); - -const createMockZodNumber = (checks: Array<{ kind: string; value?: unknown }> = []) => ({ - _def: { typeName: 'ZodNumber', checks }, -}); - -const createMockZodObject = ( - shape: Record, -) => ({ - _def: { - typeName: 'ZodObject', - shape: () => shape, - }, -}); - describe('zodToJsonSchema', () => { describe('primitive types', () => { - it('should convert ZodString', () => { - const schema = createMockZodString(); + it('should convert z.string()', () => { + const schema = z.string(); const result = zodToJsonSchema(schema); expect(result.type).toBe('string'); }); - it('should convert ZodString with constraints', () => { - const schema = createMockZodString([ - { kind: 'min', value: 1 }, - { kind: 'max', value: 100 }, - ]); + it('should convert z.string() with constraints', () => { + const schema = z.string().min(1).max(100); const result = zodToJsonSchema(schema); expect(result.type).toBe('string'); expect(result.minLength).toBe(1); expect(result.maxLength).toBe(100); }); - it('should convert ZodString with email format', () => { - const schema = createMockZodString([{ kind: 'email' }]); + it('should convert z.string().email()', () => { + const schema = z.string().email(); const result = zodToJsonSchema(schema); + expect(result.type).toBe('string'); expect(result.format).toBe('email'); }); - it('should convert ZodNumber', () => { - const schema = createMockZodNumber(); + it('should convert z.number()', () => { + const schema = z.number(); const result = zodToJsonSchema(schema); expect(result.type).toBe('number'); }); - it('should convert ZodNumber with constraints', () => { - const schema = createMockZodNumber([ - { kind: 'min', value: 0 }, - { kind: 'max', value: 100 }, - ]); + it('should convert z.number() with constraints', () => { + const schema = z.number().min(0).max(100); const result = zodToJsonSchema(schema); + expect(result.type).toBe('number'); expect(result.minimum).toBe(0); expect(result.maximum).toBe(100); }); - it('should convert ZodNumber with int check', () => { - const schema = createMockZodNumber([{ kind: 'int' }]); + it('should convert z.number().int()', () => { + const schema = z.number().int(); const result = zodToJsonSchema(schema); expect(result.type).toBe('integer'); }); - it('should convert ZodBoolean', () => { - const schema = { _def: { typeName: 'ZodBoolean' } }; + it('should convert z.boolean()', () => { + const schema = z.boolean(); const result = zodToJsonSchema(schema); expect(result.type).toBe('boolean'); }); - it('should convert ZodNull', () => { - const schema = { _def: { typeName: 'ZodNull' } }; + it('should convert z.null()', () => { + const schema = z.null(); const result = zodToJsonSchema(schema); expect(result.type).toBe('null'); }); }); describe('complex types', () => { - it('should convert ZodArray', () => { - const schema = { - _def: { - typeName: 'ZodArray', - type: createMockZodString(), - }, - }; + it('should convert z.array()', () => { + const schema = z.array(z.string()); const result = zodToJsonSchema(schema); expect(result.type).toBe('array'); expect(result.items?.type).toBe('string'); }); - it('should convert ZodObject', () => { - const schema = createMockZodObject({ - name: createMockZodString(), - age: createMockZodNumber(), + it('should convert z.object()', () => { + const schema = z.object({ + name: z.string(), + age: z.number(), }); - const result = zodToJsonSchema(schema); expect(result.type).toBe('object'); expect(result.properties?.name?.type).toBe('string'); @@ -106,81 +80,61 @@ describe('zodToJsonSchema', () => { }); it('should handle optional fields', () => { - const schema = createMockZodObject({ - required: createMockZodString(), - optional: { - _def: { - typeName: 'ZodOptional', - innerType: createMockZodString(), - }, - }, + const schema = z.object({ + required: z.string(), + optional: z.string().optional(), }); - const result = zodToJsonSchema(schema); expect(result.required).toContain('required'); expect(result.required).not.toContain('optional'); }); - it('should convert ZodEnum', () => { - const schema = { - _def: { - typeName: 'ZodEnum', - values: ['a', 'b', 'c'], - }, - }; + it('should convert z.enum()', () => { + const schema = z.enum(['a', 'b', 'c']); const result = zodToJsonSchema(schema); - expect(result.type).toBe('string'); expect(result.enum).toEqual(['a', 'b', 'c']); }); - it('should convert ZodUnion', () => { - const schema = { - _def: { - typeName: 'ZodUnion', - options: [createMockZodString(), createMockZodNumber()], - }, - }; + it('should convert z.union()', () => { + const schema = z.union([z.string(), z.number()]); const result = zodToJsonSchema(schema); - expect(result.oneOf?.length).toBe(2); + expect(result.anyOf?.length).toBe(2); }); - it('should convert ZodNullable', () => { - const schema = { - _def: { - typeName: 'ZodNullable', - innerType: createMockZodString(), - }, - }; + it('should convert z.nullable()', () => { + const schema = z.string().nullable(); const result = zodToJsonSchema(schema); - expect(result.oneOf?.length).toBe(2); + expect(result.anyOf?.length).toBe(2); }); }); - describe('edge cases', () => { - it('should return empty object for ZodAny', () => { - const schema = { _def: { typeName: 'ZodAny' } }; - const result = zodToJsonSchema(schema); - expect(result).toEqual({}); - }); - - it('should return empty object for unknown types', () => { - const schema = { _def: { typeName: 'ZodUnknownType' } }; - const result = zodToJsonSchema(schema); - expect(result).toEqual({}); + describe('error handling', () => { + it('should throw for objects without toJSONSchema', () => { + const notAZodSchema = { type: 'string' }; + expect(() => zodToJsonSchema(notAZodSchema)).toThrow( + 'Schema does not have toJSONSchema method', + ); }); - it('should return empty object when no _def', () => { - const schema = {}; - const result = zodToJsonSchema(schema); - expect(result).toEqual({}); + it('should throw for empty objects', () => { + const emptyObject = {}; + expect(() => zodToJsonSchema(emptyObject)).toThrow( + 'Schema does not have toJSONSchema method', + ); }); }); }); describe('isZodSchema', () => { - it('should return true for valid Zod schema', () => { - const schema = { _def: { typeName: 'ZodString' } }; - expect(isZodSchema(schema)).toBe(true); + it('should return true for Zod schemas', () => { + expect(isZodSchema(z.string())).toBe(true); + expect(isZodSchema(z.number())).toBe(true); + expect(isZodSchema(z.object({ name: z.string() }))).toBe(true); + }); + + it('should return true for objects with toJSONSchema method', () => { + const customSchema = { toJSONSchema: () => ({ type: 'string' }) }; + expect(isZodSchema(customSchema)).toBe(true); }); it('should return false for null', () => { @@ -200,4 +154,8 @@ describe('isZodSchema', () => { expect(isZodSchema(123)).toBe(false); expect(isZodSchema(true)).toBe(false); }); + + it('should return false for objects with non-function toJSONSchema', () => { + expect(isZodSchema({ toJSONSchema: 'not a function' })).toBe(false); + }); }); From d26b870254444ee37dbea2ee657d62674d3e1599 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 02:30:40 +0000 Subject: [PATCH 2/2] docs: update Zod version requirement to 4+ Update installation docs to specify Zod 4 or later is required for structured output validation with the SDK. --- docs/docs/installation.mdx | 6 +++--- docs/installation.mdx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/docs/installation.mdx b/docs/docs/installation.mdx index 57cbd83..c83f6f5 100644 --- a/docs/docs/installation.mdx +++ b/docs/docs/installation.mdx @@ -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: diff --git a/docs/installation.mdx b/docs/installation.mdx index cbd93f3..c05f854 100644 --- a/docs/installation.mdx +++ b/docs/installation.mdx @@ -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: