diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts index 826ab0e23..a3a0e9a98 100644 --- a/packages/zod/src/index.ts +++ b/packages/zod/src/index.ts @@ -18,6 +18,7 @@ import { jsStringEscape, pascal, resolveRef, + stringify, ZodCoerceType, } from '@orval/core'; import uniq from 'lodash.uniq'; @@ -146,6 +147,46 @@ export const generateZodValidationSchemaDefinition = ( const max = schema.maximum ?? schema.maxLength ?? schema.maxItems; const matches = schema.pattern ?? undefined; + let defaultVarName: string | undefined; + if (schema.default !== undefined) { + defaultVarName = `${name}Default${constsCounterValue}`; + let defaultValue: string; + + const isDateType = + schema.type === 'string' && + (schema.format === 'date' || schema.format === 'date-time') && + context.output.override.useDates; + + if (isDateType) { + defaultValue = `new Date("${escape(schema.default)}")`; + } else if (isObject(schema.default)) { + const entries = Object.entries(schema.default) + .map(([key, value]) => { + if (isString(value)) { + return `${key}: "${escape(value)}"`; + } + + if (Array.isArray(value)) { + const arrayItems = value.map((item) => + isString(item) ? `"${escape(item)}"` : `${item}`, + ); + return `${key}: [${arrayItems.join(', ')}]`; + } + + return `${key}: ${value}`; + }) + .join(', '); + defaultValue = `{ ${entries} }`; + } else { + const rawStringified = stringify(schema.default); + defaultValue = + rawStringified === undefined + ? 'null' + : rawStringified.replace(/'/g, '"'); + } + consts.push(`export const ${defaultVarName} = ${defaultValue};`); + } + switch (type) { case 'tuple': /** @@ -396,7 +437,9 @@ export const generateZodValidationSchemaDefinition = ( ]); } - if (!required && nullable) { + if (!required && schema.default) { + functions.push(['default', defaultVarName]); + } else if (!required && nullable) { functions.push(['nullish', undefined]); } else if (nullable) { functions.push(['nullable', undefined]); diff --git a/packages/zod/src/zod.test.ts b/packages/zod/src/zod.test.ts index 06f7398f7..73cd86e29 100644 --- a/packages/zod/src/zod.test.ts +++ b/packages/zod/src/zod.test.ts @@ -342,6 +342,240 @@ describe('generateZodValidationSchemaDefinition`', () => { consts: [], }); }); + + describe('default value handling', () => { + const context: ContextSpecs = { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs; + + it('generates a default value for a non-required string schema', () => { + const schemaWithDefault: SchemaObject = { + type: 'string', + default: 'hello', + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithDefault, + context, + 'testStringDefault', + false, + { required: false }, + ); + + expect(result).toEqual({ + functions: [ + ['string', undefined], + ['default', 'testStringDefaultDefault'], + ], + consts: [`export const testStringDefaultDefault = "hello";`], + }); + + const parsed = parseZodValidationSchemaDefinition(result, context, false); + expect(parsed.zod).toBe('zod.string().default(testStringDefaultDefault)'); + expect(parsed.consts).toBe( + 'export const testStringDefaultDefault = "hello";', + ); + }); + + it('generates a default value for a number schema', () => { + const schemaWithNumberDefault: SchemaObject = { + type: 'number', + default: 42, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithNumberDefault, + context, + 'testNumberDefault', + false, + { required: false }, + ); + + expect(result).toEqual({ + functions: [ + ['number', undefined], + ['default', 'testNumberDefaultDefault'], + ], + consts: ['export const testNumberDefaultDefault = 42;'], + }); + + const parsed = parseZodValidationSchemaDefinition(result, context, false); + expect(parsed.zod).toBe('zod.number().default(testNumberDefaultDefault)'); + expect(parsed.consts).toBe('export const testNumberDefaultDefault = 42;'); + }); + + it('generates a default value for a boolean schema', () => { + const schemaWithBooleanDefault: SchemaObject = { + type: 'boolean', + default: true, + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithBooleanDefault, + context, + 'testBooleanDefault', + false, + { required: false }, + ); + + expect(result).toEqual({ + functions: [ + ['boolean', undefined], + ['default', 'testBooleanDefaultDefault'], + ], + consts: ['export const testBooleanDefaultDefault = true;'], + }); + + const parsed = parseZodValidationSchemaDefinition(result, context, false); + expect(parsed.zod).toBe( + 'zod.boolean().default(testBooleanDefaultDefault)', + ); + expect(parsed.consts).toBe( + 'export const testBooleanDefaultDefault = true;', + ); + }); + + it('generates a default value for an array schema', () => { + const schemaWithArrayDefault: SchemaObject = { + type: 'array', + items: { type: 'string' }, + default: ['a', 'b'], + }; + + const result = generateZodValidationSchemaDefinition( + schemaWithArrayDefault, + context, + 'testArrayDefault', + false, + { required: false }, + ); + + expect(result).toEqual({ + functions: [ + ['array', { functions: [['string', undefined]], consts: [] }], + ['default', 'testArrayDefaultDefault'], + ], + consts: ['export const testArrayDefaultDefault = ["a", "b"];'], + }); + + const parsed = parseZodValidationSchemaDefinition(result, context, false); + expect(parsed.zod).toBe( + 'zod.array(zod.string()).default(testArrayDefaultDefault)', + ); + expect(parsed.consts).toBe( + 'export const testArrayDefaultDefault = ["a", "b"];', + ); + }); + + it('generates a default value for an object schema', () => { + const schemaWithObjectDefault: SchemaObject = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + }, + default: { name: 'Fluffy', age: 3 }, + }; + + const context: ContextSpecs = { + output: { + override: { useDates: false }, + }, + } as ContextSpecs; + + const result = generateZodValidationSchemaDefinition( + schemaWithObjectDefault, + context, + 'testObjectDefault', + false, + { required: false }, + ); + + expect(result).toEqual({ + functions: [ + [ + 'object', + { + name: { + functions: [ + ['string', undefined], + ['optional', undefined], + ], + consts: [], + }, + age: { + functions: [ + ['number', undefined], + ['optional', undefined], + ], + consts: [], + }, + }, + ], + ['default', 'testObjectDefaultDefault'], + ], + consts: [ + 'export const testObjectDefaultDefault = { name: "Fluffy", age: 3 };', + ], + }); + + const parsed = parseZodValidationSchemaDefinition(result, context, false); + expect(parsed.zod).toBe( + 'zod.object({\n "name": zod.string().optional(),\n "age": zod.number().optional()\n}).default(testObjectDefaultDefault)', + ); + expect(parsed.consts).toBe( + 'export const testObjectDefaultDefault = { name: "Fluffy", age: 3 };', + ); + }); + + it('generates a default value for a date schema with useDates enabled', () => { + const schemaWithDateDefault: SchemaObject = { + type: 'string', + format: 'date', + default: '2025-01-01', + }; + + const dateContext: ContextSpecs = { + output: { + override: { + useDates: true, + }, + }, + } as ContextSpecs; + + const result = generateZodValidationSchemaDefinition( + schemaWithDateDefault, + dateContext, + 'testDateDefault', + false, + { required: false }, + ); + + expect(result).toEqual({ + functions: [ + ['date', undefined], + ['default', 'testDateDefaultDefault'], + ], + consts: [ + 'export const testDateDefaultDefault = new Date("2025-01-01");', + ], + }); + + const parsed = parseZodValidationSchemaDefinition( + result, + dateContext, + false, + ); + expect(parsed.zod).toBe('zod.date().default(testDateDefaultDefault)'); + expect(parsed.consts).toBe( + 'export const testDateDefaultDefault = new Date("2025-01-01");', + ); + }); + }); }); const basicApiSchema = {