@@ -261,6 +261,18 @@ function arePropertySchemasMutuallyExclusive(
261261 }
262262 }
263263
264+ // Object property count constraints - check if ranges don't overlap
265+ if ( prop1 . type === 'object' && prop2 . type === 'object' ) {
266+ const minProps1 = prop1 . minProperties ?? 0 ;
267+ const maxProps1 = prop1 . maxProperties ?? Infinity ;
268+ const minProps2 = prop2 . minProperties ?? 0 ;
269+ const maxProps2 = prop2 . maxProperties ?? Infinity ;
270+
271+ if ( rangesDoNotOverlap ( minProps1 , maxProps1 , minProps2 , maxProps2 ) ) {
272+ return { isExclusive : true } ;
273+ }
274+ }
275+
264276 return { isExclusive : false } ;
265277}
266278
@@ -358,6 +370,26 @@ function areSignaturesMutuallyExclusive(
358370 reason : `Schemas have overlapping properties: ${ ambiguousProperties . join ( ', ' ) } .` ,
359371 } ;
360372 }
373+
374+ // Check if one schema requires properties that the other doesn't have at all
375+ // This makes them mutually exclusive
376+ if ( sig1 . required && sig1 . required . size > 0 ) {
377+ const requiredNotInSig2Properties = [ ...sig1 . required ] . filter (
378+ ( prop ) => ! sig2 . properties || ! sig2 . properties . has ( prop )
379+ ) ;
380+ if ( requiredNotInSig2Properties . length > 0 ) {
381+ return { isExclusive : true } ;
382+ }
383+ }
384+
385+ if ( sig2 . required && sig2 . required . size > 0 ) {
386+ const requiredNotInSig1Properties = [ ...sig2 . required ] . filter (
387+ ( prop ) => ! sig1 . properties || ! sig1 . properties . has ( prop )
388+ ) ;
389+ if ( requiredNotInSig1Properties . length > 0 ) {
390+ return { isExclusive : true } ;
391+ }
392+ }
361393 }
362394
363395 // Array items check
@@ -371,6 +403,32 @@ function areSignaturesMutuallyExclusive(
371403 }
372404 }
373405
406+ // additionalProperties check - schemas with conflicting additionalProperties settings
407+ if ( sig1 . additionalProperties !== undefined || sig2 . additionalProperties !== undefined ) {
408+ const addlProps1 = sig1 . additionalProperties ;
409+ const addlProps2 = sig2 . additionalProperties ;
410+
411+ // If one explicitly disallows additional properties (false) and the other allows them (true or schema)
412+ // AND they have overlapping required properties, this creates ambiguity
413+ const allowsAdditional1 = addlProps1 !== false ;
414+ const allowsAdditional2 = addlProps2 !== false ;
415+
416+ if ( allowsAdditional1 !== allowsAdditional2 ) {
417+ // Check if they have overlapping required properties
418+ if ( sig1 . required && sig2 . required ) {
419+ const requiredOverlap = [ ...sig1 . required ] . filter ( ( prop ) => sig2 . required ! . has ( prop ) ) ;
420+ if ( requiredOverlap . length > 0 ) {
421+ return {
422+ isExclusive : false ,
423+ reason : `Schemas have conflicting additionalProperties settings with overlapping required properties: ${ requiredOverlap . join (
424+ ', '
425+ ) } .`,
426+ } ;
427+ }
428+ }
429+ }
430+ }
431+
374432 return { isExclusive : true } ;
375433}
376434
@@ -405,6 +463,23 @@ function checkOneOfMutualExclusivity(
405463 resolve : UserContext [ 'resolve' ] ,
406464 parentSchema : Oas3Schema | Oas3_1Schema
407465) : void {
466+ // Check for empty schemas first - an empty schema {} accepts any value
467+ for ( let i = 0 ; i < schemas . length ; i ++ ) {
468+ const schema = schemas [ i ] ;
469+
470+ // Skip $ref schemas - they're not empty
471+ if ( isRef ( schema ) ) continue ;
472+
473+ // Check if schema is empty (no properties defined)
474+ const keys = Object . keys ( schema ) ;
475+ if ( keys . length === 0 ) {
476+ report ( {
477+ message : `Empty schema at position ${ i } in \`oneOf\` matches all values and cannot be mutually exclusive.` ,
478+ location : location . child ( [ 'oneOf' , i ] ) ,
479+ } ) ;
480+ }
481+ }
482+
408483 // Check for impossible nullable + oneOf with null type combination
409484 // This is a specific anti-pattern where the parent schema is nullable
410485 // AND one of the oneOf options is type: 'null', making it impossible to distinguish
0 commit comments