diff --git a/.changeset/ten-poems-appear.md b/.changeset/ten-poems-appear.md new file mode 100644 index 00000000..6e08c318 --- /dev/null +++ b/.changeset/ten-poems-appear.md @@ -0,0 +1,5 @@ +--- +"openapi-zod-client": minor +--- + +Add `schemaRefiner` option to allow refining the OpenAPI schema before its converted to a Zod schema diff --git a/lib/src/openApiToZod.ts b/lib/src/openApiToZod.ts index 88d505b6..2bd35d25 100644 --- a/lib/src/openApiToZod.ts +++ b/lib/src/openApiToZod.ts @@ -20,10 +20,12 @@ type ConversionArgs = { * @see https://github.com/colinhacks/zod */ // eslint-disable-next-line sonarjs/cognitive-complexity -export function getZodSchema({ schema, ctx, meta: inheritedMeta, options }: ConversionArgs): CodeMeta { - if (!schema) { +export function getZodSchema({ schema: $schema, ctx, meta: inheritedMeta, options }: ConversionArgs): CodeMeta { + if (!$schema) { throw new Error("Schema is required"); } + + const schema = options?.schemaRefiner?.($schema, inheritedMeta) ?? $schema; const code = new CodeMeta(schema, ctx, inheritedMeta); const meta = { parent: code.inherit(inheritedMeta?.parent), diff --git a/lib/src/template-context.ts b/lib/src/template-context.ts index 03f34d14..aced2cbd 100644 --- a/lib/src/template-context.ts +++ b/lib/src/template-context.ts @@ -1,4 +1,4 @@ -import type { OpenAPIObject, OperationObject, PathItemObject, SchemaObject } from "openapi3-ts"; +import type { OpenAPIObject, OperationObject, PathItemObject, ReferenceObject, SchemaObject } from "openapi3-ts"; import { sortBy, sortListFromRefArray, sortObjKeysFromArray } from "pastable/server"; import { ts } from "tanu"; import { match } from "ts-pattern"; @@ -11,6 +11,7 @@ import { getTypescriptFromOpenApi } from "./openApiToTypescript"; import { getZodSchema } from "./openApiToZod"; import { topologicalSort } from "./topologicalSort"; import { asComponentSchema, normalizeString } from "./utils"; +import type { CodeMetaData } from "./CodeMeta"; const file = ts.createSourceFile("", "", ts.ScriptTarget.ESNext, true); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); @@ -403,4 +404,9 @@ export type TemplateContextOptions = { * If 2 schemas have the same name but different types, export subsequent names with numbers appended */ exportAllNamedSchemas?: boolean; + + /** + * A function that runs in the schema conversion process to refine the schema before it's converted to a Zod schema. + */ + schemaRefiner?: (schema: T, parentMeta?: CodeMetaData) => T; }; diff --git a/lib/tests/schema-refiner.test.ts b/lib/tests/schema-refiner.test.ts new file mode 100644 index 00000000..79b89b42 --- /dev/null +++ b/lib/tests/schema-refiner.test.ts @@ -0,0 +1,41 @@ +import { isReferenceObject } from "openapi3-ts"; +import { getZodSchema } from "../src/openApiToZod"; +import { test, expect } from "vitest"; + +test("schema-refiner", () => { + expect( + getZodSchema({ + schema: { + properties: { + name: { + type: "string", + }, + email: { + type: "string", + }, + }, + }, + options: { + schemaRefiner(schema) { + if (isReferenceObject(schema) || !schema.properties) { + return schema; + } + + if (!schema.required && schema.properties) { + for (const key in schema.properties) { + const prop = schema.properties[key]; + + if (!isReferenceObject(prop)) { + prop.nullable = true; + } + } + } + + return schema; + }, + }, + }) + ).toMatchInlineSnapshot( + '"z.object({ name: z.string().nullable(), email: z.string().nullable() }).partial().passthrough()"' + ); +});