Skip to content

Commit

Permalink
feat: add default zod support (#1924)
Browse files Browse the repository at this point in the history
  • Loading branch information
ruangustavo authored Feb 21, 2025
1 parent 3830a38 commit d5cd20a
Show file tree
Hide file tree
Showing 2 changed files with 278 additions and 1 deletion.
45 changes: 44 additions & 1 deletion packages/zod/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
jsStringEscape,
pascal,
resolveRef,
stringify,
ZodCoerceType,
} from '@orval/core';
import uniq from 'lodash.uniq';
Expand Down Expand Up @@ -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':
/**
Expand Down Expand Up @@ -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]);
Expand Down
234 changes: 234 additions & 0 deletions packages/zod/src/zod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down

0 comments on commit d5cd20a

Please sign in to comment.