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 packages/orval/src/reusable-schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ describe('generateReusableSchemaSet', () => {
`zodParams({"operationId":"","location":"schema","schemaName":"Pet","fieldPath":["name"],"validator":"string"})`,
);
expect(entry.zod).toContain(
`zodParams({"operationId":"","location":"schema","schemaName":"Pet","fieldPath":["age"],"validator":"number"})`,
`zod.number(zodParams({"operationId":"","location":"schema","schemaName":"Pet","fieldPath":["age"],"validator":"number"})).int(zodParams({"operationId":"","location":"schema","schemaName":"Pet","fieldPath":["age"],"validator":"int"}))`,
);
});

Expand Down
46 changes: 31 additions & 15 deletions packages/zod/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,7 @@ const resolveZodType = (schema: OpenApiSchemaObject): ResolvedZodType => {
// Filter out 'null' type as it's handled separately via nullable
const nonNullTypes = schemaTypeValue
.filter((t): t is string => isString(t))
.filter((t) => t !== 'null' && possibleSchemaTypes.has(t))
.map((t) => (t === 'integer' ? 'number' : t));
.filter((t) => t !== 'null' && possibleSchemaTypes.has(t));

// If multiple types, return a special marker for union handling
if (nonNullTypes.length > 1) {
Expand Down Expand Up @@ -148,14 +147,7 @@ const resolveZodType = (schema: OpenApiSchemaObject): ResolvedZodType => {
if (constValue === null) return 'null';
}

switch (type) {
case 'integer': {
return 'number';
}
default: {
return type ?? 'unknown';
}
}
return type ?? 'unknown';
};

// https://github.com/colinhacks/zod#coercion-for-primitives
Expand Down Expand Up @@ -197,7 +189,7 @@ export interface ZodValidationSchemaDefinition {
consts: string[];
}

const minAndMaxTypes = new Set(['number', 'string', 'array']);
const minAndMaxTypes = new Set(['number', 'integer', 'string', 'array']);

const removeReadOnlyProperties = (
schema: OpenApiSchemaObject,
Expand Down Expand Up @@ -1244,7 +1236,7 @@ export const generateZodValidationSchemaDefinition = (
break;
}

functions.push([type, undefined]);
functions.push([type === 'integer' ? 'int' : type, undefined]);

break;
}
Expand Down Expand Up @@ -1778,6 +1770,18 @@ ${Object.entries(objectArgs)

const combinedArgs = buildCombinedArgs(fn, args, fieldPath);

if (fn === 'int' && shouldCoerce('number')) {
const numberArgs = buildCombinedArgs('number', undefined, fieldPath);
current = {
expr: zodMiniCall(
'pipe',
`${zodMiniCoerceCall('number', numberArgs)}, ${zodMiniCall('int', combinedArgs)}`,
),
kind: 'number',
};
continue;
}

if (fn === 'optional' || fn === 'nullable' || fn === 'nullish') {
const value = requireCurrent(fn);
current = { expr: zodMiniCall(fn, value.expr), kind: value.kind };
Expand Down Expand Up @@ -1843,9 +1847,11 @@ ${Object.entries(objectArgs)
current = {
expr: zodMiniCall(fn, combinedArgs),
kind:
fn === 'enum' || fn === 'literal' || fn === 'stringFormat'
? 'string'
: fn.split('.')[0],
fn === 'int'
? 'number'
: fn === 'enum' || fn === 'literal' || fn === 'stringFormat'
? 'string'
: fn.split('.')[0],
};
}

Expand Down Expand Up @@ -2161,6 +2167,16 @@ ${Object.entries(objectArgs)
combinedArgs = formattedArgs;
}

if (fn === 'int') {
const numberArgs = buildCombinedArgs('number', undefined, fieldPath);
if (shouldCoerce('number')) {
return `.coerce.number(${numberArgs}).int(${combinedArgs})`;
}
if (!isZodV4) {
return `.number(${numberArgs}).int(${combinedArgs})`;
}
}

if (
(fn !== 'date' && shouldCoerceType) ||
(fn === 'date' && shouldCoerceType && context.output.override.useDates)
Expand Down
250 changes: 247 additions & 3 deletions packages/zod/src/zod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,70 @@ describe('parseZodValidationSchemaDefinition', () => {
);
});

it('renders zod mini integer bounds as numeric checks', () => {
const parseResult = parseZodValidationSchemaDefinition(
{
functions: [
['int', undefined],
['min', 'ageMin'],
['max', 'ageMax'],
['multipleOf', 'ageMultipleOf'],
['optional', undefined],
],
consts: [],
},
{
output: {
override: {
useDates: false,
},
},
} as ContextSpec,
false,
false,
true,
undefined,
undefined,
'mini',
);

expect(parseResult.zod).toBe(
'/*#__PURE__*/ zod.optional(/*#__PURE__*/ zod.int().check(/*#__PURE__*/ zod.gte(ageMin)).check(/*#__PURE__*/ zod.lte(ageMax)).check(/*#__PURE__*/ zod.multipleOf(ageMultipleOf)))',
);
});

it('renders zod mini coerced integer bounds as checks on the pipe', () => {
const parseResult = parseZodValidationSchemaDefinition(
{
functions: [
['int', undefined],
['min', 'ageMin'],
['max', 'ageMax'],
['multipleOf', 'ageMultipleOf'],
['optional', undefined],
],
consts: [],
},
{
output: {
override: {
useDates: false,
},
},
} as ContextSpec,
true,
false,
true,
undefined,
undefined,
'mini',
);

expect(parseResult.zod).toBe(
'/*#__PURE__*/ zod.optional(/*#__PURE__*/ zod.pipe(/*#__PURE__*/ zod.coerce.number(), /*#__PURE__*/ zod.int()).check(/*#__PURE__*/ zod.gte(ageMin)).check(/*#__PURE__*/ zod.lte(ageMax)).check(/*#__PURE__*/ zod.multipleOf(ageMultipleOf)))',
);
});

it('renders zod mini allOf fallback as intersections', () => {
const parseResult = parseZodValidationSchemaDefinition(
{
Expand Down Expand Up @@ -464,6 +528,80 @@ describe('parseZodValidationSchemaDefinition with params injection', () => {
);
});

it('injects integer params on the int validator for every target', () => {
const input: ZodValidationSchemaDefinition = {
functions: [
['int', undefined],
['optional', undefined],
],
consts: [],
};
const params =
'zodParams({"operationId":"createUser","location":"body","schemaName":"CreateUserBody","fieldPath":[],"validator":"int"})';
const numberParams =
'zodParams({"operationId":"createUser","location":"body","schemaName":"CreateUserBody","fieldPath":[],"validator":"number"})';

expect(
parseZodValidationSchemaDefinition(
input,
ctx,
false,
false,
false,
undefined,
makeInjection(),
).zod,
).toBe(`zod.number(${numberParams}).int(${params}).optional()`);
expect(
parseZodValidationSchemaDefinition(
input,
ctx,
true,
false,
true,
undefined,
makeInjection(),
).zod,
).toBe(`zod.coerce.number(${numberParams}).int(${params}).optional()`);
expect(
parseZodValidationSchemaDefinition(
input,
ctx,
false,
false,
true,
undefined,
makeInjection(),
).zod,
).toBe(`zod.int(${params}).optional()`);
expect(
parseZodValidationSchemaDefinition(
input,
ctx,
false,
false,
true,
undefined,
makeInjection(),
'mini',
).zod,
).toBe(`/*#__PURE__*/ zod.optional(/*#__PURE__*/ zod.int(${params}))`);
expect(
parseZodValidationSchemaDefinition(
input,
ctx,
true,
false,
true,
undefined,
makeInjection(),
'mini',
).zod,
).toBe(
`/*#__PURE__*/ zod.optional(/*#__PURE__*/ zod.pipe(/*#__PURE__*/ zod.coerce.number(${numberParams}), /*#__PURE__*/ zod.int(${params})))`,
);
});

it('skips nullary validators (unknown, any, never, null, undefined, void)', () => {
for (const fn of ['unknown', 'any', 'never', 'null', 'undefined', 'void']) {
const input: ZodValidationSchemaDefinition = {
Expand Down Expand Up @@ -3533,6 +3671,112 @@ describe('generateZodValidationSchemaDefinition`', () => {
);
expect(parsed.zod).toBe('zod.number().optional()');
});
it('generates native integer schemas for each supported Zod target', () => {
const schema: OpenApiSchemaObject = {
type: 'integer',
};

const resultV3 = generateZodValidationSchemaDefinition(
schema,
context,
'testInteger',
false,
false,
{ required: false },
);

expect(resultV3).toEqual({
functions: [
['int', undefined],
['optional', undefined],
],
consts: [],
});
expect(
parseZodValidationSchemaDefinition(
resultV3,
context,
false,
false,
false,
).zod,
).toBe('zod.number().int().optional()');

const resultV4 = generateZodValidationSchemaDefinition(
schema,
context,
'testInteger',
false,
true,
{ required: false },
);

expect(resultV4).toEqual({
functions: [
['int', undefined],
['optional', undefined],
],
consts: [],
});
expect(
parseZodValidationSchemaDefinition(
resultV4,
context,
false,
false,
true,
).zod,
).toBe('zod.int().optional()');
expect(
parseZodValidationSchemaDefinition(
resultV4,
context,
false,
false,
true,
undefined,
undefined,
'mini',
).zod,
).toBe('/*#__PURE__*/ zod.optional(/*#__PURE__*/ zod.int())');
expect(
parseZodValidationSchemaDefinition(resultV4, context, true, false, true)
.zod,
).toBe('zod.coerce.number().int().optional()');
expect(
parseZodValidationSchemaDefinition(
resultV4,
context,
true,
false,
true,
undefined,
undefined,
'mini',
).zod,
).toBe(
'/*#__PURE__*/ zod.optional(/*#__PURE__*/ zod.pipe(/*#__PURE__*/ zod.coerce.number(), /*#__PURE__*/ zod.int()))',
);
});
it('coerces integer schemas on the Zod v3 target', () => {
const schema: OpenApiSchemaObject = {
type: 'integer',
};

const result = generateZodValidationSchemaDefinition(
schema,
context,
'testCoercedInteger',
false,
false,
{ required: false },
);

expect(
parseZodValidationSchemaDefinition(result, context, true, false, false)
.zod,
).toBe('zod.coerce.number().int().optional()');
});
it('generates an number with min', () => {
const schema: OpenApiSchemaObject = {
type: 'number',
Expand Down Expand Up @@ -6701,7 +6945,7 @@ describe('generateZod required defaults regression (#2987)', () => {
/"number": zod\.number\(\)\.default\(getGizmoResponseNumberDefault\)/,
);
expect(result.implementation).toMatch(
/"integer": zod\.number\(\)\.default\(getGizmoResponseIntegerDefault\)/,
/"integer": zod\.int\(\)\.default\(getGizmoResponseIntegerDefault\)/,
);
expect(result.implementation).toMatch(
/"nullableString": zod\.string\(\)\.nullish\(\)\.default\(getGizmoResponseNullableStringDefault\)/,
Expand Down Expand Up @@ -6854,9 +7098,9 @@ describe('generateZodWithMultiTypeArray', () => {
);

expect(parsed.zod).toContain('zod.union([');
expect(parsed.zod).toContain('zod.number()');
expect(parsed.zod).toContain('zod.int()');
expect(parsed.zod).not.toMatch(
/zod\.number\(\)[^,\]]*\.(?:stringFormat|regex)\(/,
/zod\.int\(\)[^,\]]*\.(?:stringFormat|regex)\(/,
);
expect(parsed.zod.match(/\.stringFormat\(/g) ?? []).toHaveLength(1);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const errorCodeMax = 600;

export const Error = zod.object({
code: zod
.number()
.int()
.min(errorCodeMin)
.max(errorCodeMax)
.describe('HTTP-like error code'),
Expand Down
Loading
Loading