Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sixty-falcons-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@redocly/respect-core": patch
---

Fix AJV validation error when using discriminator with constraint patterns allOf + not . AJV with `discriminator: true` requires discriminator properties to have `const` or `enum` directly, but valid OpenAPI schemas can use `allOf + not` patterns. The schema check now strips discriminators before AJV validation which allows full dereferenced schemas with complex constraints to validate correctly while maintaining data validation semantics.
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,59 @@ describe('checkSchema', () => {
]);
});

it('should handle discriminator with allOf + not pattern without throwing', () => {
const discriminatorDescriptionOperation = {
...descriptionOperation,
responses: {
'200': {
description: 'successful operation',
content: {
'application/json': {
schema: {
oneOf: [
{
type: 'object',
properties: {
method: {
allOf: [
{ enum: ['cash', 'payment-card', 'paypal'] },
{ not: { enum: ['payment-card', 'paypal'] } },
],
},
},
},
{
type: 'object',
properties: {
method: { const: 'payment-card' },
},
},
],
discriminator: { propertyName: 'method' },
},
},
},
},
},
};

expect(() =>
checkSchema({
stepCallCtx: {
...stepCallCtx,
$response: {
body: { method: 'cash' },
statusCode: 200,
header: { 'content-type': 'application/json' },
contentType: 'application/json',
},
} as unknown as StepCallContext,
descriptionOperation: discriminatorDescriptionOperation,
ctx,
})
).not.toThrow();
});

it('should return empty checks if no response available', () => {
const stepCtx = {
$request: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '../../../types.js';
import { printErrors as printAjvErrors } from '../../../utils/ajv-errors.js';
import { checkCircularRefsInSchema } from '../../../utils/check-circular-refs-in-schema.js';
import { removeDiscriminatorsFromSchema } from '../../../utils/remove-discriminators-from-schema.js';
import { CHECKS } from '../../checks/index.js';
import { removeWriteOnlyProperties } from '../../description-parser/index.js';

Expand Down Expand Up @@ -79,18 +80,14 @@ function checkSchemaFromDescription({

if (schemaFromDescription && !isSchemaWithCircularRef) {
try {
const processedSchema = removeDiscriminatorsFromSchema(
removeWriteOnlyProperties(schemaFromDescription as JSONSchemaType<unknown>)
);
checks.push({
name: CHECKS.SCHEMA_CHECK,
passed: ajvStrict.validate(
removeWriteOnlyProperties(schemaFromDescription as JSONSchemaType<unknown>),
resultBody
),
passed: ajvStrict.validate(processedSchema, resultBody),
message: ajvStrict.errors
? printAjvErrors(
removeWriteOnlyProperties(schemaFromDescription as JSONSchemaType<unknown>),
resultBody,
ajvStrict.errors
)
? printAjvErrors(processedSchema, resultBody, ajvStrict.errors)
: '',
severity: ctx.severity['SCHEMA_CHECK'],
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { removeDiscriminatorsFromSchema } from '../remove-discriminators-from-schema.js';

describe('removeDiscriminatorsFromSchema', () => {
it('should return schema unchanged when no discriminator is present', () => {
const schema = {
type: 'object',
properties: {
method: { type: 'string' },
},
};
expect(removeDiscriminatorsFromSchema(schema)).toEqual(schema);
});

it('should remove top-level discriminator', () => {
const schema = {
oneOf: [{ properties: { method: { const: 'card' } } }],
discriminator: { propertyName: 'method' },
};
const result = removeDiscriminatorsFromSchema(schema);
expect(result).not.toHaveProperty('discriminator');
expect(result.oneOf).toBeDefined();
});

it('should remove nested discriminators', () => {
const schema = {
properties: {
instrument: {
oneOf: [{ properties: { method: { const: 'card' } } }],
discriminator: { propertyName: 'method' },
},
},
};
const result = removeDiscriminatorsFromSchema(schema);
expect(result.properties.instrument).not.toHaveProperty('discriminator');
});

it('should handle allOf + not pattern (the discriminator bug case)', () => {
const schema = {
oneOf: [
{
properties: {
method: {
allOf: [
{ enum: ['cash', 'payment-card', 'paypal', 'ach'] },
{ not: { enum: ['payment-card', 'paypal', 'ach'] } },
],
},
},
},
{
properties: {
method: { const: 'payment-card' },
},
},
],
discriminator: { propertyName: 'method' },
};
const result = removeDiscriminatorsFromSchema(schema);
expect(result).not.toHaveProperty('discriminator');
expect(result.oneOf).toHaveLength(2);
expect(result.oneOf[0].properties.method.allOf).toBeDefined();
});

it('should not mutate the original schema', () => {
const schema = {
oneOf: [{ properties: { method: { const: 'card' } } }],
discriminator: { propertyName: 'method' },
};
removeDiscriminatorsFromSchema(schema);
expect(schema).toHaveProperty('discriminator');

Check failure on line 70 in packages/respect-core/src/utils/__tests__/remove-discriminators-from-schema.test.ts

View workflow job for this annotation

GitHub Actions / build-and-unit

packages/respect-core/src/utils/__tests__/remove-discriminators-from-schema.test.ts > removeDiscriminatorsFromSchema > should not mutate the original schema

AssertionError: expected { oneOf: [ { properties: { …(1) } } ] } to have property "discriminator" ❯ packages/respect-core/src/utils/__tests__/remove-discriminators-from-schema.test.ts:70:20
});

it('should handle null and non-object values gracefully', () => {
expect(removeDiscriminatorsFromSchema(null)).toBeNull();
expect(removeDiscriminatorsFromSchema('string')).toBe('string');
expect(removeDiscriminatorsFromSchema(42)).toBe(42);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* removes `discriminator` keywords from a fully-dereferenced schema.
*/
export function removeDiscriminatorsFromSchema(schema: any): any {
if (!schema || typeof schema !== 'object') return schema;

stripDiscriminators(schema);
return schema;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function mutates input, failing its own non-mutation test

High Severity

removeDiscriminatorsFromSchema mutates its input in-place via delete node.discriminator in stripDiscriminators, but the test "should not mutate the original schema" asserts that the original object retains its discriminator property after the call. This test will fail. The function needs to deep-clone the schema before stripping, or the test expectation is wrong. In the current call site the mutation is masked because removeWriteOnlyProperties already deep-clones, but the function's contract is broken.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d7ca08e. Configure here.


function stripDiscriminators(node: any): void {
if (!node || typeof node !== 'object') return;

if (Array.isArray(node)) {
for (const item of node) {
stripDiscriminators(item);
}
return;
}

if ('discriminator' in node) {
delete node.discriminator;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strips any key named "discriminator" not just OpenAPI keyword

Medium Severity

stripDiscriminators removes every key named discriminator from every nested object, not just the OpenAPI discriminator keyword from Schema Objects. When a schema has a data property literally named discriminator inside properties (or $defs, definitions, patternProperties, etc.), the function deletes that property definition, silently corrupting the schema used for validation. The check needs to distinguish the OpenAPI discriminator keyword (which is a sibling of oneOf/anyOf and contains propertyName) from regular map entries.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c690a69. Configure here.


for (const key of Object.keys(node)) {
stripDiscriminators(node[key]);
}
}
Loading