From f7dbdd4980b724dab659596c406c384096eee0d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:36:59 +0000 Subject: [PATCH 1/5] Initial plan From f786cb4f240c538a9bc420c39b5c2465316295f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 18:01:36 +0000 Subject: [PATCH 2/5] Fix allOf being generated as union instead of intersection in Zod plugins Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../openapi-ts/src/plugins/zod/mini/plugin.ts | 69 +++++++++++++------ .../openapi-ts/src/plugins/zod/v4/plugin.ts | 69 +++++++++++++------ 2 files changed, 98 insertions(+), 40 deletions(-) diff --git a/packages/openapi-ts/src/plugins/zod/mini/plugin.ts b/packages/openapi-ts/src/plugins/zod/mini/plugin.ts index d6f6478b1..4c5f506ec 100644 --- a/packages/openapi-ts/src/plugins/zod/mini/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/mini/plugin.ts @@ -69,31 +69,60 @@ const arrayTypeToZodSchema = ({ }); } else { if (schema.logicalOperator === 'and') { - // TODO: parser - handle intersection - // return tsc.typeArrayNode( - // tsc.typeIntersectionNode({ types: itemExpressions }), - // ); - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }), - parameters: [ - tsc.callExpression({ + const firstSchema = schema.items![0]!; + // we want to add an intersection, but not every schema can use the same API. + // if the first item contains another array or not an object, we cannot use + // `.intersection()` as that does not exist on `.union()` and non-object schemas. + let intersectionExpression: ts.Expression; + if ( + firstSchema.logicalOperator === 'or' || + (firstSchema.type && firstSchema.type !== 'object') + ) { + intersectionExpression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ expression: zSymbol.placeholder, - name: identifiers.union, + name: identifiers.intersection, }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemExpressions, + parameters: itemExpressions, + }); + } else { + intersectionExpression = itemExpressions[0]!; + for (let i = 1; i < itemExpressions.length; i++) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: intersectionExpression, + name: identifiers.intersection, }), - ], + parameters: [intersectionExpression, itemExpressions[i]!], + }); + } + } + + result.expression = tsc.callExpression({ + functionName, + parameters: [intersectionExpression], + }); + } else { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: zSymbol.placeholder, + name: identifiers.array, }), - ], - }); + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: zSymbol.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemExpressions, + }), + ], + }), + ], + }); + } } } diff --git a/packages/openapi-ts/src/plugins/zod/v4/plugin.ts b/packages/openapi-ts/src/plugins/zod/v4/plugin.ts index dba3157c1..27ce9fe2b 100644 --- a/packages/openapi-ts/src/plugins/zod/v4/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/v4/plugin.ts @@ -69,31 +69,60 @@ const arrayTypeToZodSchema = ({ }); } else { if (schema.logicalOperator === 'and') { - // TODO: parser - handle intersection - // return tsc.typeArrayNode( - // tsc.typeIntersectionNode({ types: itemExpressions }), - // ); - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }), - parameters: [ - tsc.callExpression({ + const firstSchema = schema.items![0]!; + // we want to add an intersection, but not every schema can use the same API. + // if the first item contains another array or not an object, we cannot use + // `.and()` as that does not exist on `.union()` and non-object schemas. + let intersectionExpression: ts.Expression; + if ( + firstSchema.logicalOperator === 'or' || + (firstSchema.type && firstSchema.type !== 'object') + ) { + intersectionExpression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ expression: zSymbol.placeholder, - name: identifiers.union, + name: identifiers.intersection, }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemExpressions, + parameters: itemExpressions, + }); + } else { + intersectionExpression = itemExpressions[0]!; + for (let i = 1; i < itemExpressions.length; i++) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: intersectionExpression, + name: identifiers.and, }), - ], + parameters: [itemExpressions[i]!], + }); + } + } + + result.expression = tsc.callExpression({ + functionName, + parameters: [intersectionExpression], + }); + } else { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: zSymbol.placeholder, + name: identifiers.array, }), - ], - }); + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: zSymbol.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemExpressions, + }), + ], + }), + ], + }); + } } } From 2b414dccc589e48df591ee488bde7d3490e7b7e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 18:53:05 +0000 Subject: [PATCH 3/5] Add comprehensive snapshot tests for allOf in array items across all Zod versions Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../specs/2.0.x/array-items-all-of.json | 75 +++++++++++++++++++ .../specs/3.0.x/array-items-all-of.yaml | 44 +++++++++++ .../specs/3.1.x/array-items-all-of.yaml | 44 +++++++++++ .../2.0.x/mini/array-items-all-of/zod.gen.ts | 20 +++++ .../2.0.x/v3/array-items-all-of/zod.gen.ts | 29 +++++++ .../2.0.x/v4/array-items-all-of/zod.gen.ts | 20 +++++ .../3.0.x/mini/array-items-all-of/zod.gen.ts | 20 +++++ .../3.0.x/v3/array-items-all-of/zod.gen.ts | 29 +++++++ .../3.0.x/v4/array-items-all-of/zod.gen.ts | 20 +++++ .../3.1.x/mini/array-items-all-of/zod.gen.ts | 20 +++++ .../3.1.x/v3/array-items-all-of/zod.gen.ts | 29 +++++++ .../3.1.x/v4/array-items-all-of/zod.gen.ts | 20 +++++ .../zod/v3/test/3.0.x.test.ts | 8 ++ .../zod/v3/test/3.1.x.test.ts | 8 ++ .../zod/v3/test/openapi.test.ts | 9 +++ .../2.0.x/mini/array-items-all-of/zod.gen.ts | 20 +++++ .../2.0.x/v3/array-items-all-of/zod.gen.ts | 29 +++++++ .../2.0.x/v4/array-items-all-of/zod.gen.ts | 20 +++++ .../3.0.x/mini/array-items-all-of/zod.gen.ts | 20 +++++ .../3.0.x/v3/array-items-all-of/zod.gen.ts | 29 +++++++ .../3.0.x/v4/array-items-all-of/zod.gen.ts | 20 +++++ .../3.1.x/mini/array-items-all-of/zod.gen.ts | 20 +++++ .../3.1.x/v3/array-items-all-of/zod.gen.ts | 29 +++++++ .../3.1.x/v4/array-items-all-of/zod.gen.ts | 20 +++++ .../zod/v4/test/3.0.x.test.ts | 8 ++ .../zod/v4/test/3.1.x.test.ts | 8 ++ .../zod/v4/test/openapi.test.ts | 9 +++ .../openapi-ts/src/plugins/zod/mini/plugin.ts | 2 +- 28 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.json create mode 100644 packages/openapi-ts-tests/specs/3.0.x/array-items-all-of.yaml create mode 100644 packages/openapi-ts-tests/specs/3.1.x/array-items-all-of.yaml create mode 100644 packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts create mode 100644 packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/array-items-all-of/zod.gen.ts diff --git a/packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.json b/packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.json new file mode 100644 index 000000000..41d05865a --- /dev/null +++ b/packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.json @@ -0,0 +1,75 @@ +{ + "swagger": "2.0", + "info": { + "title": "OpenAPI 2.0 array items allOf example", + "version": "1" + }, + "definitions": { + "ArrayWithAllOfObjects": { + "type": "array", + "items": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + } + ] + } + }, + "ArrayWithAllOfPrimitives": { + "type": "array", + "items": { + "allOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + }, + "ArrayWithAllOfRefs": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/BaseModel" + }, + { + "type": "object", + "properties": { + "extra": { + "type": "string" + } + } + } + ] + } + }, + "BaseModel": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + } + } +} diff --git a/packages/openapi-ts-tests/specs/3.0.x/array-items-all-of.yaml b/packages/openapi-ts-tests/specs/3.0.x/array-items-all-of.yaml new file mode 100644 index 000000000..ce970fd19 --- /dev/null +++ b/packages/openapi-ts-tests/specs/3.0.x/array-items-all-of.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.2 +info: + title: OpenAPI 3.0.2 array items allOf example + version: '1' +components: + schemas: + # Test case 1: Array with allOf of object schemas + ArrayWithAllOfObjects: + type: array + items: + allOf: + - type: object + properties: + id: + type: integer + - type: object + properties: + name: + type: string + # Test case 2: Array with allOf of primitives + ArrayWithAllOfPrimitives: + type: array + items: + allOf: + - type: number + - type: string + # Test case 3: Array with allOf including refs + ArrayWithAllOfRefs: + type: array + items: + allOf: + - $ref: '#/components/schemas/BaseModel' + - type: object + properties: + extra: + type: string + BaseModel: + type: object + properties: + id: + type: integer + createdAt: + type: string + format: date-time diff --git a/packages/openapi-ts-tests/specs/3.1.x/array-items-all-of.yaml b/packages/openapi-ts-tests/specs/3.1.x/array-items-all-of.yaml new file mode 100644 index 000000000..fc70640fd --- /dev/null +++ b/packages/openapi-ts-tests/specs/3.1.x/array-items-all-of.yaml @@ -0,0 +1,44 @@ +openapi: 3.1.0 +info: + title: OpenAPI 3.1.0 array items allOf example + version: '1' +components: + schemas: + # Test case 1: Array with allOf of object schemas + ArrayWithAllOfObjects: + type: array + items: + allOf: + - type: object + properties: + id: + type: integer + - type: object + properties: + name: + type: string + # Test case 2: Array with allOf of primitives + ArrayWithAllOfPrimitives: + type: array + items: + allOf: + - type: number + - type: string + # Test case 3: Array with allOf including refs + ArrayWithAllOfRefs: + type: array + items: + allOf: + - $ref: '#/components/schemas/BaseModel' + - type: object + properties: + extra: + type: string + BaseModel: + type: object + properties: + id: + type: integer + createdAt: + type: string + format: date-time diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..ef475d167 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/v4-mini'; + +export const zArrayWithAllOfObjects = z.array(z.intersection(z.object({ + id: z.optional(z.int()) +}), z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(z.intersection(zBaseModel, z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..0acb4b674 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts @@ -0,0 +1,29 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zArrayWithAllOfObjects = z.array(z.union([ + z.object({ + id: z.number().int().optional() + }), + z.object({ + name: z.string().optional() + }) +])); + +export const zArrayWithAllOfPrimitives = z.array(z.union([ + z.number(), + z.string() +])); + +export const zBaseModel = z.object({ + id: z.number().int().optional(), + createdAt: z.string().datetime().optional() +}); + +export const zArrayWithAllOfRefs = z.array(z.union([ + zBaseModel, + z.object({ + extra: z.string().optional() + }) +])); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..87e0c90b2 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v4'; + +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.optional(z.int()) +}).and(z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..ef475d167 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/v4-mini'; + +export const zArrayWithAllOfObjects = z.array(z.intersection(z.object({ + id: z.optional(z.int()) +}), z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(z.intersection(zBaseModel, z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..0acb4b674 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts @@ -0,0 +1,29 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zArrayWithAllOfObjects = z.array(z.union([ + z.object({ + id: z.number().int().optional() + }), + z.object({ + name: z.string().optional() + }) +])); + +export const zArrayWithAllOfPrimitives = z.array(z.union([ + z.number(), + z.string() +])); + +export const zBaseModel = z.object({ + id: z.number().int().optional(), + createdAt: z.string().datetime().optional() +}); + +export const zArrayWithAllOfRefs = z.array(z.union([ + zBaseModel, + z.object({ + extra: z.string().optional() + }) +])); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..87e0c90b2 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v4'; + +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.optional(z.int()) +}).and(z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..ef475d167 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/v4-mini'; + +export const zArrayWithAllOfObjects = z.array(z.intersection(z.object({ + id: z.optional(z.int()) +}), z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(z.intersection(zBaseModel, z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..0acb4b674 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts @@ -0,0 +1,29 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zArrayWithAllOfObjects = z.array(z.union([ + z.object({ + id: z.number().int().optional() + }), + z.object({ + name: z.string().optional() + }) +])); + +export const zArrayWithAllOfPrimitives = z.array(z.union([ + z.number(), + z.string() +])); + +export const zBaseModel = z.object({ + id: z.number().int().optional(), + createdAt: z.string().datetime().optional() +}); + +export const zArrayWithAllOfRefs = z.array(z.union([ + zBaseModel, + z.object({ + extra: z.string().optional() + }) +])); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..87e0c90b2 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v4'; + +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.optional(z.int()) +}).and(z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts b/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts index 3e255b3bd..ac6504ba9 100644 --- a/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts @@ -34,6 +34,14 @@ for (const zodVersion of zodVersions) { }); const scenarios = [ + { + config: createConfig({ + input: 'array-items-all-of.yaml', + output: 'array-items-all-of', + }), + description: + 'generates correct array when items use allOf (intersection)', + }, { config: createConfig({ input: 'array-items-one-of-length-1.yaml', diff --git a/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts b/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts index c8d0d0b1c..266cc913c 100644 --- a/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts @@ -34,6 +34,14 @@ for (const zodVersion of zodVersions) { }); const scenarios = [ + { + config: createConfig({ + input: 'array-items-all-of.yaml', + output: 'array-items-all-of', + }), + description: + 'generates correct array when items use allOf (intersection)', + }, { config: createConfig({ input: 'array-items-one-of-length-1.yaml', diff --git a/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts b/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts index 5605ecc63..4351f1cfc 100644 --- a/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts +++ b/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts @@ -35,6 +35,15 @@ for (const version of versions) { }); const scenarios = [ + { + config: createConfig({ + input: + 'array-items-all-of.' + (version === '2.0.x' ? 'json' : 'yaml'), + output: 'array-items-all-of', + }), + description: + 'generates correct array when items use allOf (intersection)', + }, { config: createConfig({ input: 'full.yaml', diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..95f43fa31 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +export const zArrayWithAllOfObjects = z.array(z.intersection(z.object({ + id: z.optional(z.int()) +}), z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(z.intersection(zBaseModel, z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..c5e624333 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts @@ -0,0 +1,29 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +export const zArrayWithAllOfObjects = z.array(z.union([ + z.object({ + id: z.number().int().optional() + }), + z.object({ + name: z.string().optional() + }) +])); + +export const zArrayWithAllOfPrimitives = z.array(z.union([ + z.number(), + z.string() +])); + +export const zBaseModel = z.object({ + id: z.number().int().optional(), + createdAt: z.string().datetime().optional() +}); + +export const zArrayWithAllOfRefs = z.array(z.union([ + zBaseModel, + z.object({ + extra: z.string().optional() + }) +])); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..a17f5ae50 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.optional(z.int()) +}).and(z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..95f43fa31 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +export const zArrayWithAllOfObjects = z.array(z.intersection(z.object({ + id: z.optional(z.int()) +}), z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(z.intersection(zBaseModel, z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..c5e624333 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts @@ -0,0 +1,29 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +export const zArrayWithAllOfObjects = z.array(z.union([ + z.object({ + id: z.number().int().optional() + }), + z.object({ + name: z.string().optional() + }) +])); + +export const zArrayWithAllOfPrimitives = z.array(z.union([ + z.number(), + z.string() +])); + +export const zBaseModel = z.object({ + id: z.number().int().optional(), + createdAt: z.string().datetime().optional() +}); + +export const zArrayWithAllOfRefs = z.array(z.union([ + zBaseModel, + z.object({ + extra: z.string().optional() + }) +])); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..a17f5ae50 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.optional(z.int()) +}).and(z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..95f43fa31 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import * as z from 'zod/mini'; + +export const zArrayWithAllOfObjects = z.array(z.intersection(z.object({ + id: z.optional(z.int()) +}), z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(z.intersection(zBaseModel, z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..c5e624333 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts @@ -0,0 +1,29 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod/v3'; + +export const zArrayWithAllOfObjects = z.array(z.union([ + z.object({ + id: z.number().int().optional() + }), + z.object({ + name: z.string().optional() + }) +])); + +export const zArrayWithAllOfPrimitives = z.array(z.union([ + z.number(), + z.string() +])); + +export const zBaseModel = z.object({ + id: z.number().int().optional(), + createdAt: z.string().datetime().optional() +}); + +export const zArrayWithAllOfRefs = z.array(z.union([ + zBaseModel, + z.object({ + extra: z.string().optional() + }) +])); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/array-items-all-of/zod.gen.ts new file mode 100644 index 000000000..a17f5ae50 --- /dev/null +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/array-items-all-of/zod.gen.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { z } from 'zod'; + +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.optional(z.int()) +}).and(z.object({ + name: z.optional(z.string()) +}))); + +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); + +export const zBaseModel = z.object({ + id: z.optional(z.int()), + createdAt: z.optional(z.iso.datetime()) +}); + +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.optional(z.string()) +}))); diff --git a/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts b/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts index 3e255b3bd..ac6504ba9 100644 --- a/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts @@ -34,6 +34,14 @@ for (const zodVersion of zodVersions) { }); const scenarios = [ + { + config: createConfig({ + input: 'array-items-all-of.yaml', + output: 'array-items-all-of', + }), + description: + 'generates correct array when items use allOf (intersection)', + }, { config: createConfig({ input: 'array-items-one-of-length-1.yaml', diff --git a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts index c8d0d0b1c..266cc913c 100644 --- a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts @@ -34,6 +34,14 @@ for (const zodVersion of zodVersions) { }); const scenarios = [ + { + config: createConfig({ + input: 'array-items-all-of.yaml', + output: 'array-items-all-of', + }), + description: + 'generates correct array when items use allOf (intersection)', + }, { config: createConfig({ input: 'array-items-one-of-length-1.yaml', diff --git a/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts b/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts index 5605ecc63..4351f1cfc 100644 --- a/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts @@ -35,6 +35,15 @@ for (const version of versions) { }); const scenarios = [ + { + config: createConfig({ + input: + 'array-items-all-of.' + (version === '2.0.x' ? 'json' : 'yaml'), + output: 'array-items-all-of', + }), + description: + 'generates correct array when items use allOf (intersection)', + }, { config: createConfig({ input: 'full.yaml', diff --git a/packages/openapi-ts/src/plugins/zod/mini/plugin.ts b/packages/openapi-ts/src/plugins/zod/mini/plugin.ts index 4c5f506ec..4c03374eb 100644 --- a/packages/openapi-ts/src/plugins/zod/mini/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/mini/plugin.ts @@ -90,7 +90,7 @@ const arrayTypeToZodSchema = ({ for (let i = 1; i < itemExpressions.length; i++) { intersectionExpression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: intersectionExpression, + expression: zSymbol.placeholder, name: identifiers.intersection, }), parameters: [intersectionExpression, itemExpressions[i]!], From 7e7811863c44bcf707edfd978ad807d27907a61a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 23:28:30 +0000 Subject: [PATCH 4/5] Address review feedback: fix v3 plugin, remove duplicate tests, use YAML for all specs Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .../specs/2.0.x/array-items-all-of.json | 75 ------------------- .../specs/2.0.x/array-items-all-of.yaml | 40 ++++++++++ .../2.0.x/v3/array-items-all-of/zod.gen.ts | 27 +++---- .../3.0.x/v3/array-items-all-of/zod.gen.ts | 27 +++---- .../3.1.x/v3/array-items-all-of/zod.gen.ts | 27 +++---- .../zod/v3/test/3.0.x.test.ts | 8 -- .../zod/v3/test/3.1.x.test.ts | 8 -- .../zod/v3/test/openapi.test.ts | 3 +- .../2.0.x/v3/array-items-all-of/zod.gen.ts | 27 +++---- .../3.0.x/v3/array-items-all-of/zod.gen.ts | 27 +++---- .../3.1.x/v3/array-items-all-of/zod.gen.ts | 27 +++---- .../zod/v4/test/3.0.x.test.ts | 8 -- .../zod/v4/test/3.1.x.test.ts | 8 -- .../zod/v4/test/openapi.test.ts | 3 +- .../openapi-ts/src/plugins/zod/v3/plugin.ts | 69 ++++++++++++----- 15 files changed, 145 insertions(+), 239 deletions(-) delete mode 100644 packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.json create mode 100644 packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.yaml diff --git a/packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.json b/packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.json deleted file mode 100644 index 41d05865a..000000000 --- a/packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "title": "OpenAPI 2.0 array items allOf example", - "version": "1" - }, - "definitions": { - "ArrayWithAllOfObjects": { - "type": "array", - "items": { - "allOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "integer" - } - } - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - } - } - } - ] - } - }, - "ArrayWithAllOfPrimitives": { - "type": "array", - "items": { - "allOf": [ - { - "type": "number" - }, - { - "type": "string" - } - ] - } - }, - "ArrayWithAllOfRefs": { - "type": "array", - "items": { - "allOf": [ - { - "$ref": "#/definitions/BaseModel" - }, - { - "type": "object", - "properties": { - "extra": { - "type": "string" - } - } - } - ] - } - }, - "BaseModel": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - } - } -} diff --git a/packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.yaml b/packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.yaml new file mode 100644 index 000000000..ae2086355 --- /dev/null +++ b/packages/openapi-ts-tests/specs/2.0.x/array-items-all-of.yaml @@ -0,0 +1,40 @@ +swagger: '2.0' +info: + title: OpenAPI 2.0 array items allOf example + version: '1' +definitions: + ArrayWithAllOfObjects: + type: array + items: + allOf: + - type: object + properties: + id: + type: integer + - type: object + properties: + name: + type: string + ArrayWithAllOfPrimitives: + type: array + items: + allOf: + - type: number + - type: string + ArrayWithAllOfRefs: + type: array + items: + allOf: + - $ref: '#/definitions/BaseModel' + - type: object + properties: + extra: + type: string + BaseModel: + type: object + properties: + id: + type: integer + createdAt: + type: string + format: date-time diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts index 0acb4b674..c4af44ea7 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts @@ -2,28 +2,19 @@ import { z } from 'zod'; -export const zArrayWithAllOfObjects = z.array(z.union([ - z.object({ - id: z.number().int().optional() - }), - z.object({ - name: z.string().optional() - }) -])); +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.number().int().optional() +}).and(z.object({ + name: z.string().optional() +}))); -export const zArrayWithAllOfPrimitives = z.array(z.union([ - z.number(), - z.string() -])); +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); export const zBaseModel = z.object({ id: z.number().int().optional(), createdAt: z.string().datetime().optional() }); -export const zArrayWithAllOfRefs = z.array(z.union([ - zBaseModel, - z.object({ - extra: z.string().optional() - }) -])); +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.string().optional() +}))); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts index 0acb4b674..c4af44ea7 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts @@ -2,28 +2,19 @@ import { z } from 'zod'; -export const zArrayWithAllOfObjects = z.array(z.union([ - z.object({ - id: z.number().int().optional() - }), - z.object({ - name: z.string().optional() - }) -])); +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.number().int().optional() +}).and(z.object({ + name: z.string().optional() +}))); -export const zArrayWithAllOfPrimitives = z.array(z.union([ - z.number(), - z.string() -])); +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); export const zBaseModel = z.object({ id: z.number().int().optional(), createdAt: z.string().datetime().optional() }); -export const zArrayWithAllOfRefs = z.array(z.union([ - zBaseModel, - z.object({ - extra: z.string().optional() - }) -])); +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.string().optional() +}))); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts index 0acb4b674..c4af44ea7 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts @@ -2,28 +2,19 @@ import { z } from 'zod'; -export const zArrayWithAllOfObjects = z.array(z.union([ - z.object({ - id: z.number().int().optional() - }), - z.object({ - name: z.string().optional() - }) -])); +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.number().int().optional() +}).and(z.object({ + name: z.string().optional() +}))); -export const zArrayWithAllOfPrimitives = z.array(z.union([ - z.number(), - z.string() -])); +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); export const zBaseModel = z.object({ id: z.number().int().optional(), createdAt: z.string().datetime().optional() }); -export const zArrayWithAllOfRefs = z.array(z.union([ - zBaseModel, - z.object({ - extra: z.string().optional() - }) -])); +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.string().optional() +}))); diff --git a/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts b/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts index ac6504ba9..3e255b3bd 100644 --- a/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts @@ -34,14 +34,6 @@ for (const zodVersion of zodVersions) { }); const scenarios = [ - { - config: createConfig({ - input: 'array-items-all-of.yaml', - output: 'array-items-all-of', - }), - description: - 'generates correct array when items use allOf (intersection)', - }, { config: createConfig({ input: 'array-items-one-of-length-1.yaml', diff --git a/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts b/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts index 266cc913c..c8d0d0b1c 100644 --- a/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts @@ -34,14 +34,6 @@ for (const zodVersion of zodVersions) { }); const scenarios = [ - { - config: createConfig({ - input: 'array-items-all-of.yaml', - output: 'array-items-all-of', - }), - description: - 'generates correct array when items use allOf (intersection)', - }, { config: createConfig({ input: 'array-items-one-of-length-1.yaml', diff --git a/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts b/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts index 4351f1cfc..e15c35ae4 100644 --- a/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts +++ b/packages/openapi-ts-tests/zod/v3/test/openapi.test.ts @@ -37,8 +37,7 @@ for (const version of versions) { const scenarios = [ { config: createConfig({ - input: - 'array-items-all-of.' + (version === '2.0.x' ? 'json' : 'yaml'), + input: 'array-items-all-of.yaml', output: 'array-items-all-of', }), description: diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts index c5e624333..b5f786712 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/array-items-all-of/zod.gen.ts @@ -2,28 +2,19 @@ import { z } from 'zod/v3'; -export const zArrayWithAllOfObjects = z.array(z.union([ - z.object({ - id: z.number().int().optional() - }), - z.object({ - name: z.string().optional() - }) -])); +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.number().int().optional() +}).and(z.object({ + name: z.string().optional() +}))); -export const zArrayWithAllOfPrimitives = z.array(z.union([ - z.number(), - z.string() -])); +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); export const zBaseModel = z.object({ id: z.number().int().optional(), createdAt: z.string().datetime().optional() }); -export const zArrayWithAllOfRefs = z.array(z.union([ - zBaseModel, - z.object({ - extra: z.string().optional() - }) -])); +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.string().optional() +}))); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts index c5e624333..b5f786712 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/array-items-all-of/zod.gen.ts @@ -2,28 +2,19 @@ import { z } from 'zod/v3'; -export const zArrayWithAllOfObjects = z.array(z.union([ - z.object({ - id: z.number().int().optional() - }), - z.object({ - name: z.string().optional() - }) -])); +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.number().int().optional() +}).and(z.object({ + name: z.string().optional() +}))); -export const zArrayWithAllOfPrimitives = z.array(z.union([ - z.number(), - z.string() -])); +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); export const zBaseModel = z.object({ id: z.number().int().optional(), createdAt: z.string().datetime().optional() }); -export const zArrayWithAllOfRefs = z.array(z.union([ - zBaseModel, - z.object({ - extra: z.string().optional() - }) -])); +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.string().optional() +}))); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts index c5e624333..b5f786712 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/array-items-all-of/zod.gen.ts @@ -2,28 +2,19 @@ import { z } from 'zod/v3'; -export const zArrayWithAllOfObjects = z.array(z.union([ - z.object({ - id: z.number().int().optional() - }), - z.object({ - name: z.string().optional() - }) -])); +export const zArrayWithAllOfObjects = z.array(z.object({ + id: z.number().int().optional() +}).and(z.object({ + name: z.string().optional() +}))); -export const zArrayWithAllOfPrimitives = z.array(z.union([ - z.number(), - z.string() -])); +export const zArrayWithAllOfPrimitives = z.array(z.intersection(z.number(), z.string())); export const zBaseModel = z.object({ id: z.number().int().optional(), createdAt: z.string().datetime().optional() }); -export const zArrayWithAllOfRefs = z.array(z.union([ - zBaseModel, - z.object({ - extra: z.string().optional() - }) -])); +export const zArrayWithAllOfRefs = z.array(zBaseModel.and(z.object({ + extra: z.string().optional() +}))); diff --git a/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts b/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts index ac6504ba9..3e255b3bd 100644 --- a/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts @@ -34,14 +34,6 @@ for (const zodVersion of zodVersions) { }); const scenarios = [ - { - config: createConfig({ - input: 'array-items-all-of.yaml', - output: 'array-items-all-of', - }), - description: - 'generates correct array when items use allOf (intersection)', - }, { config: createConfig({ input: 'array-items-one-of-length-1.yaml', diff --git a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts index 266cc913c..c8d0d0b1c 100644 --- a/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts @@ -34,14 +34,6 @@ for (const zodVersion of zodVersions) { }); const scenarios = [ - { - config: createConfig({ - input: 'array-items-all-of.yaml', - output: 'array-items-all-of', - }), - description: - 'generates correct array when items use allOf (intersection)', - }, { config: createConfig({ input: 'array-items-one-of-length-1.yaml', diff --git a/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts b/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts index 4351f1cfc..e15c35ae4 100644 --- a/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts +++ b/packages/openapi-ts-tests/zod/v4/test/openapi.test.ts @@ -37,8 +37,7 @@ for (const version of versions) { const scenarios = [ { config: createConfig({ - input: - 'array-items-all-of.' + (version === '2.0.x' ? 'json' : 'yaml'), + input: 'array-items-all-of.yaml', output: 'array-items-all-of', }), description: diff --git a/packages/openapi-ts/src/plugins/zod/v3/plugin.ts b/packages/openapi-ts/src/plugins/zod/v3/plugin.ts index feeb1fb5d..ab7fc5535 100644 --- a/packages/openapi-ts/src/plugins/zod/v3/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/v3/plugin.ts @@ -72,31 +72,60 @@ const arrayTypeToZodSchema = ({ }); } else { if (schema.logicalOperator === 'and') { - // TODO: parser - handle intersection - // return tsc.typeArrayNode( - // tsc.typeIntersectionNode({ types: itemExpressions }), - // ); - } - - arrayExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }), - parameters: [ - tsc.callExpression({ + const firstSchema = schema.items![0]!; + // we want to add an intersection, but not every schema can use the same API. + // if the first item contains another array or not an object, we cannot use + // `.and()` as that does not exist on `.union()` and non-object schemas. + let intersectionExpression: ts.Expression; + if ( + firstSchema.logicalOperator === 'or' || + (firstSchema.type && firstSchema.type !== 'object') + ) { + intersectionExpression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ expression: zSymbol.placeholder, - name: identifiers.union, + name: identifiers.intersection, }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemExpressions, + parameters: itemExpressions, + }); + } else { + intersectionExpression = itemExpressions[0]!; + for (let i = 1; i < itemExpressions.length; i++) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: intersectionExpression, + name: identifiers.and, }), - ], + parameters: [itemExpressions[i]!], + }); + } + } + + arrayExpression = tsc.callExpression({ + functionName, + parameters: [intersectionExpression], + }); + } else { + arrayExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: zSymbol.placeholder, + name: identifiers.array, }), - ], - }); + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: zSymbol.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemExpressions, + }), + ], + }), + ], + }); + } } } From 078face47b40462033ce557ce0048ccf88246854 Mon Sep 17 00:00:00 2001 From: Lubos Date: Tue, 7 Oct 2025 08:55:47 +0800 Subject: [PATCH 5/5] Fix allOf generation in Zod plugins Fixes the generation of allOf in array items for Zod plugins. --- .changeset/mighty-paws-design.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mighty-paws-design.md diff --git a/.changeset/mighty-paws-design.md b/.changeset/mighty-paws-design.md new file mode 100644 index 000000000..84d22f643 --- /dev/null +++ b/.changeset/mighty-paws-design.md @@ -0,0 +1,5 @@ +--- +"@hey-api/openapi-ts": patch +--- + +fix(zod): allOf in array items being generated as union instead of intersection