|
| 1 | +import { vi } from 'vitest'; |
| 2 | +import * as z from 'zod/v4'; |
| 3 | + |
| 4 | +import { standardSchemaToJsonSchema } from '../../src/util/standardSchema.js'; |
| 5 | +import { isZodRawShape, normalizeRawShapeSchema } from '../../src/util/zodCompat.js'; |
| 6 | + |
| 7 | +describe('isZodRawShape', () => { |
| 8 | + test('treats empty object as a raw shape (matches v1)', () => { |
| 9 | + expect(isZodRawShape({})).toBe(true); |
| 10 | + }); |
| 11 | + test('detects raw shape with zod fields', () => { |
| 12 | + expect(isZodRawShape({ a: z.string() })).toBe(true); |
| 13 | + }); |
| 14 | + test('rejects a Standard Schema instance', () => { |
| 15 | + expect(isZodRawShape(z.object({ a: z.string() }))).toBe(false); |
| 16 | + }); |
| 17 | + test('rejects a shape with non-Zod Standard Schema fields', () => { |
| 18 | + const nonZod = { '~standard': { version: 1, vendor: 'arktype', validate: () => ({ value: 'x' }) } }; |
| 19 | + expect(isZodRawShape({ a: nonZod })).toBe(false); |
| 20 | + }); |
| 21 | + test('rejects a shape with Zod v3 fields (only v4 is wrappable)', () => { |
| 22 | + expect(isZodRawShape({ a: mockZodV3String() })).toBe(false); |
| 23 | + }); |
| 24 | + test('rejects non-plain objects with no own-enumerable properties', () => { |
| 25 | + expect(isZodRawShape([])).toBe(false); |
| 26 | + expect(isZodRawShape([z.string()])).toBe(false); |
| 27 | + expect(isZodRawShape(new Date())).toBe(false); |
| 28 | + expect(isZodRawShape(new Map())).toBe(false); |
| 29 | + expect(isZodRawShape(/regex/)).toBe(false); |
| 30 | + }); |
| 31 | + test('accepts a null-prototype plain object', () => { |
| 32 | + const o = Object.create(null); |
| 33 | + o.a = z.string(); |
| 34 | + expect(isZodRawShape(o)).toBe(true); |
| 35 | + }); |
| 36 | +}); |
| 37 | + |
| 38 | +// Minimal structural mock of a Zod v3 schema: has `_def.typeName` and |
| 39 | +// `~standard.vendor === 'zod'` (zod >=3.24), but no `_zod`. |
| 40 | +function mockZodV3String(): unknown { |
| 41 | + return { |
| 42 | + _def: { typeName: 'ZodString', checks: [], coerce: false }, |
| 43 | + '~standard': { version: 1, vendor: 'zod', validate: (v: unknown) => ({ value: v }) }, |
| 44 | + parse: (v: unknown) => v |
| 45 | + }; |
| 46 | +} |
| 47 | + |
| 48 | +describe('normalizeRawShapeSchema', () => { |
| 49 | + test('wraps empty raw shape into z.object({})', () => { |
| 50 | + const wrapped = normalizeRawShapeSchema({}); |
| 51 | + expect(wrapped).toBeDefined(); |
| 52 | + expect(standardSchemaToJsonSchema(wrapped!, 'input').type).toBe('object'); |
| 53 | + }); |
| 54 | + test('passes through an already-wrapped Standard Schema unchanged', () => { |
| 55 | + const schema = z.object({ a: z.string() }); |
| 56 | + expect(normalizeRawShapeSchema(schema)).toBe(schema); |
| 57 | + }); |
| 58 | + test('returns undefined for undefined input', () => { |
| 59 | + expect(normalizeRawShapeSchema(undefined)).toBeUndefined(); |
| 60 | + }); |
| 61 | + test('throws TypeError for an invalid object that is neither raw shape nor Standard Schema', () => { |
| 62 | + expect(() => normalizeRawShapeSchema({ a: 'not a zod schema' } as never)).toThrow(TypeError); |
| 63 | + }); |
| 64 | + test('passes through a Standard Schema without `~standard.jsonSchema` (per-vendor handling deferred to standardSchemaToJsonSchema)', () => { |
| 65 | + const noJson = { '~standard': { version: 1, vendor: 'x', validate: () => ({ value: {} }) } }; |
| 66 | + expect(normalizeRawShapeSchema(noJson as never)).toBe(noJson); |
| 67 | + }); |
| 68 | + test('passes through a zod 4.0-4.1 schema so standardSchemaToJsonSchema can apply its z.toJSONSchema fallback', () => { |
| 69 | + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); |
| 70 | + const real = z.object({ a: z.string() }); |
| 71 | + // Simulate zod 4.0-4.1: shadow `~standard` with `jsonSchema` removed, keep `_zod` intact. |
| 72 | + const { jsonSchema: _drop, ...stdNoJson } = real['~standard'] as unknown as Record<string, unknown>; |
| 73 | + void _drop; |
| 74 | + Object.defineProperty(real, '~standard', { value: { ...stdNoJson, vendor: 'zod' }, configurable: true }); |
| 75 | + |
| 76 | + const normalized = normalizeRawShapeSchema(real); |
| 77 | + expect(normalized).toBe(real); |
| 78 | + const json = standardSchemaToJsonSchema(normalized!, 'input'); |
| 79 | + expect(json.type).toBe('object'); |
| 80 | + expect((json.properties as Record<string, unknown>)?.a).toBeDefined(); |
| 81 | + warn.mockRestore(); |
| 82 | + }); |
| 83 | + test('throws actionable TypeError for a raw shape with Zod v3 fields', () => { |
| 84 | + expect(() => normalizeRawShapeSchema({ a: mockZodV3String() } as never)).toThrow(/Zod v4 schemas.*Got a Zod v3 field schema/); |
| 85 | + }); |
| 86 | + test('throws the intended TypeError (not Object.values crash) for null input', () => { |
| 87 | + expect(() => normalizeRawShapeSchema(null as never)).toThrow(/must be a Standard Schema/); |
| 88 | + }); |
| 89 | +}); |
0 commit comments