Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for all ZodEffects and ZodPipeline #9

Merged
merged 7 commits into from
Apr 17, 2023
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
6 changes: 4 additions & 2 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
name: Validate

on:
- pull_request
- push
pull_request:
push:
branches:
- 'master'
permissions: {}

jobs:
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,20 +298,26 @@ Wherever `title` is used in schemas across the document, it will instead be crea

This can be an extremely powerful way to generate better Open API documentation. There are some Open API features like [discriminator mapping](https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/) which require all schemas in the union to contain a ref.

To display components which are not referenced by simply add the Zod Schema to the schema components directly.
To display components which are not referenced in the responses or requests simply add the Zod Schema to the schema components directly.

eg.

```typescript
{
"components": {
"schemas": {
MyJobSchema // note: this will register this Zod Schema as MyJobSchema unless `ref` is specified on the type
MyJobSchema // note: this will register this Zod Schema as MyJobSchema unless `ref` in `openapi()` is specified on the type
}
}
}
```

##### Zod Effects

`.transform()` and `.preprocess()` are complicated because they are technically two types (input & output). This means that we need to understand which type you are after. This means if you are adding the ZodSchema directly to the `components` section, we need to know whether you want the response or request type created. You can do this by setting the `refType` field to `input` or `output` in `.openapi()`. This defaults to `output` by default.

If you use a registered schema with a ZodEffect in both a request and response schema you will receive an error because we cannot register two different schemas under the same `ref`.

#### Parameters

Query, Path, Header & Cookie parameters can be similarly registered:
Expand Down Expand Up @@ -356,7 +362,9 @@ const header = z.string().openapi({
- ZodDiscriminatedUnion
- `discriminator` mapping when all schemas in the union contain a `ref`.
- ZodEffects
- `pre-process` and `refine` support
- `transform` support for request schemas. Wrap your transform in a ZodPipeline to enable response schema creation or declare a manual `type` in the `.openapi()` section of that schema.
- `pre-process` support for response schemas. Wrap your transform in a ZodPipeline to enable request schema creation or declare a manual `type` in the `.openapi()` section of that schema.
- `refine` full support.
- ZodEnum
- ZodLiteral
- ZodNativeEnum
Expand All @@ -368,6 +376,7 @@ const header = z.string().openapi({
- `exclusiveMin`/`min`/`exclusiveMax`/`max` mapping for `.min()`, `.max()`, `lt()`, `gt()`
- ZodObject
- ZodOptional
- ZodPipeline
- ZodRecord
- ZodString
- `format` mapping for `.url()`, `.uuid()`, `.email()`, `.datetime()`
Expand Down
3 changes: 3 additions & 0 deletions src/create/components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ describe('getDefaultComponents', () => {
type: 'string',
},
zodSchema: aSchema,
types: ['input', 'output'],
},
b: {
schemaObject: {
Expand Down Expand Up @@ -149,6 +150,7 @@ describe('createComponents', () => {
type: 'string',
},
zodSchema: z.string().openapi({ ref: 'a' }),
types: ['output'],
},
},
headers: {
Expand Down Expand Up @@ -224,6 +226,7 @@ describe('createComponents', () => {
type: 'string',
},
zodSchema: z.string().openapi({ ref: 'a' }),
types: ['output'],
},
},
headers: {
Expand Down
19 changes: 14 additions & 5 deletions src/create/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@ import { oas30, oas31 } from 'openapi3-ts';
import { ZodType } from 'zod';

import { ZodOpenApiComponentsObject, ZodOpenApiVersion } from './document';
import { SchemaState } from './schema';
import { createSchemaWithMetadata } from './schema/metadata';

export interface Schema {
export type CreationType = 'input' | 'output';

export interface SchemaComponent {
zodSchema?: ZodType;
schemaObject:
| oas31.SchemaObject
| oas31.ReferenceObject
| oas30.SchemaObject
| oas30.ReferenceObject;
types?: [CreationType, ...CreationType[]];
}

interface SchemaComponentObject {
[ref: string]: Schema | undefined;
[ref: string]: SchemaComponent | undefined;
}

export interface Parameter {
export interface ParameterComponent {
zodSchema?: ZodType;
paramObject:
| oas31.ParameterObject
Expand All @@ -27,7 +31,7 @@ export interface Parameter {
}

interface ParametersComponentObject {
[ref: string]: Parameter | undefined;
[ref: string]: ParameterComponent | undefined;
}

export interface Header {
Expand Down Expand Up @@ -90,9 +94,14 @@ const createSchemas = (
}

if (schema instanceof ZodType) {
const state: SchemaState = {
components,
type: schema._def.openapi?.refType ?? 'output',
};
components.schemas[ref] = {
schemaObject: createSchemaWithMetadata(schema, components),
schemaObject: createSchemaWithMetadata(schema, state),
zodSchema: schema,
types: state.effectType ? [state.effectType] : ['input', 'output'],
};
return;
}
Expand Down
3 changes: 3 additions & 0 deletions src/create/content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe('createContent', () => {
},
},
getDefaultComponents(),
'output',
);

expect(result).toStrictEqual(expectedResult);
Expand Down Expand Up @@ -66,6 +67,7 @@ describe('createContent', () => {
},
},
getDefaultComponents(),
'output',
);

expect(result).toStrictEqual(expectedResult);
Expand Down Expand Up @@ -99,6 +101,7 @@ describe('createContent', () => {
},
},
getDefaultComponents(),
'output',
);

expect(result).toStrictEqual(expectedResult);
Expand Down
13 changes: 10 additions & 3 deletions src/create/content.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { oas31 } from 'openapi3-ts';
import { AnyZodObject, ZodType } from 'zod';

import { ComponentsObject } from './components';
import { ComponentsObject, CreationType } from './components';
import { ZodOpenApiContentObject, ZodOpenApiMediaTypeObject } from './document';
import { createSchemaOrRef } from './schema';

Expand All @@ -12,6 +12,7 @@ const createMediaTypeSchema = (
| oas31.ReferenceObject
| undefined,
components: ComponentsObject,
type: CreationType,
): oas31.SchemaObject | oas31.ReferenceObject | undefined => {
if (!schemaObject) {
return undefined;
Expand All @@ -21,32 +22,38 @@ const createMediaTypeSchema = (
return schemaObject;
}

return createSchemaOrRef(schemaObject, components);
return createSchemaOrRef(schemaObject, {
components,
type,
});
};

const createMediaTypeObject = (
mediaTypeObject: ZodOpenApiMediaTypeObject | undefined,
components: ComponentsObject,
type: CreationType,
): oas31.MediaTypeObject | undefined => {
if (!mediaTypeObject) {
return undefined;
}

return {
...mediaTypeObject,
schema: createMediaTypeSchema(mediaTypeObject.schema, components),
schema: createMediaTypeSchema(mediaTypeObject.schema, components, type),
};
};

export const createContent = (
contentObject: ZodOpenApiContentObject,
components: ComponentsObject,
type: CreationType,
): oas31.ContentObject =>
Object.entries(contentObject).reduce<oas31.ContentObject>(
(acc, [path, zodOpenApiMediaTypeObject]): oas31.ContentObject => {
const mediaTypeObject = createMediaTypeObject(
zodOpenApiMediaTypeObject,
components,
type,
);

if (mediaTypeObject) {
Expand Down
5 changes: 4 additions & 1 deletion src/create/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ export const createBaseParameter = (
components: ComponentsObject,
): oas31.BaseParameterObject => {
const { ref, ...rest } = schema._def.openapi?.param ?? {};
const schemaOrRef = createSchemaOrRef(schema, components);
const schemaOrRef = createSchemaOrRef(schema, {
components,
type: 'input',
});
const required = !schema.isOptional();
return {
...rest,
Expand Down
2 changes: 1 addition & 1 deletion src/create/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const createRequestBody = (
}
return {
...requestBodyObject,
content: createContent(requestBodyObject.content, components),
content: createContent(requestBodyObject.content, components, 'input'),
};
};

Expand Down
7 changes: 5 additions & 2 deletions src/create/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ export const createBaseHeader = (
components: ComponentsObject,
): oas31.BaseParameterObject => {
const { ref, ...rest } = schema._def.openapi?.header ?? {};
const schemaOrRef = createSchemaOrRef(schema, components);
const schemaOrRef = createSchemaOrRef(schema, {
components,
type: 'input',
});
const required = !schema.isOptional();
return {
...rest,
Expand Down Expand Up @@ -123,7 +126,7 @@ const createResponse = (
return {
...rest,
...(maybeHeaders && { headers: maybeHeaders }),
...(content && { content: createContent(content, components) }),
...(content && { content: createContent(content, components, 'output') }),
};
};

Expand Down
8 changes: 4 additions & 4 deletions src/create/schema/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts';
import { z } from 'zod';

import { extendZodWithOpenApi } from '../../extendZod';
import { getDefaultComponents } from '../components';
import { createOutputState } from '../../testing/state';

import { createArraySchema } from './array';

Expand All @@ -18,7 +18,7 @@ describe('createArraySchema', () => {
};
const schema = z.array(z.string());

const result = createArraySchema(schema, getDefaultComponents());
const result = createArraySchema(schema, createOutputState());

expect(result).toStrictEqual(expected);
});
Expand All @@ -34,7 +34,7 @@ describe('createArraySchema', () => {
};
const schema = z.array(z.string()).min(0).max(10);

const result = createArraySchema(schema, getDefaultComponents());
const result = createArraySchema(schema, createOutputState());

expect(result).toStrictEqual(expected);
});
Expand All @@ -50,7 +50,7 @@ describe('createArraySchema', () => {
};
const schema = z.array(z.string()).length(10);

const result = createArraySchema(schema, getDefaultComponents());
const result = createArraySchema(schema, createOutputState());

expect(result).toStrictEqual(expected);
});
Expand Down
8 changes: 3 additions & 5 deletions src/create/schema/array.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { oas31 } from 'openapi3-ts';
import { ZodArray, ZodTypeAny } from 'zod';

import { ComponentsObject } from '../components';

import { createSchemaOrRef } from '.';
import { SchemaState, createSchemaOrRef } from '.';

export const createArraySchema = (
zodArray: ZodArray<any, any>,
components: ComponentsObject,
state: SchemaState,
): oas31.SchemaObject => {
const zodType = zodArray._def.type as ZodTypeAny;
const minItems =
Expand All @@ -16,7 +14,7 @@ export const createArraySchema = (
zodArray._def.exactLength?.value ?? zodArray._def.maxLength?.value;
return {
type: 'array',
items: createSchemaOrRef(zodType, components),
items: createSchemaOrRef(zodType, state),
...(minItems !== undefined && { minItems }),
...(maxItems !== undefined && { maxItems }),
};
Expand Down
4 changes: 2 additions & 2 deletions src/create/schema/catch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts';
import { z } from 'zod';

import { extendZodWithOpenApi } from '../../extendZod';
import { getDefaultComponents } from '../components';
import { createOutputState } from '../../testing/state';

import { createCatchSchema } from './catch';

Expand All @@ -15,7 +15,7 @@ describe('createCatchSchema', () => {
};
const schema = z.string().catch('bob');

const result = createCatchSchema(schema, getDefaultComponents());
const result = createCatchSchema(schema, createOutputState());

expect(result).toStrictEqual(expected);
});
Expand Down
8 changes: 3 additions & 5 deletions src/create/schema/catch.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { oas31 } from 'openapi3-ts';
import { ZodCatch, ZodType } from 'zod';

import { ComponentsObject } from '../components';

import { createSchemaOrRef } from '.';
import { SchemaState, createSchemaOrRef } from '.';

export const createCatchSchema = (
zodCatch: ZodCatch<any>,
components: ComponentsObject,
state: SchemaState,
): oas31.SchemaObject | oas31.ReferenceObject =>
createSchemaOrRef(zodCatch._def.innerType as ZodType, components);
createSchemaOrRef(zodCatch._def.innerType as ZodType, state);
6 changes: 3 additions & 3 deletions src/create/schema/default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts';
import { z } from 'zod';

import { extendZodWithOpenApi } from '../../extendZod';
import { getDefaultComponents } from '../components';
import { createOutputState } from '../../testing/state';

import { createDefaultSchema } from './default';

Expand All @@ -16,7 +16,7 @@ describe('createDefaultSchema', () => {
};
const schema = z.string().default('a');

const result = createDefaultSchema(schema, getDefaultComponents());
const result = createDefaultSchema(schema, createOutputState());

expect(result).toStrictEqual(expected);
});
Expand All @@ -34,7 +34,7 @@ describe('createDefaultSchema', () => {
};
const schema = z.string().openapi({ ref: 'ref' }).optional().default('a');

const result = createDefaultSchema(schema, getDefaultComponents());
const result = createDefaultSchema(schema, createOutputState());

expect(result).toStrictEqual(expected);
});
Expand Down
Loading