11import type {
2+ $ZodObject ,
23 $ZodPipe ,
34 $ZodTransform ,
45 $ZodTuple ,
@@ -44,7 +45,8 @@ import { ezDateOutBrand } from "./date-out-schema";
4445import { contentTypes } from "./content-type" ;
4546import { DocumentationError } from "./errors" ;
4647import { ezFileBrand } from "./file-schema" ;
47- import { extractObjectSchema , IOSchema } from "./io-schema" ;
48+ import { IOSchema } from "./io-schema" ;
49+ import { flattenIO } from "./json-schema-helpers" ;
4850import { Alternatives } from "./logical-container" ;
4951import { metaSymbol } from "./metadata" ;
5052import { Method } from "./method" ;
@@ -80,13 +82,7 @@ export type IsHeader = (
8082
8183export type BrandHandling = Record < string | symbol , Depicter > ;
8284
83- interface ReqResHandlingProps < S extends $ZodType >
84- extends Omit < OpenAPIContext , "isResponse" > {
85- schema : S ;
86- composition : "inline" | "components" ;
87- description ?: string ;
88- brandHandling ?: BrandHandling ;
89- }
85+ type ReqResCommons = Omit < OpenAPIContext , "isResponse" > ;
9086
9187const shortDescriptionLimit = 50 ;
9288const isoDateDocumentationUrl =
@@ -164,6 +160,7 @@ const canMerge = R.pipe(
164160 R . isEmpty ,
165161) ;
166162
163+ /** @todo DNRY with flattenIO() */
167164const intersect = (
168165 children : Array < JSONSchema . BaseSchema > ,
169166) : JSONSchema . ObjectSchema => {
@@ -209,6 +206,21 @@ export const depictLiteral: Depicter = ({ jsonSchema }) => ({
209206 ...jsonSchema ,
210207} ) ;
211208
209+ export const depictObject : Depicter = (
210+ { zodSchema, jsonSchema } ,
211+ { isResponse } ,
212+ ) => {
213+ if ( isResponse ) return jsonSchema ;
214+ if ( ! isSchema < $ZodObject > ( zodSchema , "object" ) ) return jsonSchema ;
215+ const { required = [ ] } = jsonSchema as JSONSchema . ObjectSchema ;
216+ const result : string [ ] = [ ] ;
217+ for ( const key of required ) {
218+ const valueSchema = zodSchema . _zod . def . shape [ key ] ;
219+ if ( valueSchema && ! doesAccept ( valueSchema , undefined ) ) result . push ( key ) ;
220+ }
221+ return { ...jsonSchema , required : result } ;
222+ } ;
223+
212224const ensureCompliance = ( {
213225 $ref,
214226 type,
@@ -306,7 +318,7 @@ export const depictPipeline: Depicter = ({ zodSchema, jsonSchema }, ctx) => {
306318 ctx . isResponse ? "in" : "out"
307319 ] ;
308320 if ( ! isSchema < $ZodTransform > ( target , "transform" ) ) return jsonSchema ;
309- const opposingDepiction = depict ( opposite , { ctx } ) ;
321+ const opposingDepiction = ensureCompliance ( depict ( opposite , { ctx } ) ) ;
310322 if ( isSchemaObject ( opposingDepiction ) ) {
311323 if ( ! ctx . isResponse ) {
312324 const { type : opposingType , ...rest } = opposingDepiction ;
@@ -347,39 +359,6 @@ const enumerateExamples = (examples: unknown[]): ExamplesObject | undefined =>
347359 )
348360 : undefined ;
349361
350- export const depictExamples = (
351- schema : $ZodType ,
352- isResponse : boolean ,
353- omitProps : string [ ] = [ ] ,
354- ) : ExamplesObject | undefined =>
355- R . pipe (
356- getExamples ,
357- R . map (
358- R . when (
359- ( one ) : one is FlatObject => isObject ( one ) && ! Array . isArray ( one ) ,
360- R . omit ( omitProps ) ,
361- ) ,
362- ) ,
363- enumerateExamples ,
364- ) ( {
365- schema,
366- variant : isResponse ? "parsed" : "original" ,
367- validate : true ,
368- pullProps : true ,
369- } ) ;
370-
371- export const depictParamExamples = (
372- schema : z . ZodType ,
373- param : string ,
374- ) : ExamplesObject | undefined => {
375- return R . pipe (
376- getExamples ,
377- R . filter ( R . both ( isObject , R . has ( param ) ) ) ,
378- R . pluck ( param ) ,
379- enumerateExamples ,
380- ) ( { schema, variant : "original" , validate : true , pullProps : true } ) ;
381- } ;
382-
383362export const defaultIsHeader = (
384363 name : string ,
385364 familiar ?: string [ ] ,
@@ -391,20 +370,22 @@ export const defaultIsHeader = (
391370export const depictRequestParams = ( {
392371 path,
393372 method,
394- schema ,
373+ request ,
395374 inputSources,
396375 makeRef,
397376 composition,
398- brandHandling,
399377 isHeader,
400378 security,
401379 description = `${ method . toUpperCase ( ) } ${ path } Parameter` ,
402- } : ReqResHandlingProps < IOSchema > & {
380+ } : ReqResCommons & {
381+ composition : "inline" | "components" ;
382+ description ?: string ;
383+ request : JSONSchema . BaseSchema ;
403384 inputSources : InputSource [ ] ;
404385 isHeader ?: IsHeader ;
405386 security ?: Alternatives < Security > ;
406387} ) => {
407- const objectSchema = extractObjectSchema ( schema ) ;
388+ const flat = flattenIO ( request ) ;
408389 const pathParams = getRoutePathParams ( path ) ;
409390 const isQueryEnabled = inputSources . includes ( "query" ) ;
410391 const areParamsEnabled = inputSources . includes ( "params" ) ;
@@ -419,8 +400,8 @@ export const depictRequestParams = ({
419400 areHeadersEnabled &&
420401 ( isHeader ?.( name , method , path ) ?? defaultIsHeader ( name , securityHeaders ) ) ;
421402
422- return Object . entries ( objectSchema . shape ) . reduce < ParameterObject [ ] > (
423- ( acc , [ name , paramSchema ] ) => {
403+ return Object . entries ( flat . properties ) . reduce < ParameterObject [ ] > (
404+ ( acc , [ name , jsonSchema ] ) => {
424405 const location = isPathParam ( name )
425406 ? "path"
426407 : isHeaderParam ( name )
@@ -429,22 +410,26 @@ export const depictRequestParams = ({
429410 ? "query"
430411 : undefined ;
431412 if ( ! location ) return acc ;
432- const depicted = depict ( paramSchema , {
433- rules : { ...brandHandling , ...depicters } ,
434- ctx : { isResponse : false , makeRef, path, method } ,
435- } ) ;
413+ const depicted = ensureCompliance ( jsonSchema ) ;
436414 const result =
437415 composition === "components"
438- ? makeRef ( paramSchema , depicted , makeCleanId ( description , name ) )
416+ ? makeRef ( jsonSchema , depicted , makeCleanId ( description , name ) )
439417 : depicted ;
440418 return acc . concat ( {
441419 name,
442420 in : location ,
443- deprecated : globalRegistry . get ( paramSchema ) ? .deprecated ,
444- required : ! doesAccept ( paramSchema , undefined ) ,
421+ deprecated : jsonSchema . deprecated ,
422+ required : flat . required . includes ( name ) ,
445423 description : depicted . description || description ,
446424 schema : result ,
447- examples : depictParamExamples ( objectSchema , name ) ,
425+ examples : enumerateExamples (
426+ isSchemaObject ( depicted ) && depicted . examples ?. length
427+ ? depicted . examples // own examples or from the flat:
428+ : R . pluck (
429+ name ,
430+ flat . examples . filter ( R . both ( isObject , R . has ( name ) ) ) ,
431+ ) ,
432+ ) ,
448433 } ) ;
449434 } ,
450435 [ ] ,
@@ -462,6 +447,7 @@ const depicters: Partial<Record<FirstPartyKind | ProprietaryBrand, Depicter>> =
462447 pipe : depictPipeline ,
463448 literal : depictLiteral ,
464449 enum : depictEnum ,
450+ object : depictObject ,
465451 [ ezDateInBrand ] : depictDateIn ,
466452 [ ezDateOutBrand ] : depictDateOut ,
467453 [ ezUploadBrand ] : depictUpload ,
@@ -477,6 +463,7 @@ const onEach: Depicter = ({ zodSchema, jsonSchema }, { isResponse }) => {
477463 schema : zodSchema ,
478464 variant : isResponse ? "parsed" : "original" ,
479465 validate : true ,
466+ pullProps : true ,
480467 } ) ;
481468 if ( examples . length ) result . examples = examples . slice ( ) ;
482469 return result ;
@@ -506,7 +493,7 @@ const fixReferences = (
506493 }
507494 if ( R . is ( Array , entry ) ) stack . push ( ...R . values ( entry ) ) ;
508495 }
509- return ensureCompliance ( subject ) ;
496+ return subject ;
510497} ;
511498
512499/** @link https://github.com/colinhacks/zod/issues/4275 */
@@ -574,11 +561,6 @@ export const excludeParamsFromDepiction = (
574561 return [ result , hasRequired || Boolean ( result . required ?. length ) ] ;
575562} ;
576563
577- export const excludeExamplesFromDepiction = (
578- depicted : SchemaObject | ReferenceObject ,
579- ) : SchemaObject | ReferenceObject =>
580- isReferenceObject ( depicted ) ? depicted : R . omit ( [ "examples" ] , depicted ) ;
581-
582564export const depictResponse = ( {
583565 method,
584566 path,
@@ -593,25 +575,34 @@ export const depictResponse = ({
593575 description = `${ method . toUpperCase ( ) } ${ path } ${ ucFirst ( variant ) } response ${
594576 hasMultipleStatusCodes ? statusCode : ""
595577 } `. trim ( ) ,
596- } : ReqResHandlingProps < $ZodType > & {
578+ } : ReqResCommons & {
579+ schema : $ZodType ;
580+ composition : "inline" | "components" ;
581+ description ?: string ;
582+ brandHandling ?: BrandHandling ;
597583 mimeTypes : ReadonlyArray < string > | null ;
598584 variant : ResponseVariant ;
599585 statusCode : number ;
600586 hasMultipleStatusCodes : boolean ;
601587} ) : ResponseObject => {
602588 if ( ! mimeTypes ) return { description } ;
603- const depictedSchema = excludeExamplesFromDepiction (
589+ const response = ensureCompliance (
604590 depict ( schema , {
605591 rules : { ...brandHandling , ...depicters } ,
606592 ctx : { isResponse : true , makeRef, path, method } ,
607593 } ) ,
608594 ) ;
595+ const examples = [ ] ;
596+ if ( isSchemaObject ( response ) && response . examples ) {
597+ examples . push ( ...response . examples ) ;
598+ delete response . examples ; // moving them up
599+ }
609600 const media : MediaTypeObject = {
610601 schema :
611602 composition === "components"
612- ? makeRef ( schema , depictedSchema , makeCleanId ( description ) )
613- : depictedSchema ,
614- examples : depictExamples ( schema , true ) ,
603+ ? makeRef ( schema , response , makeCleanId ( description ) )
604+ : response ,
605+ examples : enumerateExamples ( examples ) ,
615606 } ;
616607 return { description, content : R . fromPairs ( R . xprod ( mimeTypes , [ media ] ) ) } ;
617608} ;
@@ -708,34 +699,62 @@ export const depictSecurityRefs = (
708699 } , { } ) ,
709700 ) ;
710701
702+ export const depictRequest = ( {
703+ schema,
704+ brandHandling,
705+ makeRef,
706+ path,
707+ method,
708+ } : ReqResCommons & {
709+ schema : IOSchema ;
710+ brandHandling ?: BrandHandling ;
711+ } ) =>
712+ depict ( schema , {
713+ rules : { ...brandHandling , ...depicters } ,
714+ ctx : { isResponse : false , makeRef, path, method } ,
715+ } ) ;
716+
711717export const depictBody = ( {
712718 method,
713719 path,
714720 schema,
721+ request,
715722 mimeType,
716723 makeRef,
717724 composition,
718- brandHandling,
719725 paramNames,
720726 description = `${ method . toUpperCase ( ) } ${ path } Request body` ,
721- } : ReqResHandlingProps < IOSchema > & {
727+ } : ReqResCommons & {
728+ schema : IOSchema ;
729+ composition : "inline" | "components" ;
730+ description ?: string ;
731+ request : JSONSchema . BaseSchema ;
722732 mimeType : string ;
723733 paramNames : string [ ] ;
724734} ) => {
725735 const [ withoutParams , hasRequired ] = excludeParamsFromDepiction (
726- depict ( schema , {
727- rules : { ...brandHandling , ...depicters } ,
728- ctx : { isResponse : false , makeRef, path, method } ,
729- } ) ,
736+ ensureCompliance ( request ) ,
730737 paramNames ,
731738 ) ;
732- const bodyDepiction = excludeExamplesFromDepiction ( withoutParams ) ;
739+ const examples = [ ] ;
740+ if ( isSchemaObject ( withoutParams ) && withoutParams . examples ) {
741+ examples . push ( ...withoutParams . examples ) ;
742+ delete withoutParams . examples ; // pull up
743+ }
733744 const media : MediaTypeObject = {
734745 schema :
735746 composition === "components"
736- ? makeRef ( schema , bodyDepiction , makeCleanId ( description ) )
737- : bodyDepiction ,
738- examples : depictExamples ( extractObjectSchema ( schema ) , false , paramNames ) ,
747+ ? makeRef ( schema , withoutParams , makeCleanId ( description ) )
748+ : withoutParams ,
749+ examples : enumerateExamples (
750+ examples . length
751+ ? examples
752+ : flattenIO ( request )
753+ . examples . filter (
754+ ( one ) : one is FlatObject => isObject ( one ) && ! Array . isArray ( one ) ,
755+ )
756+ . map ( R . omit ( paramNames ) ) ,
757+ ) ,
739758 } ;
740759 const body : RequestBodyObject = {
741760 description,
0 commit comments