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

Replace current and previous properties with symbols to allow JSON serialization of zod schema #393

Merged
merged 2 commits into from
Dec 20, 2024
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,6 @@
"entryPoint": "src/index.ts",
"template": "oss-npm-package",
"type": "package",
"version": "9.0.0-main-20240928013837"
"version": "9.1.0"
}
}
23 changes: 14 additions & 9 deletions src/create/schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ZodType, ZodTypeDef } from 'zod';

import { currentSymbol, previousSymbol } from '../../extendZodTypes';
import type { oas30, oas31 } from '../../openapi3-ts/dist';
import {
type ComponentsObject,
Expand Down Expand Up @@ -220,18 +221,22 @@ export const createSchemaOrRef = <
return existingRef;
}

const previous = zodSchema._def.zodOpenApi?.previous
? (createSchemaOrRef(zodSchema._def.zodOpenApi.previous, state, true) as
| RefObject
| undefined)
const previous = zodSchema._def.zodOpenApi?.[previousSymbol]
? (createSchemaOrRef(
zodSchema._def.zodOpenApi[previousSymbol],
state,
true,
) as RefObject | undefined)
: undefined;

const current =
zodSchema._def.zodOpenApi?.current &&
zodSchema._def.zodOpenApi.current !== zodSchema
? (createSchemaOrRef(zodSchema._def.zodOpenApi.current, state, true) as
| RefObject
| undefined)
zodSchema._def.zodOpenApi?.[currentSymbol] &&
zodSchema._def.zodOpenApi[currentSymbol] !== zodSchema
? (createSchemaOrRef(
zodSchema._def.zodOpenApi[currentSymbol],
state,
true,
) as RefObject | undefined)
: undefined;

const ref = zodSchema._def.zodOpenApi?.openapi?.ref ?? component?.ref;
Expand Down
15 changes: 11 additions & 4 deletions src/create/schema/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,10 @@ describe('enhanceWithMetadata', () => {
"coerce": false,
"typeName": "ZodString",
"zodOpenApi": {
"current": [Circular],
"openapi": {
"ref": "foo",
},
Symbol(current): [Circular],
},
},
"and": [Function],
Expand Down Expand Up @@ -339,10 +339,10 @@ describe('enhanceWithMetadata', () => {
"coerce": false,
"typeName": "ZodString",
"zodOpenApi": {
"current": [Circular],
"openapi": {
"ref": "foo",
},
Symbol(current): [Circular],
},
},
"and": [Function],
Expand Down Expand Up @@ -604,10 +604,10 @@ describe('enhanceWithMetadata', () => {
"coerce": false,
"typeName": "ZodString",
"zodOpenApi": {
"current": [Circular],
"openapi": {
"ref": "foo",
},
Symbol(current): [Circular],
},
},
"and": [Function],
Expand Down Expand Up @@ -690,10 +690,10 @@ describe('enhanceWithMetadata', () => {
"coerce": false,
"typeName": "ZodString",
"zodOpenApi": {
"current": [Circular],
"openapi": {
"ref": "foo",
},
Symbol(current): [Circular],
},
},
"and": [Function],
Expand Down Expand Up @@ -934,4 +934,11 @@ describe('enhanceWithMetadata', () => {
]
`);
});

it('does not fail JSON serialization', () => {
const FooSchema = z.string().openapi({ ref: 'foo' });
expect(() => {
JSON.stringify(FooSchema);
}).not.toThrow();
});
});
1 change: 0 additions & 1 deletion src/entries/extend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ import { extendZodWithOpenApi } from '../extendZod';

extendZodWithOpenApi(z);

// eslint-disable-next-line @typescript-eslint/consistent-type-exports
export * from '../extendZodTypes'; // compatibility with < TS 5.0 as the export type * syntax is not supported
12 changes: 7 additions & 5 deletions src/extendZod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from 'zod';

import { createSchema } from './create/schema/single';
import { extendZodWithOpenApi } from './extendZod';
import { currentSymbol, previousSymbol } from './extendZodTypes';

extendZodWithOpenApi(z);

Expand All @@ -22,16 +23,17 @@ describe('extendZodWithOpenApi', () => {
expect(a._def.zodOpenApi?.openapi?.description).toBe('test');
expect(b._def.zodOpenApi?.openapi?.description).toBe('test2');
expect(
b._def.zodOpenApi?.previous?._def.zodOpenApi?.openapi?.description,
b._def.zodOpenApi?.[previousSymbol]?._def.zodOpenApi?.openapi
?.description,
).toBe('test');
});

it('sets current metadata when a schema is used again', () => {
const a = z.string().openapi({ ref: 'a' });
const b = a.uuid();

expect(a._def.zodOpenApi?.current).toBe(a);
expect(b._def.zodOpenApi?.current).toBe(a);
expect(a._def.zodOpenApi?.[currentSymbol]).toBe(a);
expect(b._def.zodOpenApi?.[currentSymbol]).toBe(a);
});

it('adds ._def.zodOpenApi.openapi fields to a zod type', () => {
Expand All @@ -53,7 +55,7 @@ describe('extendZodWithOpenApi', () => {
const b = a.extend({ b: z.string() });

expect(a._def.zodOpenApi?.openapi?.ref).toBe('a');
expect(b._def.zodOpenApi?.previous).toStrictEqual(a);
expect(b._def.zodOpenApi?.[previousSymbol]).toStrictEqual(a);
});

it('removes previous openapi ref for an object when .omit or .pick is used', () => {
Expand All @@ -74,7 +76,7 @@ describe('extendZodWithOpenApi', () => {
});

expect(a._def.zodOpenApi?.openapi?.ref).toBe('a');
expect(b._def.zodOpenApi?.previous).toStrictEqual(a);
expect(b._def.zodOpenApi?.[previousSymbol]).toStrictEqual(a);
expect(c._def.zodOpenApi?.openapi).toEqual({});
expect(d._def.zodOpenApi?.openapi).toEqual({});

Expand Down
24 changes: 12 additions & 12 deletions src/extendZod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ZodRawShape, ZodTypeDef, z } from 'zod';

import './extendZodTypes';
import { currentSymbol, previousSymbol } from './extendZodTypes';

type ZodOpenApiMetadataDef = NonNullable<ZodTypeDef['zodOpenApi']>;
type ZodOpenApiMetadata = ZodOpenApiMetadataDef['openapi'];
Expand Down Expand Up @@ -42,11 +42,11 @@ export function extendZodWithOpenApi(zod: typeof z) {
});

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
result._def.zodOpenApi.current = result;
result._def.zodOpenApi[currentSymbol] = result;

if (zodOpenApi) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
result._def.zodOpenApi.previous = this;
result._def.zodOpenApi[previousSymbol] = this;
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
Expand All @@ -63,13 +63,13 @@ export function extendZodWithOpenApi(zod: typeof z) {
if (def.zodOpenApi) {
const cloned = { ...def.zodOpenApi };
cloned.openapi = mergeOpenApi({ description: args[0] }, cloned.openapi);
cloned.previous = this;
cloned.current = result;
cloned[previousSymbol] = this;
cloned[currentSymbol] = result;
def.zodOpenApi = cloned;
} else {
def.zodOpenApi = {
openapi: { description: args[0] },
current: result,
[currentSymbol]: result,
};
}

Expand All @@ -88,11 +88,11 @@ export function extendZodWithOpenApi(zod: typeof z) {
if (zodOpenApi) {
const cloned = { ...zodOpenApi };
cloned.openapi = mergeOpenApi({}, cloned.openapi);
cloned.previous = this;
cloned[previousSymbol] = this;
extendResult._def.zodOpenApi = cloned;
} else {
extendResult._def.zodOpenApi = {
previous: this,
[previousSymbol]: this,
};
}

Expand All @@ -112,8 +112,8 @@ export function extendZodWithOpenApi(zod: typeof z) {
if (zodOpenApi) {
const cloned = { ...zodOpenApi };
cloned.openapi = mergeOpenApi({}, cloned.openapi);
delete cloned.previous;
delete cloned.current;
delete cloned[previousSymbol];
delete cloned[currentSymbol];
omitResult._def.zodOpenApi = cloned;
}

Expand All @@ -133,8 +133,8 @@ export function extendZodWithOpenApi(zod: typeof z) {
if (zodOpenApi) {
const cloned = { ...zodOpenApi };
cloned.openapi = mergeOpenApi({}, cloned.openapi);
delete cloned.previous;
delete cloned.current;
delete cloned[previousSymbol];
delete cloned[currentSymbol];
pickResult._def.zodOpenApi = cloned;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any
Expand Down
7 changes: 5 additions & 2 deletions src/extendZodTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ type SchemaObject = oas30.SchemaObject & oas31.SchemaObject;

type ReplaceDate<T> = T extends Date ? Date | string : T;

export const currentSymbol = Symbol('current');
export const previousSymbol = Symbol('previous');

/**
* zod-openapi metadata
*/
Expand Down Expand Up @@ -77,12 +80,12 @@ interface ZodOpenApiMetadataDef {
/**
* Used to keep track of the Zod Schema had `.openapi` called on it
*/
current?: ZodTypeAny;
[currentSymbol]?: ZodTypeAny;
/**
* Used to keep track of the previous Zod Schema that had `.openapi` called on it if another `.openapi` is called.
* This can also be present when .extend is called on an object.
*/
previous?: ZodTypeAny;
[previousSymbol]?: ZodTypeAny;
}

interface ZodOpenApiExtendMetadata {
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
"removeComments": false,
"target": "ES2022"
},
"exclude": ["lib*/**/*", "crackle.config.ts"],
"exclude": ["lib*/**/*", "crackle.config.ts", "dist", "api", "extend"],
"extends": "skuba/config/tsconfig.json"
}
Loading