From d6a5cbee548f6ac00b31d98f49cc1e3bf6515006 Mon Sep 17 00:00:00 2001 From: Alon Weiss Date: Fri, 20 Dec 2024 15:45:51 +0200 Subject: [PATCH 1/2] Replace current and previous properties with symbols to allow JSON serialization of zod schema (#393) Co-authored-by: samchungy --- package.json | 2 +- src/create/schema/index.ts | 23 ++++++++++++++--------- src/create/schema/metadata.test.ts | 15 +++++++++++---- src/entries/extend.ts | 1 - src/extendZod.test.ts | 12 +++++++----- src/extendZod.ts | 24 ++++++++++++------------ src/extendZodTypes.ts | 7 +++++-- tsconfig.json | 2 +- 8 files changed, 51 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 5f6bca4..142462c 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,6 @@ "entryPoint": "src/index.ts", "template": "oss-npm-package", "type": "package", - "version": "9.0.0-main-20240928013837" + "version": "9.1.0" } } diff --git a/src/create/schema/index.ts b/src/create/schema/index.ts index 7a00d3a..5f69f07 100644 --- a/src/create/schema/index.ts +++ b/src/create/schema/index.ts @@ -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, @@ -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; diff --git a/src/create/schema/metadata.test.ts b/src/create/schema/metadata.test.ts index c415adf..c61860c 100644 --- a/src/create/schema/metadata.test.ts +++ b/src/create/schema/metadata.test.ts @@ -253,10 +253,10 @@ describe('enhanceWithMetadata', () => { "coerce": false, "typeName": "ZodString", "zodOpenApi": { - "current": [Circular], "openapi": { "ref": "foo", }, + Symbol(current): [Circular], }, }, "and": [Function], @@ -339,10 +339,10 @@ describe('enhanceWithMetadata', () => { "coerce": false, "typeName": "ZodString", "zodOpenApi": { - "current": [Circular], "openapi": { "ref": "foo", }, + Symbol(current): [Circular], }, }, "and": [Function], @@ -604,10 +604,10 @@ describe('enhanceWithMetadata', () => { "coerce": false, "typeName": "ZodString", "zodOpenApi": { - "current": [Circular], "openapi": { "ref": "foo", }, + Symbol(current): [Circular], }, }, "and": [Function], @@ -690,10 +690,10 @@ describe('enhanceWithMetadata', () => { "coerce": false, "typeName": "ZodString", "zodOpenApi": { - "current": [Circular], "openapi": { "ref": "foo", }, + Symbol(current): [Circular], }, }, "and": [Function], @@ -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(); + }); }); diff --git a/src/entries/extend.ts b/src/entries/extend.ts index fea7d7e..ad509e9 100644 --- a/src/entries/extend.ts +++ b/src/entries/extend.ts @@ -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 diff --git a/src/extendZod.test.ts b/src/extendZod.test.ts index a39bb68..831ee36 100644 --- a/src/extendZod.test.ts +++ b/src/extendZod.test.ts @@ -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); @@ -22,7 +23,8 @@ 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'); }); @@ -30,8 +32,8 @@ describe('extendZodWithOpenApi', () => { 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', () => { @@ -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', () => { @@ -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({}); diff --git a/src/extendZod.ts b/src/extendZod.ts index 4738f1c..149daee 100644 --- a/src/extendZod.ts +++ b/src/extendZod.ts @@ -1,6 +1,6 @@ import type { ZodRawShape, ZodTypeDef, z } from 'zod'; -import './extendZodTypes'; +import { currentSymbol, previousSymbol } from './extendZodTypes'; type ZodOpenApiMetadataDef = NonNullable; type ZodOpenApiMetadata = ZodOpenApiMetadataDef['openapi']; @@ -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 @@ -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, }; } @@ -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, }; } @@ -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; } @@ -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 diff --git a/src/extendZodTypes.ts b/src/extendZodTypes.ts index c1762fc..c4ff49d 100644 --- a/src/extendZodTypes.ts +++ b/src/extendZodTypes.ts @@ -7,6 +7,9 @@ type SchemaObject = oas30.SchemaObject & oas31.SchemaObject; type ReplaceDate = T extends Date ? Date | string : T; +export const currentSymbol = Symbol('current'); +export const previousSymbol = Symbol('previous'); + /** * zod-openapi metadata */ @@ -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 { diff --git a/tsconfig.json b/tsconfig.json index ff183e6..59cef52 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" } From 22dc62f057b4791657d4221e77651da65e9b47dd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:46:58 +0000 Subject: [PATCH 2/2] Release v4.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 142462c..f7bd74f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zod-openapi", - "version": "4.2.0", + "version": "4.2.1", "description": "Convert Zod Schemas to OpenAPI v3.x documentation", "keywords": [ "typescript",