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

RobinTail / express-zod-api / 3733855692

pending completion
3733855692

Pull #752

github

GitHub
Merge 82de5eda7 into f8fe2a03a
Pull Request #752: Bump @typescript-eslint/parser from 5.46.1 to 5.47.0

413 of 440 branches covered (93.86%)

969 of 969 relevant lines covered (100.0%)

325.18 hits per line

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

96.38
/src/open-api-helpers.ts
1
import {
2
  ContentObject,
3
  ExampleObject,
4
  ExamplesObject,
5
  MediaTypeObject,
6
  OAuthFlowsObject,
7
  ParameterObject,
8
  RequestBodyObject,
9
  ResponseObject,
10
  SchemaObject,
11
  SecurityRequirementObject,
12
  SecuritySchemeObject,
13
  TagObject,
14
} from "openapi3-ts";
15
import { omit } from "ramda";
176✔
16
import { z } from "zod";
176✔
17
import {
176✔
18
  ArrayElement,
19
  getExamples,
20
  getRoutePathParams,
21
  hasTopLevelTransformingEffect,
22
  routePathParamsRegex,
23
} from "./common-helpers";
24
import { InputSources, TagsConfig } from "./config-type";
25
import { ZodDateIn, ZodDateInDef, isoDateRegex } from "./date-in-schema";
176✔
26
import { ZodDateOut, ZodDateOutDef } from "./date-out-schema";
27
import { AbstractEndpoint } from "./endpoint";
28
import { OpenAPIError } from "./errors";
176✔
29
import { ZodFile, ZodFileDef } from "./file-schema";
30
import { IOSchema } from "./io-schema";
31
import {
176✔
32
  LogicalContainer,
33
  andToOr,
34
  mapLogicalContainer,
35
} from "./logical-container";
36
import { copyMeta } from "./metadata";
176✔
37
import { Method } from "./method";
38
import { Security } from "./security";
39
import { ZodUpload, ZodUploadDef } from "./upload-schema";
40

41
type MediaExamples = Pick<MediaTypeObject, "examples">;
42

43
type DepictHelper<T extends z.ZodType<any>> = (params: {
44
  schema: T;
45
  initial?: SchemaObject;
46
  isResponse: boolean;
47
}) => SchemaObject;
48

49
type DepictingRules = Partial<
50
  Record<
51
    | z.ZodFirstPartyTypeKind
52
    | ZodFileDef["typeName"]
53
    | ZodUploadDef["typeName"]
54
    | ZodDateInDef["typeName"]
55
    | ZodDateOutDef["typeName"],
56
    DepictHelper<any>
57
  >
58
>;
59

60
interface ReqResDepictHelperCommonProps {
61
  method: Method;
62
  path: string;
63
  endpoint: AbstractEndpoint;
64
}
65

66
const shortDescriptionLimit = 50;
176✔
67
const isoDateDocumentationUrl =
68
  "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString";
176✔
69

70
/* eslint-disable @typescript-eslint/no-use-before-define */
71

72
export const reformatParamsInPath = (path: string) =>
176✔
73
  path.replace(routePathParamsRegex, (param) => `{${param.slice(1)}}`);
280✔
74

75
export const depictDefault: DepictHelper<z.ZodDefault<z.ZodTypeAny>> = ({
176✔
76
  schema: {
77
    _def: { innerType, defaultValue },
78
  },
79
  initial,
80
  isResponse,
81
}) => ({
24✔
82
  ...initial,
83
  ...depictSchema({ schema: innerType, initial, isResponse }),
84
  default: defaultValue(),
85
});
86

87
export const depictCatch: DepictHelper<z.ZodCatch<z.ZodTypeAny>> = ({
176✔
88
  schema: {
89
    _def: { innerType },
90
  },
91
  initial,
92
  isResponse,
93
}) => ({
8✔
94
  ...initial,
95
  ...depictSchema({ schema: innerType, initial, isResponse }),
96
});
97

98
export const depictAny: DepictHelper<z.ZodAny> = ({ initial }) => ({
176✔
99
  ...initial,
100
  format: "any",
101
});
102

103
export const depictUpload: DepictHelper<ZodUpload> = ({
176✔
104
  initial,
105
  isResponse,
106
}) => {
107
  if (isResponse) {
24✔
108
    throw new OpenAPIError("Please use z.upload() only for input.");
8✔
109
  }
110
  return {
16✔
111
    ...initial,
112
    type: "string",
113
    format: "binary",
114
  };
115
};
116

117
export const depictFile: DepictHelper<ZodFile> = ({
176✔
118
  schema: { isBinary, isBase64 },
119
  initial,
120
  isResponse,
121
}) => {
122
  if (!isResponse) {
40✔
123
    throw new OpenAPIError("Please use z.file() only within ResultHandler.");
8✔
124
  }
125
  return {
32✔
126
    ...initial,
127
    type: "string",
128
    format: isBinary ? "binary" : isBase64 ? "byte" : "file",
48✔
129
  };
130
};
131

132
export const depictUnion: DepictHelper<
176✔
133
  z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>
134
> = ({ schema: { options }, initial, isResponse }) => ({
176✔
135
  ...initial,
136
  oneOf: options.map((option) => depictSchema({ schema: option, isResponse })),
144✔
137
});
138

139
export const depictDiscriminatedUnion: DepictHelper<
176✔
140
  z.ZodDiscriminatedUnion<string, z.ZodObject<any>[]>
141
> = ({ schema: { options, discriminator }, initial, isResponse }) => {
176✔
142
  return {
24✔
143
    ...initial,
144
    discriminator: {
145
      propertyName: discriminator,
146
    },
147
    oneOf: Array.from(options.values()).map((option) =>
148
      depictSchema({ schema: option, isResponse })
48✔
149
    ),
150
  };
151
};
152

153
export const depictIntersection: DepictHelper<
176✔
154
  z.ZodIntersection<z.ZodTypeAny, z.ZodTypeAny>
155
> = ({
176✔
156
  schema: {
157
    _def: { left, right },
158
  },
159
  initial,
160
  isResponse,
161
}) => ({
56✔
162
  ...initial,
163
  allOf: [
164
    depictSchema({ schema: left, isResponse }),
165
    depictSchema({ schema: right, isResponse }),
166
  ],
167
});
168

169
export const depictOptional: DepictHelper<z.ZodOptional<any>> = ({
176✔
170
  schema,
171
  initial,
172
  isResponse,
173
}) => ({
56✔
174
  ...initial,
175
  ...depictSchema({ schema: schema.unwrap(), isResponse }),
176
});
177

178
export const depictNullable: DepictHelper<z.ZodNullable<any>> = ({
176✔
179
  schema,
180
  initial,
181
  isResponse,
182
}) => ({
48✔
183
  ...initial,
184
  nullable: true,
185
  ...depictSchema({ schema: schema.unwrap(), isResponse }),
186
});
187

188
export const depictEnum: DepictHelper<
176✔
189
  z.ZodEnum<any> | z.ZodNativeEnum<any>
190
> = ({
176✔
191
  schema: {
192
    _def: { values },
193
  },
194
  initial,
195
}) => ({
32✔
196
  ...initial,
197
  type: typeof Object.values(values)[0] as "string" | "number",
198
  enum: Object.values(values),
199
});
200

201
export const depictLiteral: DepictHelper<z.ZodLiteral<any>> = ({
176✔
202
  schema: {
203
    _def: { value },
204
  },
205
  initial,
206
}) => ({
712✔
207
  ...initial,
208
  type: typeof value as "string" | "number" | "boolean",
209
  enum: [value],
210
});
211

212
export const depictObject: DepictHelper<z.AnyZodObject> = ({
176✔
213
  schema,
214
  initial,
215
  isResponse,
216
}) => ({
1,616✔
217
  ...initial,
218
  type: "object",
219
  properties: depictObjectProperties({ schema, isResponse }),
220
  required: Object.keys(schema.shape).filter((key) => {
221
    const prop = schema.shape[key];
2,464✔
222
    return isResponse && hasCoercion(prop)
2,464✔
223
      ? prop instanceof z.ZodOptional
2,464✔
224
      : !prop.isOptional();
225
  }),
226
});
227

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

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

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

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

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

292
export const depictBigInt: DepictHelper<z.ZodBigInt> = ({ initial }) => ({
176✔
293
  ...initial,
294
  type: "integer",
295
  format: "bigint",
296
});
297

298
export const depictRecord: DepictHelper<z.ZodRecord<z.ZodTypeAny>> = ({
176✔
299
  schema: { _def: def },
300
  initial,
301
  isResponse,
302
}) => {
303
  if (
96✔
304
    def.keyType instanceof z.ZodEnum ||
176✔
305
    def.keyType instanceof z.ZodNativeEnum
306
  ) {
307
    const keys = Object.values(def.keyType._def.values) as string[];
16✔
308
    const shape = keys.reduce(
16✔
309
      (carry, key) => ({
32✔
310
        ...carry,
311
        [key]: def.valueType,
312
      }),
313
      {} as z.ZodRawShape
314
    );
315
    return {
16✔
316
      ...initial,
317
      type: "object",
318
      properties: depictObjectProperties({
319
        schema: z.object(shape),
320
        isResponse,
321
      }),
322
      required: keys,
323
    };
324
  }
325
  if (def.keyType instanceof z.ZodLiteral) {
80✔
326
    return {
16✔
327
      ...initial,
328
      type: "object",
329
      properties: depictObjectProperties({
330
        schema: z.object({
331
          [def.keyType._def.value]: def.valueType,
332
        }),
333
        isResponse,
334
      }),
335
      required: [def.keyType._def.value],
336
    };
337
  }
338
  if (def.keyType instanceof z.ZodUnion) {
64✔
339
    const areOptionsLiteral = def.keyType.options.reduce(
16✔
340
      (carry: boolean, option: z.ZodTypeAny) =>
341
        carry && option instanceof z.ZodLiteral,
32✔
342
      true
343
    );
344
    if (areOptionsLiteral) {
16✔
345
      const shape = def.keyType.options.reduce(
16✔
346
        (carry: z.ZodRawShape, option: z.ZodLiteral<any>) => ({
32✔
347
          ...carry,
348
          [option.value]: def.valueType,
349
        }),
350
        {} as z.ZodRawShape
351
      );
352
      return {
16✔
353
        ...initial,
354
        type: "object",
355
        properties: depictObjectProperties({
356
          schema: z.object(shape),
357
          isResponse,
358
        }),
359
        required: def.keyType.options.map(
360
          (option: z.ZodLiteral<any>) => option.value
32✔
361
        ),
362
      };
363
    }
364
  }
365
  return {
48✔
366
    ...initial,
367
    type: "object",
368
    additionalProperties: depictSchema({ schema: def.valueType, isResponse }),
369
  };
370
};
371

372
export const depictArray: DepictHelper<z.ZodArray<z.ZodTypeAny>> = ({
176✔
373
  schema: { _def: def, element },
374
  initial,
375
  isResponse,
376
}) => ({
56✔
377
  ...initial,
378
  type: "array",
379
  items: depictSchema({ schema: element, isResponse }),
380
  ...(def.minLength ? { minItems: def.minLength.value } : {}),
56✔
381
  ...(def.maxLength ? { maxItems: def.maxLength.value } : {}),
56✔
382
});
383

384
/** @todo improve it when OpenAPI 3.1.0 will be released */
385
export const depictTuple: DepictHelper<z.ZodTuple> = ({
176✔
386
  schema: { items },
387
  initial,
388
  isResponse,
389
}) => {
390
  const types = items.map((item) => depictSchema({ schema: item, isResponse }));
72✔
391
  return {
40✔
392
    ...initial,
393
    type: "array",
394
    minItems: types.length,
395
    maxItems: types.length,
396
    items: {
397
      oneOf: types,
398
      format: "tuple",
399
      ...(types.length === 0
400
        ? {}
40✔
401
        : {
402
            description: types
403
              .map((item, index) => `${index}: ${item.type}`)
72✔
404
              .join(", "),
405
          }),
406
    },
407
  };
408
};
409

410
export const depictString: DepictHelper<z.ZodString> = ({
176✔
411
  schema: {
412
    isEmail,
413
    isURL,
414
    minLength,
415
    maxLength,
416
    isUUID,
417
    isCUID,
418
    isDatetime,
419
    _def: { checks },
420
  },
421
  initial,
422
}) => {
423
  const regexCheck = checks.find(
1,080✔
424
    (check): check is z.ZodStringCheck & { kind: "regex" } =>
425
      check.kind === "regex"
240✔
426
  );
427
  const datetimeCheck = checks.find(
1,080✔
428
    (check): check is z.ZodStringCheck & { kind: "datetime" } =>
429
      check.kind === "datetime"
248✔
430
  );
431
  const regex = regexCheck
1,080✔
432
    ? regexCheck.regex
1,080✔
433
    : datetimeCheck
434
    ? datetimeCheck.offset
1,032✔
435
      ? new RegExp(
16✔
436
          `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}:\\d{2})|Z)$`
437
        )
438
      : new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$`)
439
    : undefined;
440
  return {
1,080✔
441
    ...initial,
442
    type: "string" as const,
443
    ...(isDatetime ? { format: "date-time" } : {}),
1,080✔
444
    ...(isEmail ? { format: "email" } : {}),
1,080✔
445
    ...(isURL ? { format: "url" } : {}),
1,080✔
446
    ...(isUUID ? { format: "uuid" } : {}),
1,080✔
447
    ...(isCUID ? { format: "cuid" } : {}),
1,080✔
448
    ...(minLength ? { minLength } : {}),
1,080✔
449
    ...(maxLength ? { maxLength } : {}),
1,080✔
450
    ...(regex ? { pattern: `/${regex.source}/${regex.flags}` } : {}),
1,080✔
451
  };
452
};
453

454
/** @todo support exclusive min/max as numbers in case of OpenAPI v3.1.x */
455
export const depictNumber: DepictHelper<z.ZodNumber> = ({
176✔
456
  schema,
457
  initial,
458
}) => {
459
  const minCheck = schema._def.checks.find(({ kind }) => kind === "min") as
312✔
460
    | Extract<ArrayElement<z.ZodNumberDef["checks"]>, { kind: "min" }>
461
    | undefined;
462
  const isMinInclusive = minCheck ? minCheck.inclusive : true;
288✔
463
  const maxCheck = schema._def.checks.find(({ kind }) => kind === "max") as
344✔
464
    | Extract<ArrayElement<z.ZodNumberDef["checks"]>, { kind: "max" }>
465
    | undefined;
466
  const isMaxInclusive = maxCheck ? maxCheck.inclusive : true;
288✔
467
  return {
288✔
468
    ...initial,
469
    type: schema.isInt ? ("integer" as const) : ("number" as const),
288✔
470
    format: schema.isInt ? ("int64" as const) : ("double" as const),
288✔
471
    minimum:
472
      schema.minValue === null
473
        ? schema.isInt
288✔
474
          ? Number.MIN_SAFE_INTEGER
152✔
475
          : Number.MIN_VALUE
476
        : schema.minValue,
477
    exclusiveMinimum: !isMinInclusive,
478
    maximum:
479
      schema.maxValue === null
480
        ? schema.isInt
288✔
481
          ? Number.MAX_SAFE_INTEGER
232✔
482
          : Number.MAX_VALUE
483
        : schema.maxValue,
484
    exclusiveMaximum: !isMaxInclusive,
485
  };
486
};
487

488
export const depictObjectProperties = ({
176✔
489
  schema: { shape },
490
  isResponse,
491
}: Parameters<DepictHelper<z.AnyZodObject>>[0]) => {
492
  return Object.keys(shape).reduce(
1,672✔
493
    (carry, key) => ({
2,624✔
494
      ...carry,
495
      [key]: depictSchema({ schema: shape[key], isResponse }),
496
    }),
497
    {} as Record<string, SchemaObject>
498
  );
499
};
500

501
export const depictEffect: DepictHelper<z.ZodEffects<z.ZodTypeAny>> = ({
176✔
502
  schema,
503
  initial,
504
  isResponse,
505
}) => {
506
  const input = depictSchema({ schema: schema._def.schema, isResponse });
184✔
507
  const effect = schema._def.effect;
184✔
508
  if (isResponse && effect && effect.type === "transform") {
184✔
509
    let output = "undefined";
40✔
510
    try {
40✔
511
      output = typeof effect.transform(
40✔
512
        ["integer", "number"].includes(`${input.type}`)
513
          ? 0
40✔
514
          : "string" === input.type
515
          ? ""
16!
516
          : "boolean" === input.type
517
          ? false
×
518
          : "object" === input.type
519
          ? {}
×
520
          : "null" === input.type
521
          ? null
×
522
          : "array" === input.type
523
          ? []
×
524
          : undefined,
525
        { addIssue: () => {}, path: [] }
526
      );
527
    } catch (e) {
528
      /**/
529
    }
530
    return {
40✔
531
      ...initial,
532
      ...input,
533
      ...(["number", "string", "boolean"].includes(output)
534
        ? {
40!
535
            type: output as "number" | "string" | "boolean",
536
          }
537
        : {}),
538
    };
539
  }
540
  if (!isResponse && effect && effect.type === "preprocess") {
144✔
541
    const { type: inputType, ...rest } = input;
24✔
542
    return {
24✔
543
      ...initial,
544
      ...rest,
545
      format: `${rest.format || inputType} (preprocessed)`,
40✔
546
    };
547
  }
548
  return { ...initial, ...input };
120✔
549
};
550

551
export const depictPipeline: DepictHelper<z.ZodPipeline<any, any>> = ({
176✔
552
  schema,
553
  initial,
554
  isResponse,
555
}) =>
556
  depictSchema({
16✔
557
    schema: schema._def[isResponse ? "out" : "in"],
16✔
558
    isResponse,
559
    initial,
560
  });
561

562
export const depictBranded: DepictHelper<z.ZodBranded<z.ZodTypeAny, any>> = ({
176✔
563
  schema,
564
  initial,
565
  isResponse,
566
}) => depictSchema({ schema: schema.unwrap(), isResponse, initial });
8✔
567

568
export const depictIOExamples = <T extends IOSchema>(
176✔
569
  schema: T,
570
  isResponse: boolean,
571
  omitProps: string[] = []
624✔
572
): MediaExamples => {
573
  const examples = getExamples(schema, isResponse);
784✔
574
  if (examples.length === 0) {
784✔
575
    return {};
416✔
576
  }
577
  return {
368✔
578
    examples: examples.reduce<ExamplesObject>(
579
      (carry, example, index) => ({
384✔
580
        ...carry,
581
        [`example${index + 1}`]: <ExampleObject>{
582
          value: omit(omitProps, example),
583
        },
584
      }),
585
      {}
586
    ),
587
  };
588
};
589

590
export const depictIOParamExamples = <T extends IOSchema>(
176✔
591
  schema: T,
592
  isResponse: boolean,
593
  param: string
594
): MediaExamples => {
595
  const examples = getExamples(schema, isResponse);
224✔
596
  if (examples.length === 0) {
224✔
597
    return {};
192✔
598
  }
599
  return {
32✔
600
    examples: examples.reduce<ExamplesObject>(
601
      (carry, example, index) =>
602
        param in example
48✔
603
          ? {
48!
604
              ...carry,
605
              [`example${index + 1}`]: <ExampleObject>{
606
                value: example[param],
607
              },
608
            }
609
          : carry,
610
      {}
611
    ),
612
  };
613
};
614

615
export function extractObjectSchema(subject: IOSchema) {
176✔
616
  if (subject instanceof z.ZodObject) {
600✔
617
    return subject;
488✔
618
  }
619
  let objectSchema: z.AnyZodObject;
620
  if (
112✔
621
    subject instanceof z.ZodUnion ||
200✔
622
    subject instanceof z.ZodDiscriminatedUnion
623
  ) {
624
    objectSchema = Array.from(subject.options.values())
32✔
625
      .map((option) => extractObjectSchema(option))
64✔
626
      .reduce((acc, option) => acc.merge(option.partial()), z.object({}));
64✔
627
  } else if (subject instanceof z.ZodEffects) {
80✔
628
    if (hasTopLevelTransformingEffect(subject)) {
24✔
629
      throw new OpenAPIError(
8✔
630
        "Using transformations on the top level of input schema is not allowed."
631
      );
632
    }
633
    objectSchema = extractObjectSchema(subject._def.schema); // object refinement
16✔
634
  } else {
635
    // intersection
636
    objectSchema = extractObjectSchema(subject._def.left).merge(
56✔
637
      extractObjectSchema(subject._def.right)
638
    );
639
  }
640
  return copyMeta(subject, objectSchema);
104✔
641
}
642

643
export const depictRequestParams = ({
176✔
644
  path,
645
  method,
646
  endpoint,
647
  inputSources,
648
}: ReqResDepictHelperCommonProps & {
649
  inputSources: InputSources[Method];
650
}): ParameterObject[] => {
651
  const schema = endpoint.getInputSchema();
336✔
652
  const shape = extractObjectSchema(schema).shape;
336✔
653
  const pathParams = getRoutePathParams(path);
336✔
654
  const isQueryEnabled = inputSources.includes("query");
336✔
655
  const isParamsEnabled = inputSources.includes("params");
336✔
656
  const isPathParam = (name: string) =>
336✔
657
    isParamsEnabled && pathParams.includes(name);
680✔
658
  return Object.keys(shape)
336✔
659
    .filter((name) => isQueryEnabled || isPathParam(name))
656✔
660
    .map((name) => ({
208✔
661
      name,
662
      in: isPathParam(name) ? "path" : "query",
208✔
663
      required: !shape[name].isOptional(),
664
      schema: {
665
        description: `${method.toUpperCase()} ${path} parameter`,
666
        ...depictSchema({ schema: shape[name], isResponse: false }),
667
      },
668
      ...depictIOParamExamples(schema, false, name),
669
    }));
670
};
671

672
const depictHelpers: DepictingRules = {
176✔
673
  ZodString: depictString,
674
  ZodNumber: depictNumber,
675
  ZodBigInt: depictBigInt,
676
  ZodBoolean: depictBoolean,
677
  ZodDateIn: depictDateIn,
678
  ZodDateOut: depictDateOut,
679
  ZodNull: depictNull,
680
  ZodArray: depictArray,
681
  ZodTuple: depictTuple,
682
  ZodRecord: depictRecord,
683
  ZodObject: depictObject,
684
  ZodLiteral: depictLiteral,
685
  ZodIntersection: depictIntersection,
686
  ZodUnion: depictUnion,
687
  ZodFile: depictFile,
688
  ZodUpload: depictUpload,
689
  ZodAny: depictAny,
690
  ZodDefault: depictDefault,
691
  ZodEnum: depictEnum,
692
  ZodNativeEnum: depictEnum,
693
  ZodEffects: depictEffect,
694
  ZodOptional: depictOptional,
695
  ZodNullable: depictNullable,
696
  ZodDiscriminatedUnion: depictDiscriminatedUnion,
697
  ZodBranded: depictBranded,
698
  ZodDate: depictDate,
699
  ZodCatch: depictCatch,
700
  ZodPipeline: depictPipeline,
701
};
702

703
/**
704
 * @desc isNullable() and isOptional() validate the schema's input
705
 * @desc They always return true in case of coercion, which should be taken into account when depicting response
706
 */
707
export const hasCoercion = (schema: z.ZodType): boolean =>
176✔
708
  "coerce" in schema._def && typeof schema._def.coerce === "boolean"
2,000✔
709
    ? schema._def.coerce
2,000✔
710
    : false;
711

712
export const depictSchema: DepictHelper<z.ZodTypeAny> = ({
176✔
713
  schema,
714
  isResponse,
715
}) => {
716
  const initial: SchemaObject = {};
4,512✔
717
  if (schema.isNullable()) {
4,512✔
718
    if (!(isResponse && hasCoercion(schema))) {
144✔
719
      initial.nullable = true;
120✔
720
    }
721
  }
722
  if (schema.description) {
4,512✔
723
    initial.description = `${schema.description}`;
24✔
724
  }
725
  const examples = getExamples(schema, isResponse);
4,512✔
726
  if (examples.length > 0) {
4,512✔
727
    initial.example = examples[0];
432✔
728
  }
729
  const nextHelper =
730
    "typeName" in schema._def
4,512✔
731
      ? depictHelpers[schema._def.typeName as keyof typeof depictHelpers]
4,512!
732
      : null;
733
  if (!nextHelper) {
4,512✔
734
    throw new OpenAPIError(
64✔
735
      `Zod type ${schema.constructor.name} is unsupported`
736
    );
737
  }
738
  return nextHelper({ schema, initial, isResponse });
4,448✔
739
};
740

741
export const excludeParamsFromDepiction = (
176✔
742
  depicted: SchemaObject,
743
  pathParams: string[]
744
): SchemaObject => {
745
  const properties = depicted.properties
280✔
746
    ? omit(pathParams, depicted.properties)
280✔
747
    : undefined;
748
  const example = depicted.example
280✔
749
    ? omit(pathParams, depicted.example)
280✔
750
    : undefined;
751
  const required = depicted.required
280✔
752
    ? depicted.required.filter((name) => !pathParams.includes(name))
432✔
753
    : undefined;
754
  const allOf = depicted.allOf
280✔
755
    ? (depicted.allOf as SchemaObject[]).map((entry) =>
280✔
756
        excludeParamsFromDepiction(entry, pathParams)
64✔
757
      )
758
    : undefined;
759
  const oneOf = depicted.oneOf
280✔
760
    ? (depicted.oneOf as SchemaObject[]).map((entry) =>
280✔
761
        excludeParamsFromDepiction(entry, pathParams)
48✔
762
      )
763
    : undefined;
764

765
  return omit(
280✔
766
    Object.entries({ properties, required, example, allOf, oneOf })
767
      .filter(([{}, value]) => value === undefined)
1,400✔
768
      .map(([key]) => key),
840✔
769
    {
770
      ...depicted,
771
      properties,
772
      required,
773
      example,
774
      allOf,
775
      oneOf,
776
    }
777
  );
778
};
779

780
export const excludeExampleFromDepiction = (
176✔
781
  depicted: SchemaObject
782
): SchemaObject => omit(["example"], depicted);
776✔
783

784
export const depictResponse = ({
176✔
785
  method,
786
  path,
787
  description,
788
  endpoint,
789
  isPositive,
790
}: ReqResDepictHelperCommonProps & {
791
  description: string;
792
  isPositive: boolean;
793
}): ResponseObject => {
794
  const schema = isPositive
624✔
795
    ? endpoint.getPositiveResponseSchema()
624✔
796
    : endpoint.getNegativeResponseSchema();
797
  const mimeTypes = isPositive
624✔
798
    ? endpoint.getPositiveMimeTypes()
624✔
799
    : endpoint.getNegativeMimeTypes();
800
  const depictedSchema = excludeExampleFromDepiction(
624✔
801
    depictSchema({
802
      schema,
803
      isResponse: true,
804
    })
805
  );
806
  const examples = depictIOExamples(schema, true);
624✔
807

808
  return {
624✔
809
    description: `${method.toUpperCase()} ${path} ${description}`,
810
    content: mimeTypes.reduce(
811
      (carry, mimeType) => ({
624✔
812
        ...carry,
813
        [mimeType]: {
814
          schema: depictedSchema,
815
          ...examples,
816
        },
817
      }),
818
      {} as ContentObject
819
    ),
820
  };
821
};
822

823
type SecurityHelper<K extends Security["type"]> = (
824
  security: Security & { type: K }
825
) => SecuritySchemeObject;
826

827
const depictBasicSecurity: SecurityHelper<"basic"> = ({}) => ({
176✔
828
  type: "http",
829
  scheme: "basic",
830
});
831
const depictBearerSecurity: SecurityHelper<"bearer"> = ({
176✔
832
  format: bearerFormat,
833
}) => ({
24✔
834
  type: "http",
835
  scheme: "bearer",
836
  ...(bearerFormat ? { bearerFormat } : {}),
24!
837
});
838
// @todo add description on actual input placement
839
const depictInputSecurity: SecurityHelper<"input"> = ({ name }) => ({
176✔
840
  type: "apiKey",
841
  in: "query", // body is not supported yet, https://swagger.io/docs/specification/authentication/api-keys/
842
  name,
843
});
844
const depictHeaderSecurity: SecurityHelper<"header"> = ({ name }) => ({
176✔
845
  type: "apiKey",
846
  in: "header",
847
  name,
848
});
849
const depictCookieSecurity: SecurityHelper<"cookie"> = ({ name }) => ({
176✔
850
  type: "apiKey",
851
  in: "cookie",
852
  name,
853
});
854
const depictOpenIdSecurity: SecurityHelper<"openid"> = ({
176✔
855
  url: openIdConnectUrl,
856
}) => ({
8✔
857
  type: "openIdConnect",
858
  openIdConnectUrl,
859
});
860
const depictOAuth2Security: SecurityHelper<"oauth2"> = ({ flows = {} }) => ({
176✔
861
  type: "oauth2",
862
  flows: (
863
    Object.keys(flows) as (keyof typeof flows)[]
864
  ).reduce<OAuthFlowsObject>((acc, key) => {
865
    const flow = flows[key];
56✔
866
    if (!flow) {
56✔
867
      return acc;
16✔
868
    }
869
    const { scopes = {}, ...rest } = flow;
40!
870
    return { ...acc, [key]: { ...rest, scopes } };
40✔
871
  }, {}),
872
});
873

874
export const depictSecurity = (
176✔
875
  container: LogicalContainer<Security>
876
): LogicalContainer<SecuritySchemeObject> => {
877
  const methods: { [K in Security["type"]]: SecurityHelper<K> } = {
288✔
878
    basic: depictBasicSecurity,
879
    bearer: depictBearerSecurity,
880
    input: depictInputSecurity,
881
    header: depictHeaderSecurity,
882
    cookie: depictCookieSecurity,
883
    openid: depictOpenIdSecurity,
884
    oauth2: depictOAuth2Security,
885
  };
886
  return mapLogicalContainer(container, (security) =>
288✔
887
    (methods[security.type] as SecurityHelper<typeof security.type>)(security)
120✔
888
  );
889
};
890

891
export const depictSecurityRefs = (
176✔
892
  container: LogicalContainer<{ name: string; scopes: string[] }>
893
): SecurityRequirementObject[] => {
894
  if (typeof container === "object") {
560✔
895
    if ("or" in container) {
560✔
896
      return container.or.map((entry) =>
296✔
897
        ("and" in entry
128✔
898
          ? entry.and
128✔
899
          : [entry]
900
        ).reduce<SecurityRequirementObject>(
901
          (agg, { name, scopes }) => ({
184✔
902
            ...agg,
903
            [name]: scopes,
904
          }),
905
          {}
906
        )
907
      );
908
    }
909
    if ("and" in container) {
264✔
910
      return depictSecurityRefs(andToOr(container));
256✔
911
    }
912
  }
913
  return depictSecurityRefs({ or: [container] });
8✔
914
};
915

916
export const depictRequest = ({
176✔
917
  method,
918
  path,
919
  endpoint,
920
}: ReqResDepictHelperCommonProps): RequestBodyObject => {
921
  const pathParams = getRoutePathParams(path);
208✔
922
  const bodyDepiction = excludeExampleFromDepiction(
208✔
923
    excludeParamsFromDepiction(
924
      depictSchema({
925
        schema: endpoint.getInputSchema(),
926
        isResponse: false,
927
      }),
928
      pathParams
929
    )
930
  );
931
  const bodyExamples = depictIOExamples(
144✔
932
    endpoint.getInputSchema(),
933
    false,
934
    pathParams
935
  );
936

937
  return {
144✔
938
    content: endpoint.getInputMimeTypes().reduce(
939
      (carry, mimeType) => ({
144✔
940
        ...carry,
941
        [mimeType]: {
942
          schema: {
943
            description: `${method.toUpperCase()} ${path} request body`,
944
            ...bodyDepiction,
945
          },
946
          ...bodyExamples,
947
        },
948
      }),
949
      {} as ContentObject
950
    ),
951
  };
952
};
953

954
export const depictTags = <TAG extends string>(
176✔
955
  tags: TagsConfig<TAG>
956
): TagObject[] =>
957
  (Object.keys(tags) as TAG[]).map((tag) => {
24✔
958
    const def = tags[tag];
48✔
959
    return {
48✔
960
      name: tag,
961
      description: typeof def === "string" ? def : def.description,
48✔
962
      ...(typeof def === "object" && def.url
64✔
963
        ? { externalDocs: { url: def.url } }
48✔
964
        : {}),
965
    };
966
  });
967

968
export const ensureShortDescription = (description: string) => {
176✔
969
  if (description.length <= shortDescriptionLimit) {
72✔
970
    return description;
56✔
971
  }
972
  return description.slice(0, shortDescriptionLimit - 1) + "…";
16✔
973
};
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

© 2026 Coveralls, Inc