Skip to content
Open
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
7 changes: 6 additions & 1 deletion lib/src/generateZodClientFromOpenAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3816,6 +3816,7 @@ test("with optional, partial, all required objects", async () => {
"emittedType": {
"Nested2": true,
"Root2": true,
"VeryDeeplyNested": true,
},
"endpoints": [
{
Expand Down Expand Up @@ -3916,7 +3917,11 @@ test("with optional, partial, all required objects", async () => {
requiredProp: string;
};

const VeryDeeplyNested = z.enum(["aaa", "bbb", "ccc"]);
const VeryDeeplyNested: z.ZodType<VeryDeeplyNested> = z.enum([
"aaa",
"bbb",
"ccc",
]);
const DeeplyNested = z.array(VeryDeeplyNested);
const PartialObject = z
.object({ something: z.string(), another: z.number() })
Expand Down
41 changes: 41 additions & 0 deletions lib/src/getOpenApiDependencyGraph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,44 @@ test("recursive relations along with some basics schemas", () => {
]
`);
});

test("union type array dependencies", () => {
const schemas = {
Parent: {
type: "object",
properties: {
children: {
type: ["array", "null"], // Union type with array - this was causing the bug
items: { $ref: "Child" }
}
}
},
Child: {
type: "object",
properties: {
name: { type: "string" }
}
}
} as Record<string, SchemaObject>;

const getSchemaByRef = (ref: string) => schemas[ref]!;
const { refsDependencyGraph, deepDependencyGraph } = getOpenApiDependencyGraph(
Object.keys(schemas),
getSchemaByRef
);

expect(refsDependencyGraph).toMatchInlineSnapshot(`
{
"Parent": Set {
"Child",
},
}
`);
expect(deepDependencyGraph).toMatchInlineSnapshot(`
{
"Parent": Set {
"Child",
},
}
`);
});
2 changes: 1 addition & 1 deletion lib/src/getOpenApiDependencyGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const getOpenApiDependencyGraph = (
return;
}

if (schema.type === "array") {
if (schema.type === "array" || (Array.isArray(schema.type) && schema.type.includes("array"))) {
if (!schema.items) return;
return void visit(schema.items, fromRef);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/src/template-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export const getZodClientTemplateContext = (
if (options?.shouldExportAllTypes && nodeSchema.type === "object") {
data.emittedType[depSchemaName] = true;
}
// Also set emittedType for enum dependencies of circular schemas to ensure proper type annotations
if (nodeSchema.type === "string" && nodeSchema.enum) {
data.emittedType[depSchemaName] = true;
}
}
}
}
Expand Down
69 changes: 69 additions & 0 deletions lib/tests/circular-dependency-missing-enum-refs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { SchemaObject, SchemasObject } from "openapi3-ts";
import { describe, expect, test } from "vitest";
import { generateZodClientFromOpenAPI } from "../src/generateZodClientFromOpenAPI";

describe("circular-dependency-missing-enum-refs", () => {
test("should generate enum dependencies with proper type annotations when circular schemas use z.lazy", async () => {
// - CircularA references CircularB (circular dependency)
// - CircularA also references enum Status which should get proper type annotation
const schemas: SchemasObject = {
CircularA: {
type: "object",
properties: {
id: { type: "string" },
status: { $ref: "#/components/schemas/Status" },
circularB: { $ref: "#/components/schemas/CircularB" }
}
} as SchemaObject,

CircularB: {
type: "object",
properties: {
id: { type: "string" },
circularA: { $ref: "#/components/schemas/CircularA" }
}
} as SchemaObject,

Status: {
type: "string",
enum: ["active", "inactive"]
}
};

const openApiDoc = {
openapi: "3.1.0",
info: { title: "Test API", version: "1.0.0" },
paths: {
"/test": {
get: {
operationId: "getTest",
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/CircularA" }
}
}
}
}
}
}
},
components: { schemas }
};

const output = await generateZodClientFromOpenAPI({
openApiDoc,
disableWriteToFile: true
});

// enum dependencies of circular schemas should get proper type annotations (: z.ZodType<...>):
expect(output).toContain("const Status: z.ZodType<Status> = z.enum");

// Circular schemas should also have type annotations, but that was already the case:
expect(output).toContain("const CircularA: z.ZodType<CircularA> = z.lazy");
expect(output).toContain("const CircularB: z.ZodType<CircularB> = z.lazy");
});
});

8 changes: 4 additions & 4 deletions lib/tests/enum-null.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ test("enum-null", async () => {
type Null3 = "a" | null;
type Null4 = null;

const Null1 = z.literal(null);
const Null2 = z.enum(["a", null]);
const Null3 = z.enum(["a", null]);
const Null4 = z.literal(null);
const Null1: z.ZodType<Null1> = z.literal(null);
const Null2: z.ZodType<Null2> = z.enum(["a", null]);
const Null3: z.ZodType<Null3> = z.enum(["a", null]);
const Null4: z.ZodType<Null4> = z.literal(null);
const Compound: z.ZodType<Compound> = z
.object({ field: z.union([Null1, Null2, Null3, Null4, z.string()]) })
.partial()
Expand Down