Repository: samchungy/zod-openapi
Pull request #325 (2024-09-24 04:42:27)

Introduce createSchema
Allows users to be able to create individual schemas without needing to scaffold the whole `createDocument` function.

This is useful for third parties who want to scaffold the routes section by themselves but utilise this library's generation capabilities.


Patch:
diff --git a/README.md b/README.md
index ead93c3..6e5a808 100644
--- a/README.md
+++ b/README.md
@@ -209,6 +209,74 @@ const document = createDocument(details, {
 });
 ```
 
+### `createSchema`
+
+Creates an OpenAPI Schema Object along with any registered components. OpenAPI 3.1.0 Schema Objects are fully compatible with JSON Schema.
+
+```typescript
+import 'zod-openapi/extend';
+import { z } from 'zod';
+import { createSchema } from 'zod-openapi';
+
+const jobId = z.string().openapi({
+  description: 'A unique identifier for a job',
+  example: '12345',
+  ref: 'jobId',
+});
+
+const title = z.string().openapi({
+  description: 'Job title',
+  example: 'My job',
+});
+
+const job = z.object({
+  jobId,
+  title,
+});
+
+const { schema, components } = createSchema(job);
+```
+
+<details>
+  <summary>Creates the following object:</summary>
+  
+  ```json
+  {
+    "schema": {
+      "type": "object",
+      "properties": {
+        "jobId": {
+          "$ref": "#/components/schemas/jobId"
+        },
+        "title": {
+          "type": "string",
+          "description": "Job title",
+          "example": "My job"
+        }
+      },
+      "required": ["jobId", "title"]
+    },
+    "components": {
+      "jobId": {
+        "type": "string",
+        "description": "A unique identifier for a job",
+        "example": "12345"
+      }
+    }
+  }
+  ```
+</details>
+
+Additional options are available viathe optional `CreateSchemaOptions` parameter which can also take the same [CreateDocumentOptions](#createdocumentoptions).
+
+```typescript
+const { schema, components } = createSchema(job, {
+  schemaType: 'input'; // This controls whether this should be rendered as a request (`input`) or response (`output`). Defaults to `output`
+  openapi: '3.0.0'; // OpenAPI version to use, defaults to `'3.1.0'`
+  components: { jobId: z.string() } // Additional components to use and create while rendering the schema
+})
+```
+
 ### Request Parameters
 
 Query, Path, Header & Cookie parameters can be created using the `requestParams` key under the `method` key as follows:
diff --git a/src/create/components.ts b/src/create/components.ts
index 5531da9..bf3a816 100644
--- a/src/create/components.ts
+++ b/src/create/components.ts
@@ -445,7 +445,7 @@ export const createComponents = (
   return Object.keys(finalComponents).length ? finalComponents : undefined;
 };
 
-const createSchemaComponents = (
+export const createSchemaComponents = (
   componentsObject: ZodOpenApiComponentsObject,
   components: ComponentsObject,
   documentOptions?: CreateDocumentOptions,
diff --git a/src/create/schema/single.ts b/src/create/schema/single.ts
new file mode 100644
index 0000000..193b136
--- /dev/null
+++ b/src/create/schema/single.ts
@@ -0,0 +1,65 @@
+import type { ZodType } from 'zod';
+
+import type { OpenApiVersion } from '../../openapi';
+import type { oas30, oas31 } from '../../openapi3-ts/dist';
+import {
+  type CreationType,
+  createSchemaComponents,
+  getDefaultComponents,
+} from '../components';
+import type { CreateDocumentOptions } from '../document';
+
+import { type SchemaState, createSchema as internalCreateSchema } from '.';
+
+export interface SchemaResult {
+  schema: oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject;
+  components?:
+    | Record<
+        string,
+        oas30.SchemaObject | oas31.SchemaObject | oas31.ReferenceObject
+      >
+    | undefined;
+}
+
+export interface CreateSchemaOptions extends CreateDocumentOptions {
+  /**
+   * This controls whether this should be rendered as a request (`input`) or response (`output`). Defaults to `output`
+   */
+  schemaType?: CreationType;
+  /**
+   * OpenAPI version to use, defaults to `'3.1.0'`
+   */
+  openapi?: OpenApiVersion;
+  /**
+   * Additional components to use and create while rendering the schema
+   */
+  components?: Record<string, ZodType>;
+}
+
+export const createSchema = (
+  zodType: ZodType,
+  opts?: CreateSchemaOptions,
+): SchemaResult => {
+  const components = getDefaultComponents(
+    {
+      schemas: opts?.components,
+    },
+    opts?.openapi,
+  );
+  const state: SchemaState = {
+    components,
+    type: opts?.schemaType ?? 'output',
+    path: [],
+    visited: new Set(),
+    documentOptions: opts,
+  };
+
+  const schema = internalCreateSchema(zodType, state, ['createSchema']);
+
+  const schemaComponents = createSchemaComponents({}, components);
+
+  return {
+    schema,
+    components: schemaComponents,
+  };
+};
diff --git a/src/extendZodTypes.ts b/src/extendZodTypes.ts
index 1994943..12e0c94 100644
--- a/src/extendZodTypes.ts
+++ b/src/extendZodTypes.ts
@@ -24,7 +24,7 @@ interface ZodOpenApiMetadata<
    */
   ref?: string;
   /**
-   * Used when you are manually adding a Zod Schema to the components section. This controls whether this should be rendered as request (`input`) or response (`output`). Defaults to `output`
+   * Used when you are manually adding a Zod Schema to the components section. This controls whether this should be rendered as a request (`input`) or response (`output`). Defaults to `output`
    */
   refType?: CreationType;
   /**
diff --git a/src/index.ts b/src/index.ts
index 1de1b0b..ee7a397 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,4 @@
 export * from './create/document';
+export * from './create/schema/single';
 export * from './extendZod';
 export * from './openapi3-ts/dist';


Test patch:
diff --git a/src/create/schema/single.test.ts b/src/create/schema/single.test.ts
new file mode 100644
index 0000000..bae5893
--- /dev/null
+++ b/src/create/schema/single.test.ts
@@ -0,0 +1,61 @@
+import 'zod-openapi/extend';
+import { z } from 'zod';
+
+import { type SchemaResult, createSchema } from './single';
+
+describe('createSchema', () => {
+  it('should create a schema', () => {
+    const schema = createSchema(z.string().openapi({ description: 'foo' }));
+
+    expect(schema).toEqual<SchemaResult>({
+      schema: {
+        type: 'string',
+        description: 'foo',
+      },
+    });
+  });
+
+  it('should create a registered schema', () => {
+    const schema = createSchema(
+      z.string().openapi({ description: 'foo', ref: 'String' }),
+    );
+
+    expect(schema).toEqual<SchemaResult>({
+      schema: {
+        $ref: '#/components/schemas/String',
+      },
+      components: {
+        String: {
+          type: 'string',
+          description: 'foo',
+        },
+      },
+    });
+  });
+
+  it('should create components', () => {
+    const schema = createSchema(
+      z.object({
+        foo: z.string().openapi({ description: 'foo', ref: 'foo' }),
+      }),
+    );
+
+    expect(schema).toEqual<SchemaResult>({
+      schema: {
+        type: 'object',
+        properties: {
+          foo: {
+            $ref: '#/components/schemas/foo',
+          },
+        },
+        required: ['foo'],
+      },
+      components: {
+        foo: {
+          type: 'string',
+          description: 'foo',
+        },
+      },
+    });
+  });
+});