From c08f75ecd9c97f8b445d59600e1753b91a96efa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=ADdia=20Tarcza?= <100163235+diatrcz@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:37:01 +0100 Subject: [PATCH 1/5] fix(ibm-valid-schema-example): prevent recursive schemas from failing the validator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lídia Tarcza <100163235+diatrcz@users.noreply.github.com> --- package-lock.json | 2 +- .../src/functions/valid-schema-example.js | 51 +++++++- packages/ruleset/src/utils/index.js | 1 + .../ruleset/src/utils/nested-schema-keys.js | 16 +++ .../test/rules/valid-schema-example.test.js | 120 ++++++++++++++++++ 5 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 packages/ruleset/src/utils/nested-schema-keys.js diff --git a/package-lock.json b/package-lock.json index 6b83ce371..df55200db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15099,7 +15099,7 @@ }, "packages/validator": { "name": "ibm-openapi-validator", - "version": "1.37.10", + "version": "1.37.11", "license": "Apache-2.0", "dependencies": { "@ibm-cloud/openapi-ruleset": "1.33.7", diff --git a/packages/ruleset/src/functions/valid-schema-example.js b/packages/ruleset/src/functions/valid-schema-example.js index 36c5a273b..959b138b6 100644 --- a/packages/ruleset/src/functions/valid-schema-example.js +++ b/packages/ruleset/src/functions/valid-schema-example.js @@ -5,7 +5,7 @@ const { validate } = require('jsonschema'); const { validateSubschemas } = require('@ibm-cloud/openapi-ruleset-utilities'); -const { LoggerFactory } = require('../utils'); +const { nestedSchemaKeys, LoggerFactory } = require('../utils'); let ruleId; let logger; @@ -50,6 +50,14 @@ function checkSchemaExamples(schema, path) { function validateExamples(examples) { return examples .map(({ schema, example, path }) => { + if (hasUnresolvedRefs(schema)) { + logger.debug( + `Skipping example validation at path ${path.join(".")}: schema contains unresolved $ref references` + ); + // Skip validation for schemas with unresolved references. + return undefined; + } + // Setting required: true prevents undefined values from passing validation. const { valid, errors } = validate(example, schema, { required: true }); if (!valid) { @@ -60,7 +68,46 @@ function validateExamples(examples) { }; } }) - .filter(e => isDefined(e)); + .filter((e) => isDefined(e)); +} + +/** + * Recursively checks if a schema or any of its nested schemas contain unresolved $ref references. + * @param {object} schema - The schema to check + * @returns {boolean} - True if the schema contains unresolved $ref references + */ +function hasUnresolvedRefs(schema) { + if (!schema || typeof schema !== 'object') { + return false; + } + + if (schema.$ref) { + return true; + } + + // Recursively check nested schemas in common locations. + for (const key of nestedSchemaKeys) { + if (schema[key]) { + if (Array.isArray(schema[key])) { + // Check each item in arrays (allOf, anyOf, oneOf). + if (schema[key].some(item => hasUnresolvedRefs(item))) { + return true; + } + } else if (key === 'properties') { + // Check each property in properties object. + if (Object.values(schema[key]).some(prop => hasUnresolvedRefs(prop))) { + return true; + } + } else { + // Check single nested schema (items, additionalProperties, not). + if (hasUnresolvedRefs(schema[key])) { + return true; + } + } + } + } + + return false; } function isDefined(x) { diff --git a/packages/ruleset/src/utils/index.js b/packages/ruleset/src/utils/index.js index 5ea3ea175..fd8b04475 100644 --- a/packages/ruleset/src/utils/index.js +++ b/packages/ruleset/src/utils/index.js @@ -18,6 +18,7 @@ module.exports = { isRequestBodyExploded: require('./is-requestbody-exploded'), LoggerFactory: require('./logger-factory'), mergeAllOfSchemaProperties: require('./merge-allof-schema-properties'), + nestedSchemaKeys: require('./nested-schema-keys'), operationMethods: require('./constants'), pathHasMinimallyRepresentedResource: require('./path-has-minimally-represented-resource'), pathMatchesRegexp: require('./path-matches-regexp'), diff --git a/packages/ruleset/src/utils/nested-schema-keys.js b/packages/ruleset/src/utils/nested-schema-keys.js new file mode 100644 index 000000000..6870392c8 --- /dev/null +++ b/packages/ruleset/src/utils/nested-schema-keys.js @@ -0,0 +1,16 @@ +/** + * Copyright 2017 - 2025 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const NestedSchemaKeys = [ +'items', +'additionalProperties', +'properties', +'allOf', +'anyOf', +'oneOf', +'not', +]; + +module.exports = NestedSchemaKeys; \ No newline at end of file diff --git a/packages/ruleset/test/rules/valid-schema-example.test.js b/packages/ruleset/test/rules/valid-schema-example.test.js index 089252beb..24c9ee4f7 100644 --- a/packages/ruleset/test/rules/valid-schema-example.test.js +++ b/packages/ruleset/test/rules/valid-schema-example.test.js @@ -7,6 +7,7 @@ const { validSchemaExample } = require('../../src/rules'); const { makeCopy, rootDocument, + recursiveAPI, testRule, severityCodes, } = require('../test-utils'); @@ -22,6 +23,125 @@ describe(`Spectral rule: ${ruleId}`, () => { const results = await testRule(ruleId, rule, rootDocument); expect(results).toHaveLength(0); }); + + it('Recursive schema should not make the rule fail', async () => { + const testDocument = makeCopy(rootDocument); + + // Replace the API structure with the recursiveAPI structure + testDocument.paths = { + '/test': { + get: { + description: 'Test', + operationId: 'listTest', + parameters: [], + responses: { + 200: { + description: 'Paginated list of objects', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/TestListResponse', + }, + }, + }, + }, + }, + security: [ + { + bearer: [], + }, + ], + summary: 'List test', + tags: ['Test'], + }, + }, + }; + + testDocument.tags = [ + { + name: 'Test', + }, + ]; + + testDocument.components.securitySchemes = { + bearer: { + scheme: 'bearer', + bearerFormat: 'JWT', + type: 'http', + }, + }; + + testDocument.components.schemas = { + Templates: { + type: 'object', + properties: { + id: { + type: 'number', + example: 1234, + description: 'Id', + }, + templates: { + description: 'Nested check templates', + type: 'array', + items: { + $ref: '#/components/schemas/Templates', + }, + }, + }, + required: ['id'], + }, + TestResponse: { + type: 'object', + properties: { + id: { + type: 'number', + example: 12345, + description: 'test id', + }, + templates: { + description: 'templates', + example: [ + { + id: 2, + templates: [ + { + id: 3, + }, + { + id: 4, + }, + ], + }, + ], + type: 'array', + items: { + $ref: '#/components/schemas/Templates', + }, + }, + }, + required: ['id', 'templates'], + }, + TestListResponse: { + type: 'object', + properties: { + data: { + type: 'array', + items: { + $ref: '#/components/schemas/TestResponse', + }, + }, + }, + required: ['data'], + }, + }; + + // Remove responses and requestBodies that reference old schemas + delete testDocument.components.responses; + delete testDocument.components.requestBodies; + + const results = await testRule(ruleId, rule, testDocument); + expect(results).toHaveLength(0); + }); }); describe('Should yield errors', () => { From 77a4ecc60e0625e98f80f089c07a0357eed68b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=ADdia=20Tarcza?= <100163235+diatrcz@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:47:07 +0100 Subject: [PATCH 2/5] fix: code clean up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lídia Tarcza <100163235+diatrcz@users.noreply.github.com> --- .../ruleset/src/functions/valid-schema-example.js | 2 +- packages/ruleset/src/utils/nested-schema-keys.js | 14 +++++++------- .../test/rules/valid-schema-example.test.js | 1 - 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/ruleset/src/functions/valid-schema-example.js b/packages/ruleset/src/functions/valid-schema-example.js index 959b138b6..836e45982 100644 --- a/packages/ruleset/src/functions/valid-schema-example.js +++ b/packages/ruleset/src/functions/valid-schema-example.js @@ -52,7 +52,7 @@ function validateExamples(examples) { .map(({ schema, example, path }) => { if (hasUnresolvedRefs(schema)) { logger.debug( - `Skipping example validation at path ${path.join(".")}: schema contains unresolved $ref references` + `Skipping example validation at path ${path.join('.')}: schema contains unresolved $ref references` ); // Skip validation for schemas with unresolved references. return undefined; diff --git a/packages/ruleset/src/utils/nested-schema-keys.js b/packages/ruleset/src/utils/nested-schema-keys.js index 6870392c8..f12e79aad 100644 --- a/packages/ruleset/src/utils/nested-schema-keys.js +++ b/packages/ruleset/src/utils/nested-schema-keys.js @@ -4,13 +4,13 @@ */ const NestedSchemaKeys = [ -'items', -'additionalProperties', -'properties', -'allOf', -'anyOf', -'oneOf', -'not', + 'items', + 'additionalProperties', + 'properties', + 'allOf', + 'anyOf', + 'oneOf', + 'not', ]; module.exports = NestedSchemaKeys; \ No newline at end of file diff --git a/packages/ruleset/test/rules/valid-schema-example.test.js b/packages/ruleset/test/rules/valid-schema-example.test.js index 24c9ee4f7..4c1ca4ff7 100644 --- a/packages/ruleset/test/rules/valid-schema-example.test.js +++ b/packages/ruleset/test/rules/valid-schema-example.test.js @@ -7,7 +7,6 @@ const { validSchemaExample } = require('../../src/rules'); const { makeCopy, rootDocument, - recursiveAPI, testRule, severityCodes, } = require('../test-utils'); From 982493d4dabab29a139c7cacaedd7d60a55e16ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=ADdia=20Tarcza?= <100163235+diatrcz@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:52:22 +0100 Subject: [PATCH 3/5] fix: code clean up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lídia Tarcza <100163235+diatrcz@users.noreply.github.com> --- .../src/functions/valid-schema-example.js | 2 +- packages/ruleset/src/utils/nested-schema-keys.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/ruleset/src/functions/valid-schema-example.js b/packages/ruleset/src/functions/valid-schema-example.js index 836e45982..eb53d3e67 100644 --- a/packages/ruleset/src/functions/valid-schema-example.js +++ b/packages/ruleset/src/functions/valid-schema-example.js @@ -68,7 +68,7 @@ function validateExamples(examples) { }; } }) - .filter((e) => isDefined(e)); + .filter(e => isDefined(e)); } /** diff --git a/packages/ruleset/src/utils/nested-schema-keys.js b/packages/ruleset/src/utils/nested-schema-keys.js index f12e79aad..784bc6569 100644 --- a/packages/ruleset/src/utils/nested-schema-keys.js +++ b/packages/ruleset/src/utils/nested-schema-keys.js @@ -4,13 +4,13 @@ */ const NestedSchemaKeys = [ - 'items', - 'additionalProperties', - 'properties', - 'allOf', - 'anyOf', - 'oneOf', - 'not', + "items", + "additionalProperties", + "properties", + "allOf", + "anyOf", + "oneOf", + "not", ]; -module.exports = NestedSchemaKeys; \ No newline at end of file +module.exports = NestedSchemaKeys; From bcdb706a3190d5ed286bbc3da4acb27265c826a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=ADdia=20Tarcza?= <100163235+diatrcz@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:56:42 +0100 Subject: [PATCH 4/5] fix: code clean up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lídia Tarcza <100163235+diatrcz@users.noreply.github.com> --- packages/ruleset/src/utils/nested-schema-keys.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/ruleset/src/utils/nested-schema-keys.js b/packages/ruleset/src/utils/nested-schema-keys.js index 784bc6569..9efa53851 100644 --- a/packages/ruleset/src/utils/nested-schema-keys.js +++ b/packages/ruleset/src/utils/nested-schema-keys.js @@ -4,13 +4,13 @@ */ const NestedSchemaKeys = [ - "items", - "additionalProperties", - "properties", - "allOf", - "anyOf", - "oneOf", - "not", + 'items', + 'additionalProperties', + 'properties', + 'allOf', + 'anyOf', + 'oneOf', + 'not', ]; module.exports = NestedSchemaKeys; From 3e98094745635269cf7068db293e0594126a4ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=ADdia=20Tarcza?= <100163235+diatrcz@users.noreply.github.com> Date: Wed, 11 Mar 2026 10:01:22 +0100 Subject: [PATCH 5/5] fix: minor code fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lídia Tarcza <100163235+diatrcz@users.noreply.github.com> --- packages/ruleset/src/utils/nested-schema-keys.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ruleset/src/utils/nested-schema-keys.js b/packages/ruleset/src/utils/nested-schema-keys.js index 9efa53851..3ef61583f 100644 --- a/packages/ruleset/src/utils/nested-schema-keys.js +++ b/packages/ruleset/src/utils/nested-schema-keys.js @@ -1,9 +1,9 @@ /** - * Copyright 2017 - 2025 IBM Corporation. + * Copyright 2017 - 2026 IBM Corporation. * SPDX-License-Identifier: Apache2.0 */ -const NestedSchemaKeys = [ +const nestedSchemaKeys = [ 'items', 'additionalProperties', 'properties', @@ -13,4 +13,4 @@ const NestedSchemaKeys = [ 'not', ]; -module.exports = NestedSchemaKeys; +module.exports = nestedSchemaKeys;