anatine / zod-plugins

Plugins and utilities for Zod
590 stars 84 forks source link

[zod-nestjs] feature request - Chained syntax for `extendApi` #82

Open djbb7 opened 1 year ago

djbb7 commented 1 year ago

Currently the extendApi syntax wraps around zod objects, which is hard to read. It would be much more readable to use a chained syntax to avoid all the nested parentheses.

Current syntax:

export const GetCatsZ = extendApi(
  z.object({
    cats: extendApi(z.array(z.string()), { description: 'List of cats' }),
  }),
  { title: 'Get Cat Response' }
);

Suggested syntax:

export const GetCatsZ = 
  z.object({
    cats: z.string().array().extendApi({ description: 'List of cats' }),
  }).extendApi(
    { title: 'Get Cat Response' }
  );

I think this could be achieved by patching the z prototype schema, there is an example of a project that does that here: https://github.com/asteasolutions/zod-to-openapi/blob/master/src/zod-extensions.ts#L50

Brian-McBride commented 1 year ago

Want to create a PR for this? And how would typing work? It would be extending the Zod object.

tangye1234 commented 1 year ago

Try this:

import { extendApi as extendOpenApi } from '@anatine/zod-openapi'
import type { SchemaObject } from 'openapi3-ts'
import type { default as z, ZodTypeDef, ZodType } from 'zod'

export interface ApiOptions<T extends ZodType<any>> extends SchemaObject {
  example?: z.input<T>
  examples?: z.input<T>[]
}

declare module 'zod' {
  interface ZodType<
    Output = any,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    Def extends ZodTypeDef = ZodTypeDef,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    Input = Output
  > {
    extendApi<T extends ZodType<any>>(this: T, metadata: ApiOptions<T>): T
  }
}

export function extendZodWithExtendApi(zod: typeof z) {
  if (typeof zod.ZodType.prototype.extendApi !== 'undefined') {
    return
  }

  zod.ZodType.prototype.extendApi = function extendApi(
    this: any,
    args: SchemaObject
  ) {
    return extendOpenApi((this as ZodType<any>).transform(s => s) as any, args)
  }
}
codinsonn commented 1 year ago

Can confirm the solution provided by @tangye1234 works, check out a broader example here: https://github.com/Aetherspace/green-stack-starter-demo/blob/main/packages/%40aetherspace/schemas/aetherSchemas.ts

There is 1 caveat to this approach though, since if any other package exports zod, bundlers like webpack will not apply your prototype extensions to both versions.

You can fix this by either:

Papooch commented 2 months ago

This is already possible with extendZodWithOpenApi(z)

extendZodWithOpenApi(z)

export const GetCatsZ = 
  z.object({
    cats: z.string().array().extendApi({ description: 'List of cats' }),
  }).openapi(
    { title: 'Get Cat Response' }
  );