• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RobinTail / express-zod-api / 5547099979

pending completion
5547099979

Pull #1052

github

web-flow
Merge ba5af486f into 3ac8ab616
Pull Request #1052: Build(deps-dev): bump @typescript-eslint/eslint-plugin from 5.60.1 to 5.62.0

573 of 600 branches covered (95.5%)

1156 of 1156 relevant lines covered (100.0%)

485.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.9
/src/documentation-helpers.ts
1
import {
2
  ContentObject,
3
  ExampleObject,
4
  ExamplesObject,
5
  MediaTypeObject,
6
  OAuthFlowsObject,
7
  ParameterObject,
8
  ReferenceObject,
9
  RequestBodyObject,
10
  ResponseObject,
11
  SchemaObject,
12
  SchemaObjectType,
13
  SecurityRequirementObject,
14
  SecuritySchemeObject,
15
  TagObject,
16
  isReferenceObject,
17
  isSchemaObject,
18
} from "openapi3-ts/oas30";
168✔
19
import { omit } from "ramda";
168✔
20
import { z } from "zod";
168✔
21
import {
22
  getExamples,
23
  getRoutePathParams,
24
  hasCoercion,
25
  hasTopLevelTransformingEffect,
26
  makeCleanId,
27
  routePathParamsRegex,
28
  tryToTransform,
29
} from "./common-helpers";
168✔
30
import { InputSource, TagsConfig } from "./config-type";
31
import { ZodDateIn, isoDateRegex } from "./date-in-schema";
168✔
32
import { ZodDateOut } from "./date-out-schema";
33
import { AbstractEndpoint } from "./endpoint";
34
import { DocumentationError } from "./errors";
168✔
35
import { ZodFile } from "./file-schema";
36
import { IOSchema } from "./io-schema";
37
import {
38
  LogicalContainer,
39
  andToOr,
40
  mapLogicalContainer,
41
} from "./logical-container";
168✔
42
import { copyMeta } from "./metadata";
168✔
43
import { Method } from "./method";
44
import {
45
  HandlingRules,
46
  HandlingVariant,
47
  SchemaHandler,
48
  walkSchema,
49
} from "./schema-walker";
168✔
50
import { Security } from "./security";
51
import { ZodUpload } from "./upload-schema";
52

53
type MediaExamples = Pick<MediaTypeObject, "examples">;
54

55
export interface OpenAPIContext {
56
  isResponse: boolean;
57
  serializer: (schema: z.ZodTypeAny) => string;
58
  getRef: (name: string) => ReferenceObject | undefined;
59
  makeRef: (
60
    name: string,
61
    schema: SchemaObject | ReferenceObject
62
  ) => ReferenceObject;
63
  path: string;
64
  method: Method;
65
}
66

67
type Depicter<
68
  T extends z.ZodTypeAny,
69
  Variant extends HandlingVariant = "regular"
70
> = SchemaHandler<T, SchemaObject | ReferenceObject, OpenAPIContext, Variant>;
71

72
interface ReqResDepictHelperCommonProps
73
  extends Pick<
74
    OpenAPIContext,
75
    "serializer" | "getRef" | "makeRef" | "path" | "method"
76
  > {
77
  endpoint: AbstractEndpoint;
78
  composition: "inline" | "components";
79
  clue?: string;
80
}
81

82
const shortDescriptionLimit = 50;
168✔
83
const isoDateDocumentationUrl =
84
  "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString";
168✔
85

86
const samples: Record<
87
  Exclude<NonNullable<SchemaObjectType>, Array<any>>,
88
  any
89
> = {
168✔
90
  integer: 0,
91
  number: 0,
92
  string: "",
93
  boolean: false,
94
  object: {},
95
  null: null,
96
  array: [],
97
};
98

99
/* eslint-disable @typescript-eslint/no-use-before-define */
100

101
export const reformatParamsInPath = (path: string) =>
384✔
102
  path.replace(routePathParamsRegex, (param) => `{${param.slice(1)}}`);
384✔
103

104
export const depictDefault: Depicter<z.ZodDefault<z.ZodTypeAny>> = ({
168✔
105
  schema: {
106
    _def: { innerType, defaultValue },
107
  },
108
  next,
109
}) => ({
24✔
110
  ...next({ schema: innerType }),
111
  default: defaultValue(),
112
});
113

114
export const depictCatch: Depicter<z.ZodCatch<z.ZodTypeAny>> = ({
168✔
115
  schema: {
116
    _def: { innerType },
117
  },
118
  next,
119
}) => next({ schema: innerType });
8✔
120

121
export const depictAny: Depicter<z.ZodAny> = () => ({
168✔
122
  format: "any",
123
});
124

125
export const depictUpload: Depicter<ZodUpload> = (ctx) => {
168✔
126
  if (ctx.isResponse) {
32✔
127
    throw new DocumentationError({
8✔
128
      message: "Please use z.upload() only for input.",
129
      ...ctx,
130
    });
131
  }
132
  return {
24✔
133
    type: "string",
134
    format: "binary",
135
  };
136
};
137

138
export const depictFile: Depicter<ZodFile> = ({
168✔
139
  schema: { isBinary, isBase64 },
140
  ...ctx
141
}) => {
142
  if (!ctx.isResponse) {
48✔
143
    throw new DocumentationError({
8✔
144
      message: "Please use z.file() only within ResultHandler.",
145
      ...ctx,
146
    });
147
  }
148
  return {
40✔
149
    type: "string",
150
    format: isBinary ? "binary" : isBase64 ? "byte" : "file",
56✔
151
  };
152
};
153

154
export const depictUnion: Depicter<
8✔
155
  z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>
156
> = ({ schema: { options }, next }) => ({
168✔
157
  oneOf: options.map((option) => next({ schema: option })),
144✔
158
});
159

160
export const depictDiscriminatedUnion: Depicter<
8✔
161
  z.ZodDiscriminatedUnion<string, z.ZodObject<any>[]>
162
> = ({ schema: { options, discriminator }, next }) => {
168✔
163
  return {
24✔
164
    discriminator: { propertyName: discriminator },
165
    oneOf: Array.from(options.values()).map((option) =>
166
      next({ schema: option })
48✔
167
    ),
168
  };
169
};
170

171
export const depictIntersection: Depicter<
8✔
172
  z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>
173
> = ({
168✔
174
  schema: {
175
    _def: { left, right },
176
  },
177
  next,
178
}) => ({
72✔
179
  allOf: [left, right].map((entry) => next({ schema: entry })),
144✔
180
});
181

182
export const depictOptional: Depicter<z.ZodOptional<any>> = ({
168✔
183
  schema,
184
  next,
185
}) => next({ schema: schema.unwrap() });
88✔
186

187
export const depictNullable: Depicter<z.ZodNullable<any>> = ({
168✔
188
  schema,
189
  next,
190
}) => ({
48✔
191
  nullable: true,
192
  ...next({ schema: schema.unwrap() }),
193
});
194

195
export const depictEnum: Depicter<z.ZodEnum<any> | z.ZodNativeEnum<any>> = ({
168✔
196
  schema,
197
}) => ({
32✔
198
  type: typeof Object.values(schema.enum)[0] as "string" | "number",
199
  enum: Object.values(schema.enum),
200
});
201

202
export const depictLiteral: Depicter<z.ZodLiteral<any>> = ({
168✔
203
  schema: { value },
204
}) => ({
872✔
205
  type: typeof value as "string" | "number" | "boolean",
206
  enum: [value],
207
});
208

209
export const depictObject: Depicter<z.AnyZodObject> = ({
168✔
210
  schema,
211
  isResponse,
212
  ...rest
213
}) => {
214
  const required = Object.keys(schema.shape).filter((key) => {
2,056✔
215
    const prop = schema.shape[key];
3,272✔
216
    const isOptional =
217
      isResponse && hasCoercion(prop)
3,272✔
218
        ? prop instanceof z.ZodOptional
219
        : prop.isOptional();
220
    return !isOptional;
3,272✔
221
  });
222
  return {
2,056✔
223
    type: "object",
224
    properties: depictObjectProperties({ schema, isResponse, ...rest }),
225
    ...(required.length ? { required } : {}),
2,000✔
226
  };
227
};
228

229
/**
230
 * @see https://swagger.io/docs/specification/data-models/data-types/
231
 * @todo use type:"null" for OpenAPI 3.1
232
 * */
233
export const depictNull: Depicter<z.ZodNull> = () => ({
168✔
234
  type: "string",
235
  nullable: true,
236
  format: "null",
237
});
238

239
export const depictDateIn: Depicter<ZodDateIn> = (ctx) => {
168✔
240
  if (ctx.isResponse) {
40✔
241
    throw new DocumentationError({
8✔
242
      message: "Please use z.dateOut() for output.",
243
      ...ctx,
244
    });
245
  }
246
  return {
32✔
247
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
248
    type: "string",
249
    format: "date-time",
250
    pattern: isoDateRegex.source,
251
    externalDocs: {
252
      url: isoDateDocumentationUrl,
253
    },
254
  };
255
};
256

257
export const depictDateOut: Depicter<ZodDateOut> = (ctx) => {
168✔
258
  if (!ctx.isResponse) {
40✔
259
    throw new DocumentationError({
8✔
260
      message: "Please use z.dateIn() for input.",
261
      ...ctx,
262
    });
263
  }
264
  return {
32✔
265
    description: "YYYY-MM-DDTHH:mm:ss.sssZ",
266
    type: "string",
267
    format: "date-time",
268
    externalDocs: {
269
      url: isoDateDocumentationUrl,
270
    },
271
  };
272
};
273

274
/** @throws DocumentationError */
275
export const depictDate: Depicter<z.ZodDate> = (ctx) => {
168✔
276
  throw new DocumentationError({
16✔
277
    message: `Using z.date() within ${
278
      ctx.isResponse ? "output" : "input"
16✔
279
    } schema is forbidden. Please use z.date${
280
      ctx.isResponse ? "Out" : "In"
16✔
281
    }() instead. Check out the documentation for details.`,
282
    ...ctx,
283
  });
284
};
285

286
export const depictBoolean: Depicter<z.ZodBoolean> = () => ({
240✔
287
  type: "boolean",
288
});
289

290
export const depictBigInt: Depicter<z.ZodBigInt> = () => ({
168✔
291
  type: "integer",
292
  format: "bigint",
293
});
294

295
export const depictRecord: Depicter<z.ZodRecord<z.ZodTypeAny>> = ({
168✔
296
  schema: { keySchema, valueSchema },
297
  ...rest
298
}) => {
299
  if (keySchema instanceof z.ZodEnum || keySchema instanceof z.ZodNativeEnum) {
112✔
300
    const keys = Object.values(keySchema.enum) as string[];
16✔
301
    const shape = keys.reduce(
16✔
302
      (carry, key) => ({
32✔
303
        ...carry,
304
        [key]: valueSchema,
305
      }),
306
      {} as z.ZodRawShape
307
    );
308
    return {
16✔
309
      type: "object",
310
      properties: depictObjectProperties({
311
        schema: z.object(shape),
312
        ...rest,
313
      }),
314
      ...(keys.length ? { required: keys } : {}),
16!
315
    };
316
  }
317
  if (keySchema instanceof z.ZodLiteral) {
96✔
318
    return {
16✔
319
      type: "object",
320
      properties: depictObjectProperties({
321
        schema: z.object({
322
          [keySchema.value]: valueSchema,
323
        }),
324
        ...rest,
325
      }),
326
      required: [keySchema.value],
327
    };
328
  }
329
  if (keySchema instanceof z.ZodUnion) {
80✔
330
    const areOptionsLiteral = keySchema.options.reduce(
16✔
331
      (carry: boolean, option: z.ZodTypeAny) =>
332
        carry && option instanceof z.ZodLiteral,
32✔
333
      true
334
    );
335
    if (areOptionsLiteral) {
16✔
336
      const shape = keySchema.options.reduce(
16✔
337
        (carry: z.ZodRawShape, option: z.ZodLiteral<any>) => ({
32✔
338
          ...carry,
339
          [option.value]: valueSchema,
340
        }),
341
        {} as z.ZodRawShape
342
      );
343
      return {
16✔
344
        type: "object",
345
        properties: depictObjectProperties({
346
          schema: z.object(shape),
347
          ...rest,
348
        }),
349
        required: keySchema.options.map(
350
          (option: z.ZodLiteral<any>) => option.value
32✔
351
        ),
352
      };
353
    }
354
  }
355
  return {
64✔
356
    type: "object",
357
    additionalProperties: rest.next({ schema: valueSchema }),
358
  };
359
};
360

361
export const depictArray: Depicter<z.ZodArray<z.ZodTypeAny>> = ({
168✔
362
  schema: { _def: def, element },
363
  next,
364
}) => ({
104✔
365
  type: "array",
366
  items: next({ schema: element }),
367
  ...(def.minLength !== null && { minItems: def.minLength.value }),
144✔
368
  ...(def.maxLength !== null && { maxItems: def.maxLength.value }),
112✔
369
});
370

371
/** @todo improve it when OpenAPI 3.1.0 will be released */
372
export const depictTuple: Depicter<z.ZodTuple> = ({
168✔
373
  schema: { items },
374
  next,
375
}) => {
376
  const types = items.map((item) => next({ schema: item }));
72✔
377
  return {
40✔
378
    type: "array",
379
    minItems: types.length,
380
    maxItems: types.length,
381
    items: {
382
      oneOf: types,
383
      format: "tuple",
384
      ...(types.length > 0 && {
72✔
385
        description: types
386
          .map(
387
            (item, index) =>
388
              `${index}: ${isSchemaObject(item) ? item.type : item.$ref}`
72!
389
          )
390
          .join(", "),
391
      }),
392
    },
393
  };
394
};
395

396
export const depictString: Depicter<z.ZodString> = ({
168✔
397
  schema: {
398
    isEmail,
399
    isURL,
400
    minLength,
401
    maxLength,
402
    isUUID,
403
    isCUID,
404
    isCUID2,
405
    isULID,
406
    isIP,
407
    isEmoji,
408
    isDatetime,
409
    _def: { checks },
410
  },
411
}) => {
412
  const regexCheck = checks.find(
1,432✔
413
    (check): check is z.ZodStringCheck & { kind: "regex" } =>
414
      check.kind === "regex"
320✔
415
  );
416
  const datetimeCheck = checks.find(
1,432✔
417
    (check): check is z.ZodStringCheck & { kind: "datetime" } =>
418
      check.kind === "datetime"
328✔
419
  );
420
  const regex = regexCheck
1,432✔
421
    ? regexCheck.regex
422
    : datetimeCheck
1,360✔
423
    ? datetimeCheck.offset
16✔
424
      ? new RegExp(
425
          `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}:\\d{2})|Z)$`
426
        )
427
      : new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`)
428
    : undefined;
429
  return {
1,432✔
430
    type: "string" as const,
431
    ...(isDatetime && { format: "date-time" }),
1,448✔
432
    ...(isEmail && { format: "email" }),
1,456✔
433
    ...(isURL && { format: "url" }),
1,448✔
434
    ...(isUUID && { format: "uuid" }),
1,448✔
435
    ...(isCUID && { format: "cuid" }),
1,448✔
436
    ...(isCUID2 && { format: "cuid2" }),
1,440✔
437
    ...(isULID && { format: "ulid" }),
1,440✔
438
    ...(isIP && { format: "ip" }),
1,440✔
439
    ...(isEmoji && { format: "emoji" }),
1,440✔
440
    ...(minLength !== null && { minLength }),
1,512✔
441
    ...(maxLength !== null && { maxLength }),
1,464✔
442
    ...(regex && { pattern: `/${regex.source}/${regex.flags}` }),
1,520✔
443
  };
444
};
445

446
/** @todo support exclusive min/max as numbers in case of OpenAPI v3.1.x */
447
export const depictNumber: Depicter<z.ZodNumber> = ({ schema }) => {
168✔
448
  const minCheck = schema._def.checks.find(({ kind }) => kind === "min") as
344✔
449
    | Extract<z.ZodNumberCheck, { kind: "min" }>
450
    | undefined;
451
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
344✔
452
  const maxCheck = schema._def.checks.find(({ kind }) => kind === "max") as
376✔
453
    | Extract<z.ZodNumberCheck, { kind: "max" }>
454
    | undefined;
455
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
344✔
456
  return {
344✔
457
    type: schema.isInt ? ("integer" as const) : ("number" as const),
344✔
458
    format: schema.isInt ? ("int64" as const) : ("double" as const),
344✔
459
    minimum:
460
      schema.minValue === null
344✔
461
        ? schema.isInt
192✔
462
          ? Number.MIN_SAFE_INTEGER
463
          : Number.MIN_VALUE
464
        : schema.minValue,
465
    exclusiveMinimum: !isMinInclusive,
466
    maximum:
467
      schema.maxValue === null
344✔
468
        ? schema.isInt
288✔
469
          ? Number.MAX_SAFE_INTEGER
470
          : Number.MAX_VALUE
471
        : schema.maxValue,
472
    exclusiveMaximum: !isMaxInclusive,
473
  };
474
};
475

476
export const depictObjectProperties = ({
168✔
477
  schema: { shape },
478
  next,
479
}: Parameters<Depicter<z.AnyZodObject>>[0]) => {
480
  return Object.keys(shape).reduce(
2,112✔
481
    (carry, key) => ({
3,368✔
482
      ...carry,
483
      [key]: next({ schema: shape[key] }),
484
    }),
485
    {} as Record<string, SchemaObject | ReferenceObject>
486
  );
487
};
488

489
const makeSample = (depicted: SchemaObject) => {
168✔
490
  const type = (
491
    Array.isArray(depicted.type) ? depicted.type[0] : depicted.type
56!
492
  ) as keyof typeof samples;
493
  return samples?.[type];
56✔
494
};
495

496
export const depictEffect: Depicter<z.ZodEffects<z.ZodTypeAny>> = ({
168✔
497
  schema,
498
  isResponse,
499
  next,
500
}) => {
501
  const input = next({ schema: schema.innerType() });
264✔
502
  const { effect } = schema._def;
264✔
503
  if (isResponse && effect.type === "transform" && isSchemaObject(input)) {
264✔
504
    const outputType = tryToTransform({ effect, sample: makeSample(input) });
56✔
505
    if (outputType && ["number", "string", "boolean"].includes(outputType)) {
56✔
506
      return { type: outputType as "number" | "string" | "boolean" };
40✔
507
    } else {
508
      return next({ schema: z.any() });
16✔
509
    }
510
  }
511
  if (!isResponse && effect.type === "preprocess" && isSchemaObject(input)) {
208✔
512
    const { type: inputType, ...rest } = input;
24✔
513
    return {
24✔
514
      ...rest,
515
      format: `${rest.format || inputType} (preprocessed)`,
40✔
516
    };
517
  }
518
  return input;
184✔
519
};
520

521
export const depictPipeline: Depicter<z.ZodPipeline<any, any>> = ({
168✔
522
  schema,
523
  isResponse,
524
  next,
525
}) => next({ schema: schema._def[isResponse ? "out" : "in"] });
16✔
526

527
export const depictBranded: Depicter<z.ZodBranded<z.ZodTypeAny, any>> = ({
168✔
528
  schema,
529
  next,
530
}) => next({ schema: schema.unwrap() });
8✔
531

532
export const depictLazy: Depicter<z.ZodLazy<z.ZodTypeAny>> = ({
168✔
533
  next,
534
  schema: lazy,
535
  serializer: serialize,
536
  getRef,
537
  makeRef,
538
}): ReferenceObject => {
539
  const hash = serialize(lazy.schema);
96✔
540
  return (
96✔
541
    getRef(hash) ||
144✔
542
    (() => {
543
      makeRef(hash, {}); // make empty ref first
48✔
544
      return makeRef(hash, next({ schema: lazy.schema })); // update
48✔
545
    })()
546
  );
547
};
548

549
export const depictExamples = (
168✔
550
  schema: z.ZodTypeAny,
551
  isResponse: boolean,
552
  omitProps: string[] = []
816✔
553
): MediaExamples => {
554
  const examples = getExamples(schema, isResponse);
1,016✔
555
  if (examples.length === 0) {
1,016✔
556
    return {};
544✔
557
  }
558
  return {
472✔
559
    examples: examples.reduce<ExamplesObject>(
560
      (carry, example, index) => ({
496✔
561
        ...carry,
562
        [`example${index + 1}`]: <ExampleObject>{
563
          value: omit(omitProps, example),
564
        },
565
      }),
566
      {}
567
    ),
568
  };
569
};
570

571
export const depictParamExamples = (
168✔
572
  schema: z.ZodTypeAny,
573
  isResponse: boolean,
574
  param: string
575
): MediaExamples => {
576
  const examples = getExamples(schema, isResponse);
264✔
577
  if (examples.length === 0) {
264✔
578
    return {};
224✔
579
  }
580
  return {
40✔
581
    examples: examples.reduce<ExamplesObject>(
582
      (carry, example, index) =>
583
        param in example
56!
584
          ? {
585
              ...carry,
586
              [`example${index + 1}`]: <ExampleObject>{
587
                value: example[param],
588
              },
589
            }
590
          : carry,
591
      {}
592
    ),
593
  };
594
};
595

596
export function extractObjectSchema(
72✔
597
  subject: IOSchema,
598
  ctx: Pick<OpenAPIContext, "path" | "method" | "isResponse">
599
) {
600
  if (subject instanceof z.ZodObject) {
744✔
601
    return subject;
608✔
602
  }
603
  let objectSchema: z.AnyZodObject;
604
  if (
136✔
605
    subject instanceof z.ZodUnion ||
248✔
606
    subject instanceof z.ZodDiscriminatedUnion
607
  ) {
608
    objectSchema = Array.from(subject.options.values())
32✔
609
      .map((option) => extractObjectSchema(option, ctx))
64✔
610
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
64✔
611
  } else if (subject instanceof z.ZodEffects) {
104✔
612
    if (hasTopLevelTransformingEffect(subject)) {
24✔
613
      throw new DocumentationError({
8✔
614
        message: `Using transformations on the top level of ${
615
          ctx.isResponse ? "response" : "input"
8!
616
        } schema is not allowed.`,
617
        ...ctx,
618
      });
619
    }
620
    objectSchema = extractObjectSchema(subject._def.schema, ctx); // object refinement
16✔
621
  } else {
622
    // intersection
623
    objectSchema = extractObjectSchema(subject._def.left, ctx).merge(
80✔
624
      extractObjectSchema(subject._def.right, ctx)
625
    );
626
  }
627
  return copyMeta(subject, objectSchema);
128✔
628
}
629

630
export const depictRequestParams = ({
432✔
631
  path,
632
  method,
633
  endpoint,
634
  inputSources,
635
  serializer,
636
  getRef,
637
  makeRef,
638
  composition,
639
  clue = "parameter",
432✔
640
}: ReqResDepictHelperCommonProps & {
641
  inputSources: InputSource[];
642
}): ParameterObject[] => {
643
  const schema = endpoint.getSchema("input");
432✔
644
  const shape = extractObjectSchema(schema, {
432✔
645
    path,
646
    method,
647
    isResponse: false,
648
  }).shape;
649
  const pathParams = getRoutePathParams(path);
432✔
650
  const isQueryEnabled = inputSources.includes("query");
432✔
651
  const isParamsEnabled = inputSources.includes("params");
432✔
652
  const isPathParam = (name: string) =>
432✔
653
    isParamsEnabled && pathParams.includes(name);
800✔
654
  return Object.keys(shape)
432✔
655
    .filter((name) => isQueryEnabled || isPathParam(name))
768✔
656
    .map((name) => {
657
      const depicted = walkSchema({
248✔
658
        schema: shape[name],
659
        isResponse: false,
660
        rules: depicters,
661
        onEach,
662
        onMissing,
663
        serializer,
664
        getRef,
665
        makeRef,
666
        path,
667
        method,
668
      });
669
      const result =
670
        composition === "components"
248✔
671
          ? makeRef(makeCleanId(path, method, `${clue} ${name}`), depicted)
672
          : depicted;
673
      return {
248✔
674
        name,
675
        in: isPathParam(name) ? "path" : "query",
248✔
676
        required: !shape[name].isOptional(),
677
        description:
678
          (isSchemaObject(depicted) && depicted.description) ||
712✔
679
          `${method.toUpperCase()} ${path} ${clue}`,
680
        schema: result,
681
        ...depictParamExamples(schema, false, name),
682
      };
683
    });
684
};
685

686
export const depicters: HandlingRules<
448✔
687
  SchemaObject | ReferenceObject,
688
  OpenAPIContext
689
> = {
168✔
690
  ZodString: depictString,
691
  ZodNumber: depictNumber,
692
  ZodBigInt: depictBigInt,
693
  ZodBoolean: depictBoolean,
694
  ZodDateIn: depictDateIn,
695
  ZodDateOut: depictDateOut,
696
  ZodNull: depictNull,
697
  ZodArray: depictArray,
698
  ZodTuple: depictTuple,
699
  ZodRecord: depictRecord,
700
  ZodObject: depictObject,
701
  ZodLiteral: depictLiteral,
702
  ZodIntersection: depictIntersection,
703
  ZodUnion: depictUnion,
704
  ZodFile: depictFile,
705
  ZodUpload: depictUpload,
706
  ZodAny: depictAny,
707
  ZodDefault: depictDefault,
708
  ZodEnum: depictEnum,
709
  ZodNativeEnum: depictEnum,
710
  ZodEffects: depictEffect,
711
  ZodOptional: depictOptional,
712
  ZodNullable: depictNullable,
713
  ZodDiscriminatedUnion: depictDiscriminatedUnion,
714
  ZodBranded: depictBranded,
715
  ZodDate: depictDate,
716
  ZodCatch: depictCatch,
717
  ZodPipeline: depictPipeline,
718
  ZodLazy: depictLazy,
719
};
720

721
export const onEach: Depicter<z.ZodTypeAny, "each"> = ({
448✔
722
  schema,
723
  isResponse,
724
  prev,
725
}) => {
726
  if (isReferenceObject(prev)) {
5,680✔
727
    return {};
72✔
728
  }
729
  const { description } = schema;
5,608✔
730
  const shouldAvoidParsing = schema instanceof z.ZodLazy;
5,608✔
731
  const hasTypePropertyInDepiction = prev.type !== undefined;
5,608✔
732
  const isResponseHavingCoercion = isResponse && hasCoercion(schema);
5,608✔
733
  const isActuallyNullable =
734
    !shouldAvoidParsing &&
5,608✔
735
    hasTypePropertyInDepiction &&
736
    !isResponseHavingCoercion &&
737
    schema.isNullable();
738
  const examples = shouldAvoidParsing ? [] : getExamples(schema, isResponse);
5,608!
739
  return {
5,608✔
740
    ...(description && { description }),
5,648✔
741
    ...(isActuallyNullable && { nullable: true }),
5,696✔
742
    ...(examples.length > 0 && { example: examples[0] }),
6,176✔
743
  };
744
};
745

746
export const onMissing: Depicter<z.ZodTypeAny, "last"> = ({
448✔
747
  schema,
748
  ...ctx
749
}) => {
750
  throw new DocumentationError({
56✔
751
    message: `Zod type ${schema.constructor.name} is unsupported.`,
752
    ...ctx,
753
  });
754
};
755

756
export const excludeParamsFromDepiction = (
168✔
757
  depicted: SchemaObject | ReferenceObject,
758
  pathParams: string[]
759
): SchemaObject | ReferenceObject => {
760
  if (isReferenceObject(depicted)) {
360✔
761
    return depicted;
8✔
762
  }
763
  const properties = depicted.properties
352✔
764
    ? omit(pathParams, depicted.properties)
765
    : undefined;
766
  const example = depicted.example
352✔
767
    ? omit(pathParams, depicted.example)
768
    : undefined;
769
  const required = depicted.required
352✔
770
    ? depicted.required.filter((name) => !pathParams.includes(name))
520✔
771
    : undefined;
772
  const allOf = depicted.allOf
352✔
773
    ? (depicted.allOf as SchemaObject[]).map((entry) =>
774
        excludeParamsFromDepiction(entry, pathParams)
96✔
775
      )
776
    : undefined;
777
  const oneOf = depicted.oneOf
352✔
778
    ? (depicted.oneOf as SchemaObject[]).map((entry) =>
779
        excludeParamsFromDepiction(entry, pathParams)
48✔
780
      )
781
    : undefined;
782

783
  return omit(
352✔
784
    Object.entries({ properties, required, example, allOf, oneOf })
785
      .filter(([{}, value]) => value === undefined)
1,760✔
786
      .map(([key]) => key),
1,080✔
787
    {
788
      ...depicted,
789
      properties,
790
      required,
791
      example,
792
      allOf,
793
      oneOf,
794
    }
795
  );
796
};
797

798
export const excludeExampleFromDepiction = (
168✔
799
  depicted: SchemaObject | ReferenceObject
800
): SchemaObject | ReferenceObject =>
801
  isSchemaObject(depicted) ? omit(["example"], depicted) : depicted;
1,008!
802

803
export const depictResponse = ({
816✔
804
  method,
805
  path,
806
  endpoint,
807
  isPositive,
808
  serializer,
809
  getRef,
810
  makeRef,
811
  composition,
812
  clue = "response",
×
813
}: ReqResDepictHelperCommonProps & {
814
  isPositive: boolean;
815
}): ResponseObject => {
816
  const schema = endpoint.getSchema(isPositive ? "positive" : "negative");
816✔
817
  const mimeTypes = endpoint.getMimeTypes(isPositive ? "positive" : "negative");
816✔
818
  const depictedSchema = excludeExampleFromDepiction(
816✔
819
    walkSchema({
820
      schema,
821
      isResponse: true,
822
      rules: depicters,
823
      onEach,
824
      onMissing,
825
      serializer,
826
      getRef,
827
      makeRef,
828
      path,
829
      method,
830
    })
831
  );
832
  const examples = depictExamples(schema, true);
816✔
833
  const result =
834
    composition === "components"
816✔
835
      ? makeRef(makeCleanId(path, method, clue), depictedSchema)
836
      : depictedSchema;
837

838
  return {
816✔
839
    description: `${method.toUpperCase()} ${path} ${clue}`,
840
    content: mimeTypes.reduce(
841
      (carry, mimeType) => ({
824✔
842
        ...carry,
843
        [mimeType]: { schema: result, ...examples },
844
      }),
845
      {} as ContentObject
846
    ),
847
  };
848
};
849

850
type SecurityHelper<K extends Security["type"]> = (
851
  security: Security & { type: K }
852
) => SecuritySchemeObject;
853

854
const depictBasicSecurity: SecurityHelper<"basic"> = () => ({
168✔
855
  type: "http",
856
  scheme: "basic",
857
});
858
const depictBearerSecurity: SecurityHelper<"bearer"> = ({
168✔
859
  format: bearerFormat,
860
}) => ({
32✔
861
  type: "http",
862
  scheme: "bearer",
863
  ...(bearerFormat && { bearerFormat }),
40✔
864
});
865
// @todo add description on actual input placement
866
const depictInputSecurity: SecurityHelper<"input"> = ({ name }) => ({
168✔
867
  type: "apiKey",
868
  in: "query", // body is not supported yet, https://swagger.io/docs/specification/authentication/api-keys/
869
  name,
870
});
871
const depictHeaderSecurity: SecurityHelper<"header"> = ({ name }) => ({
168✔
872
  type: "apiKey",
873
  in: "header",
874
  name,
875
});
876
const depictCookieSecurity: SecurityHelper<"cookie"> = ({ name }) => ({
168✔
877
  type: "apiKey",
878
  in: "cookie",
879
  name,
880
});
881
const depictOpenIdSecurity: SecurityHelper<"openid"> = ({
168✔
882
  url: openIdConnectUrl,
883
}) => ({
8✔
884
  type: "openIdConnect",
885
  openIdConnectUrl,
886
});
887
const depictOAuth2Security: SecurityHelper<"oauth2"> = ({ flows = {} }) => ({
168✔
888
  type: "oauth2",
889
  flows: (
890
    Object.keys(flows) as (keyof typeof flows)[]
891
  ).reduce<OAuthFlowsObject>((acc, key) => {
892
    const flow = flows[key];
56✔
893
    if (!flow) {
56✔
894
      return acc;
16✔
895
    }
896
    const { scopes = {}, ...rest } = flow;
40!
897
    return { ...acc, [key]: { ...rest, scopes } };
40✔
898
  }, {}),
899
});
900

901
export const depictSecurity = (
392✔
902
  container: LogicalContainer<Security>
903
): LogicalContainer<SecuritySchemeObject> => {
904
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
392✔
905
    basic: depictBasicSecurity,
906
    bearer: depictBearerSecurity,
907
    input: depictInputSecurity,
908
    header: depictHeaderSecurity,
909
    cookie: depictCookieSecurity,
910
    openid: depictOpenIdSecurity,
911
    oauth2: depictOAuth2Security,
912
  };
913
  return mapLogicalContainer(container, (security) =>
392✔
914
    (methods[security.type] as SecurityHelper<typeof security.type>)(security)
144✔
915
  );
916
};
917

918
export const depictSecurityRefs = (
400✔
919
  container: LogicalContainer<{ name: string; scopes: string[] }>
920
): SecurityRequirementObject[] => {
921
  if (typeof container === "object") {
768✔
922
    if ("or" in container) {
768✔
923
      return container.or.map((entry) =>
400✔
924
        ("and" in entry
144✔
925
          ? entry.and
926
          : [entry]
927
        ).reduce<SecurityRequirementObject>(
928
          (agg, { name, scopes }) => ({
208✔
929
            ...agg,
930
            [name]: scopes,
931
          }),
932
          {}
933
        )
934
      );
935
    }
936
    if ("and" in container) {
368✔
937
      return depictSecurityRefs(andToOr(container));
360✔
938
    }
939
  }
940
  return depictSecurityRefs({ or: [container] });
8✔
941
};
942

943
export const depictRequest = ({
240✔
944
  method,
945
  path,
946
  endpoint,
947
  serializer,
948
  getRef,
949
  makeRef,
950
  composition,
951
  clue = "request body",
240✔
952
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
953
  const pathParams = getRoutePathParams(path);
240✔
954
  const bodyDepiction = excludeExampleFromDepiction(
240✔
955
    excludeParamsFromDepiction(
956
      walkSchema({
957
        schema: endpoint.getSchema("input"),
958
        isResponse: false,
959
        rules: depicters,
960
        onEach,
961
        onMissing,
962
        serializer,
963
        getRef,
964
        makeRef,
965
        path,
966
        method,
967
      }),
968
      pathParams
969
    )
970
  );
971
  const bodyExamples = depictExamples(
184✔
972
    endpoint.getSchema("input"),
973
    false,
974
    pathParams
975
  );
976
  const result =
977
    composition === "components"
184✔
978
      ? makeRef(makeCleanId(path, method, clue), bodyDepiction)
979
      : bodyDepiction;
980

981
  return {
184✔
982
    description: `${method.toUpperCase()} ${path} ${clue}`,
983
    content: endpoint.getMimeTypes("input").reduce(
984
      (carry, mimeType) => ({
184✔
985
        ...carry,
986
        [mimeType]: { schema: result, ...bodyExamples },
987
      }),
988
      {} as ContentObject
989
    ),
990
  };
991
};
992

993
export const depictTags = <TAG extends string>(
168✔
994
  tags: TagsConfig<TAG>
995
): TagObject[] =>
996
  (Object.keys(tags) as TAG[]).map((tag) => {
32✔
997
    const def = tags[tag];
64✔
998
    return {
64✔
999
      name: tag,
1000
      description: typeof def === "string" ? def : def.description,
64✔
1001
      ...(typeof def === "object" &&
88✔
1002
        def.url && { externalDocs: { url: def.url } }),
1003
    };
1004
  });
1005

1006
export const ensureShortDescription = (description: string) => {
168✔
1007
  if (description.length <= shortDescriptionLimit) {
128✔
1008
    return description;
104✔
1009
  }
1010
  return description.slice(0, shortDescriptionLimit - 1) + "…";
24✔
1011
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc