@@ -421,6 +421,29 @@ const isDiscriminatableMember = (
421421 return hasLiteralDiscriminator ( resolved , property ) ;
422422} ;
423423
424+ /**
425+ * Read a schema object's `required` keyword.
426+ *
427+ * Some generators emit `required: true` on the schema referenced by a request
428+ * body, borrowing the boolean that belongs on the request body object. Spreading
429+ * that boolean fails with `(schema.required ?? []) is not iterable`, which says
430+ * nothing about the document. Report the offending schema and the expected shape
431+ * instead. The document is not rewritten: a boolean carries no property names,
432+ * so there is nothing to recover from it. (#3719)
433+ */
434+ const getRequiredKeys = ( schema : OpenApiSchemaObject , name : string ) => {
435+ const required = schema . required as unknown ;
436+
437+ if ( required === undefined ) return [ ] ;
438+ if ( Array . isArray ( required ) ) return required ;
439+
440+ throw new Error (
441+ `Invalid OpenAPI document: schema "${ name } " has \`required: ${ JSON . stringify (
442+ required ,
443+ ) } \`, but a schema object's \`required\` must be an array of property names. A boolean \`required\` belongs on the request body object or on a parameter, not on the schema it references.`,
444+ ) ;
445+ } ;
446+
424447export const generateZodValidationSchemaDefinition = (
425448 schema : OpenApiSchemaObject | OpenApiReferenceObject | undefined ,
426449 context : ContextSpec ,
@@ -765,19 +788,24 @@ export const generateZodValidationSchemaDefinition = (
765788 const allOfRequired = schema . allOf
766789 ? [
767790 ...new Set ( [
768- ...( schema . required ?? [ ] ) ,
769- ...schemas . flatMap ( ( member ) => {
791+ ...getRequiredKeys ( schema , name ) ,
792+ ...schemas . flatMap ( ( member , index ) => {
770793 // Only the member's top-level `required` is needed. For `$ref`
771794 // members resolve shallowly (no deep property dereference) and
772795 // tolerate unresolvable refs — they simply contribute no keys.
773- const resolved =
774- '$ref' in member && typeof member . $ref === 'string'
775- ? tryResolveRefSchema ( member . $ref , context )
776- : ( member as OpenApiSchemaObject ) ;
777- const memberRequired = resolved ?. required ;
778- return Array . isArray ( memberRequired )
779- ? ( memberRequired as string [ ] )
780- : [ ] ;
796+ const isRef = '$ref' in member && typeof member . $ref === 'string' ;
797+ const resolved = isRef
798+ ? tryResolveRefSchema ( member . $ref as string , context )
799+ : ( member as OpenApiSchemaObject ) ;
800+ if ( ! resolved ) return [ ] ;
801+ // A constraint-only member never reaches the object path below,
802+ // so a misplaced boolean here would otherwise pass unreported.
803+ // Name the member, not the composing schema, or the message
804+ // points at the wrong place in the document.
805+ return getRequiredKeys (
806+ resolved ,
807+ isRef ? ( member . $ref as string ) : `${ name } .allOf[${ index } ]` ,
808+ ) ;
781809 } ) ,
782810 ] ) ,
783811 ]
@@ -1235,7 +1263,7 @@ export const generateZodValidationSchemaDefinition = (
12351263 // A property is required when this schema requires it OR when a
12361264 // sibling `allOf` member requires it (propagated via additionalRequired). (#3171)
12371265 const requiredKeys = new Set < string > ( [
1238- ...( schema . required ?? [ ] ) ,
1266+ ...getRequiredKeys ( schema , name ) ,
12391267 ...( rules ?. additionalRequired ?? [ ] ) ,
12401268 ] ) ;
12411269
0 commit comments