diff --git a/tools/spectral/ipa/__tests__/utils/compareUtils.test.js b/tools/spectral/ipa/__tests__/utils/compareUtils.test.js index 8cb9b5d091..1fe52c9d92 100644 --- a/tools/spectral/ipa/__tests__/utils/compareUtils.test.js +++ b/tools/spectral/ipa/__tests__/utils/compareUtils.test.js @@ -1,217 +1,192 @@ import { describe, expect, it } from '@jest/globals'; import { isDeepEqual, - omitDeep, - removePropertyByFlag, - removeReadOnlyProperties, - removeWriteOnlyProperties, -} from '../../rulesets/functions/utils/compareUtils'; + removePropertiesByFlag, + removePropertyKeys, + removeRequestProperties, + removeResponseProperties, +} from '../../rulesets/functions/utils/compareUtils.js'; describe('isDeepEqual', () => { - it('handles primitive values', () => { - expect(isDeepEqual(1, 1)).toBe(true); - expect(isDeepEqual('hello', 'hello')).toBe(true); + it('should return true for identical primitive values', () => { + expect(isDeepEqual('string', 'string')).toBe(true); + expect(isDeepEqual(42, 42)).toBe(true); expect(isDeepEqual(true, true)).toBe(true); expect(isDeepEqual(null, null)).toBe(true); - expect(isDeepEqual(undefined, undefined)).toBe(true); - - expect(isDeepEqual(1, 2)).toBe(false); - expect(isDeepEqual('hello', 'world')).toBe(false); - expect(isDeepEqual(true, false)).toBe(false); - expect(isDeepEqual(null, undefined)).toBe(false); - expect(isDeepEqual(1, '1')).toBe(false); - }); - - it('handles simple objects', () => { - expect(isDeepEqual({}, {})).toBe(true); - expect(isDeepEqual({ a: 1 }, { a: 1 })).toBe(true); - expect(isDeepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true); - - expect(isDeepEqual({ a: 1 }, { a: 2 })).toBe(false); - expect(isDeepEqual({ a: 1 }, { b: 1 })).toBe(false); - expect(isDeepEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false); }); - it('handles arrays', () => { - expect(isDeepEqual([], [])).toBe(true); - expect(isDeepEqual([1, 2], [1, 2])).toBe(true); - - expect(isDeepEqual([1, 2], [2, 1])).toBe(false); - expect(isDeepEqual([1, 2], [1, 2, 3])).toBe(false); + it('should return true for same type primitives', () => { + expect(isDeepEqual('string1', 'string2')).toBe(true); + expect(isDeepEqual(42, 100)).toBe(true); }); - it('handles nested objects', () => { - expect(isDeepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })).toBe(true); - - expect(isDeepEqual({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 3 } })).toBe(false); - - expect(isDeepEqual({ a: 1, b: { c: 2, d: 3 } }, { a: 1, b: { c: 2 } })).toBe(false); + it('should return false for different type primitives', () => { + expect(isDeepEqual('string', 42)).toBe(false); + expect(isDeepEqual(42, true)).toBe(false); + expect(isDeepEqual(null, 'string')).toBe(false); }); - it('handles nested arrays', () => { - expect(isDeepEqual({ a: [1, 2, { b: 3 }] }, { a: [1, 2, { b: 3 }] })).toBe(true); - - expect(isDeepEqual({ a: [1, 2, { b: 3 }] }, { a: [1, 2, { b: 4 }] })).toBe(false); + it('should handle null/undefined correctly', () => { + expect(isDeepEqual(null, null)).toBe(true); + expect(isDeepEqual(undefined, undefined)).toBe(true); }); - it('handles mixed types', () => { - expect(isDeepEqual({ a: 1 }, [1])).toBe(false); - expect(isDeepEqual({ a: 1 }, null)).toBe(false); - expect(isDeepEqual(null, { a: 1 })).toBe(false); + it('should compare object types correctly', () => { + expect(isDeepEqual({ type: 'string' }, { type: 'string' })).toBe(true); + expect(isDeepEqual({ type: 'string' }, { type: 'number' })).toBe(false); }); -}); -describe('removePropertyByFlag', () => { - it('handles primitive values', () => { - expect(removePropertyByFlag(1, 'readOnly')).toBe(1); - expect(removePropertyByFlag('hello', 'readOnly')).toBe('hello'); - expect(removePropertyByFlag(true, 'readOnly')).toBe(true); - expect(removePropertyByFlag(null, 'readOnly')).toBe(null); - expect(removePropertyByFlag(undefined, 'readOnly')).toBe(undefined); - }); + it('should ignore validation constraints', () => { + expect(isDeepEqual({ type: 'string', minLength: 5 }, { type: 'string', maxLength: 10 })).toBe(true); - it('handles empty objects', () => { - expect(removePropertyByFlag({}, 'readOnly')).toEqual({}); + expect(isDeepEqual({ type: 'array', minItems: 1 }, { type: 'array' })).toBe(true); }); - it('removes properties with flagged attributes', () => { - const input = { - type: 'object', + it('should compare schema properties structure', () => { + const schema1 = { properties: { - id: { type: 'string', readOnly: true }, name: { type: 'string' }, - password: { type: 'string', test: true }, + age: { type: 'number' }, }, + type: 'object', }; - const expectedReadOnly = { - type: 'object', + const schema2 = { properties: { name: { type: 'string' }, - password: { type: 'string', test: true }, + age: { type: 'number' }, }, + type: 'object', }; - const expectedTest = { + expect(isDeepEqual(schema1, schema2)).toBe(true); + }); + + it('should detect different properties structure', () => { + const schema1 = { + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + }, type: 'object', + }; + + const schema2 = { properties: { - id: { type: 'string', readOnly: true }, name: { type: 'string' }, + height: { type: 'number' }, }, + type: 'object', }; - expect(removePropertyByFlag(input, 'readOnly')).toEqual(expectedReadOnly); - expect(removePropertyByFlag(input, 'test')).toEqual(expectedTest); + expect(isDeepEqual(schema1, schema2)).toBe(false); }); - it('processes nested objects', () => { - const input = { - type: 'object', + it('should compare nested schema structures', () => { + const schema1 = { properties: { user: { type: 'object', properties: { - id: { type: 'string', readOnly: true }, name: { type: 'string' }, - metadata: { + details: { type: 'object', properties: { - createdAt: { type: 'string', readOnly: true }, - updatedAt: { type: 'string', readOnly: true }, - notes: { type: 'string' }, + age: { type: 'number' }, }, }, }, }, }, + type: 'object', }; - const expected = { - type: 'object', + const schema2 = { properties: { user: { type: 'object', properties: { name: { type: 'string' }, - metadata: { + details: { type: 'object', properties: { - notes: { type: 'string' }, + age: { type: 'number' }, }, }, }, }, }, + type: 'object', }; - expect(removePropertyByFlag(input, 'readOnly')).toEqual(expected); + expect(isDeepEqual(schema1, schema2)).toBe(true); }); - it('processes arrays', () => { - const input = { - type: 'array', - items: { - type: 'object', - properties: { - id: { type: 'string', readOnly: true }, - name: { type: 'string' }, - }, + it('should handle example from the prompt', () => { + const schema1 = { + properties: { + desc: { maxLength: 250, minLength: 1, type: 'string' }, + roles: { items: {}, minItems: 1, type: 'array' }, }, + type: 'object', }; - const expected = { - type: 'array', - items: { - type: 'object', - properties: { - name: { type: 'string' }, - }, + const schema2 = { + properties: { + desc: { maxLength: 250, minLength: 1, type: 'string' }, + roles: { items: {}, type: 'array' }, }, + type: 'object', }; - expect(removePropertyByFlag(input, 'readOnly')).toEqual(expected); + expect(isDeepEqual(schema1, schema2)).toBe(true); }); +}); - it('handles array of objects', () => { - const input = [ - { id: 1, readOnly: true }, - { id: 2, name: 'test' }, - { - id: 3, - properties: { - secret: { type: 'string', readOnly: true }, - visible: { type: 'string' }, - }, +describe('removePropertiesByFlag', () => { + it('should return primitives unchanged', () => { + expect(removePropertiesByFlag('string', 'readOnly')).toBe('string'); + expect(removePropertiesByFlag(42, 'readOnly')).toBe(42); + expect(removePropertiesByFlag(null, 'readOnly')).toBe(null); + }); + + it('should remove flagged properties from schema properties', () => { + const input = { + type: 'object', + properties: { + name: { type: 'string' }, + id: { type: 'string', readOnly: true }, + email: { type: 'string' }, }, - ]; - - const expected = [ - { id: 1 }, - { id: 2, name: 'test' }, - { - id: 3, - properties: { - visible: { type: 'string' }, - }, + }; + + const expected = { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, }, - ]; + }; - expect(removePropertyByFlag(input, 'readOnly')).toEqual(expected); + expect(removePropertiesByFlag(input, 'readOnly')).toEqual(expected); }); -}); -describe('removeReadOnlyProperties', () => { - it('removes readOnly properties from schema', () => { + it('should handle nested objects recursively', () => { const input = { type: 'object', properties: { - id: { type: 'string', readOnly: true }, - name: { type: 'string' }, - details: { + user: { type: 'object', properties: { - createdAt: { type: 'string', readOnly: true }, - description: { type: 'string' }, + name: { type: 'string' }, + id: { type: 'string', readOnly: true }, + details: { + type: 'object', + properties: { + age: { type: 'number' }, + createdAt: { type: 'string', readOnly: true }, + }, + }, }, }, }, @@ -220,32 +195,66 @@ describe('removeReadOnlyProperties', () => { const expected = { type: 'object', properties: { - name: { type: 'string' }, - details: { + user: { type: 'object', properties: { - description: { type: 'string' }, + name: { type: 'string' }, + details: { + type: 'object', + properties: { + age: { type: 'number' }, + }, + }, }, }, }, }; - expect(removeReadOnlyProperties(input)).toEqual(expected); + expect(removePropertiesByFlag(input, 'readOnly')).toEqual(expected); }); }); -describe('removeWriteOnlyProperties', () => { - it('removes writeOnly properties from schema', () => { +describe('removePropertyKeys', () => { + it('should return primitives unchanged', () => { + expect(removePropertyKeys('string', 'title')).toBe('string'); + expect(removePropertyKeys(42, 'title')).toBe(42); + expect(removePropertyKeys(null, 'title')).toBe(null); + }); + + it('should remove specified top-level properties', () => { const input = { + title: 'User', + description: 'User schema', type: 'object', properties: { - id: { type: 'string' }, - password: { type: 'string', writeOnly: true }, - details: { + name: { type: 'string' }, + }, + }; + + const expected = { + type: 'object', + properties: { + name: { type: 'string' }, + }, + }; + + expect(removePropertyKeys(input, 'title', 'description')).toEqual(expected); + }); + + it('should remove properties recursively', () => { + const input = { + title: 'User', + type: 'object', + properties: { + user: { + title: 'User details', type: 'object', properties: { - secretKey: { type: 'string', writeOnly: true }, - description: { type: 'string' }, + name: { + type: 'string', + title: 'User name', + description: 'Full name of the user', + }, }, }, }, @@ -254,63 +263,69 @@ describe('removeWriteOnlyProperties', () => { const expected = { type: 'object', properties: { - id: { type: 'string' }, - details: { + user: { type: 'object', properties: { - description: { type: 'string' }, + name: { + type: 'string', + }, }, }, }, }; - expect(removeWriteOnlyProperties(input)).toEqual(expected); + expect(removePropertyKeys(input, 'title', 'description')).toEqual(expected); }); }); -describe('schema compatibility use cases', () => { - it('request and response schema comparison', () => { - const requestSchema = { +describe('removeResponseProperties', () => { + it('should remove readOnly properties and metadata', () => { + const input = { + title: 'User Response', + description: 'User data returned by the API', + required: ['name'], type: 'object', properties: { name: { type: 'string' }, + id: { type: 'string', readOnly: true }, email: { type: 'string' }, - password: { type: 'string', writeOnly: true }, }, - required: ['name', 'email', 'password'], }; - const responseSchema = { + const expected = { type: 'object', properties: { - id: { type: 'string', readOnly: true }, name: { type: 'string' }, email: { type: 'string' }, - createdAt: { type: 'string', readOnly: true }, }, - required: ['id', 'name', 'email', 'createdAt'], }; - const filteredRequest = removeWriteOnlyProperties(requestSchema); - const filteredResponse = removeReadOnlyProperties(responseSchema); + expect(removeResponseProperties(input)).toEqual(expected); + }); +}); - // Verify filtered schemas - expect(filteredRequest).toEqual({ +describe('removeRequestProperties', () => { + it('should remove writeOnly properties and metadata', () => { + const input = { + title: 'User Request', + description: 'User data sent to the API', + required: ['name', 'password'], type: 'object', properties: { name: { type: 'string' }, + password: { type: 'string', writeOnly: true }, email: { type: 'string' }, }, - required: ['name', 'email', 'password'], - }); + }; - expect(filteredResponse).toEqual({ + const expected = { type: 'object', properties: { name: { type: 'string' }, email: { type: 'string' }, }, - required: ['id', 'name', 'email', 'createdAt'], - }); + }; + + expect(removeRequestProperties(input)).toEqual(expected); }); }); diff --git a/tools/spectral/ipa/rulesets/IPA-106.yaml b/tools/spectral/ipa/rulesets/IPA-106.yaml index 1f674a35cc..b378cee4bd 100644 --- a/tools/spectral/ipa/rulesets/IPA-106.yaml +++ b/tools/spectral/ipa/rulesets/IPA-106.yaml @@ -44,8 +44,11 @@ rules: ##### Implementation details Validation checks the POST method for resource collection paths. + - Validation ignores resources without a Get method. - `readOnly:true` properties of Get method response will be ignored. - `writeOnly:true` properties of Create method request will be ignored. + - Property comparison is based on `type` and `name` matching. + - `oneOf` and `discriminator` definitions must match exactly. message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-106-create-method-request-body-is-get-method-response:' severity: warn given: '$.paths[*].post.requestBody.content' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index f1e80fea76..18c892ff68 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -198,8 +198,11 @@ Validation checks the POST method for resource collection paths. Request body content of the Create method and response content of the Get method should refer to the same resource. ##### Implementation details Validation checks the POST method for resource collection paths. + - Validation ignores resources without a Get method. - `readOnly:true` properties of Get method response will be ignored. - `writeOnly:true` properties of Create method request will be ignored. + - Property comparison is based on `type` and `name` matching. + - `oneOf` and `discriminator` definitions must match exactly. #### xgen-IPA-106-create-method-request-has-no-readonly-fields ![warn](https://img.shields.io/badge/warning-yellow) diff --git a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js index 519ec920f5..830d73d5b3 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js +++ b/tools/spectral/ipa/rulesets/functions/IPA106CreateMethodRequestBodyIsGetResponse.js @@ -5,10 +5,10 @@ import { isSingletonResource, } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; -import { isDeepEqual, removeReadOnlyProperties, removeWriteOnlyProperties } from './utils/compareUtils.js'; +import { isDeepEqual, removeRequestProperties, removeResponseProperties } from './utils/compareUtils.js'; import { hasException } from './utils/exceptions.js'; import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; -import { getResponseOfGetMethodByMediaType } from './utils/methodUtils.js'; +import { getResponseOfGetMethodByMediaType, getSchemaRef } from './utils/methodUtils.js'; const RULE_NAME = 'xgen-IPA-106-create-method-request-body-is-get-method-response'; const ERROR_MESSAGE = @@ -16,6 +16,7 @@ const ERROR_MESSAGE = export default (input, _, { path, documentInventory }) => { const oas = documentInventory.resolved; + const unresolvedOas = documentInventory.unresolved; const resourcePath = path[1]; let mediaType = input; const resourcePaths = getResourcePathItems(resourcePath, oas.paths); @@ -25,24 +26,34 @@ export default (input, _, { path, documentInventory }) => { return; } - const getMethodResponseContentPerMediaType = getResponseOfGetMethodByMediaType(mediaType, resourcePath, oas); - if (!getMethodResponseContentPerMediaType) { + const getResponseContentPerMediaType = getResponseOfGetMethodByMediaType(mediaType, resourcePath, oas); + if (!getResponseContentPerMediaType) { return; } - const postMethodRequestContentPerMediaType = resolveObject(oas, path); - if (!postMethodRequestContentPerMediaType.schema) { + const postRequestContentPerMediaType = resolveObject(oas, path); + if (!postRequestContentPerMediaType.schema) { return; } - if (hasException(postMethodRequestContentPerMediaType, RULE_NAME)) { - collectException(postMethodRequestContentPerMediaType, RULE_NAME, path); + + if (hasException(postRequestContentPerMediaType, RULE_NAME)) { + collectException(postRequestContentPerMediaType, RULE_NAME, path); return; } + const postRequestContentPerMediaTypeUnresolved = resolveObject(unresolvedOas, path); + const getResponseContentPerMediaTypeUnresolved = getResponseOfGetMethodByMediaType( + mediaType, + resourcePath, + unresolvedOas + ); + const errors = checkViolationsAndReturnErrors( path, - postMethodRequestContentPerMediaType, - getMethodResponseContentPerMediaType + postRequestContentPerMediaType, + getResponseContentPerMediaType, + postRequestContentPerMediaTypeUnresolved, + getResponseContentPerMediaTypeUnresolved ); if (errors.length !== 0) { @@ -54,12 +65,14 @@ export default (input, _, { path, documentInventory }) => { function checkViolationsAndReturnErrors( path, - postMethodRequestContentPerMediaType, - getMethodResponseContentPerMediaType + postRequestContentPerMediaType, + getResponseContentPerMediaType, + postRequestContentPerMediaTypeUnresolved, + getResponseContentPerMediaTypeUnresolved ) { const errors = []; - if (!getMethodResponseContentPerMediaType.schema) { + if (!getResponseContentPerMediaType.schema) { return [ { path, @@ -68,9 +81,17 @@ function checkViolationsAndReturnErrors( ]; } - // Create filtered versions of schemas by removing properties with appropriate flags - const filteredCreateRequestSchema = removeWriteOnlyProperties(postMethodRequestContentPerMediaType); - const filteredGetResponseSchema = removeReadOnlyProperties(getMethodResponseContentPerMediaType); + const postRequestSchemaRef = getSchemaRef(postRequestContentPerMediaTypeUnresolved.schema); + const getResponseSchemaRef = getSchemaRef(getResponseContentPerMediaTypeUnresolved.schema); + + if (postRequestSchemaRef && getResponseSchemaRef) { + if (postRequestSchemaRef === getResponseSchemaRef) { + return []; + } + } + + const filteredCreateRequestSchema = removeRequestProperties(postRequestContentPerMediaType.schema); + const filteredGetResponseSchema = removeResponseProperties(getResponseContentPerMediaType.schema); if (!isDeepEqual(filteredCreateRequestSchema, filteredGetResponseSchema)) { errors.push({ diff --git a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js index 888255c5b9..29115e64a3 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/compareUtils.js @@ -1,33 +1,64 @@ // utils/compareUtils.js /** - * Deep equality check between two values + * Deep schema structure equality check between two values + * Compares property names and types, but not specific values * Does not handle circular references - * @param {*} value1 First value to compare - * @param {*} value2 Second value to compare - * @returns {boolean} Whether the values are deeply equal + * @param {*} object1 First schema to compare + * @param {*} object2 Second schema to compare + * @returns {boolean} Whether the schemas have identical structure */ -export function isDeepEqual(value1, value2) { - // If the values are strictly equal (including handling null/undefined) - if (value1 === value2) return true; +export function isDeepEqual(object1, object2) { + if (object1 === object2) { + return true; + } - // If either value is null or not an object, they're not equal (we already checked strict equality) - if (value1 == null || value2 == null || typeof value1 !== 'object' || typeof value2 !== 'object') { - return false; + if (typeof object1 !== 'object' || typeof object2 !== 'object') { + return typeof object1 === typeof object2; } - const keys1 = Object.keys(value1); - const keys2 = Object.keys(value2); + if (object1.properties && object2.properties) { + const propKeys1 = Object.keys(object1.properties); + const propKeys2 = Object.keys(object2.properties); + + if (propKeys1.length !== propKeys2.length) return false; + + for (const key of propKeys1) { + if (!propKeys2.includes(key)) return false; + + // Check if the types match for each property + if (object1.properties[key].type !== object2.properties[key].type) return false; + + // Recursively check nested objects + if (typeof object1.properties[key] === 'object' && typeof object2.properties[key] === 'object') { + if (!isDeepEqual(object1.properties[key], object2.properties[key])) return false; + } + } + } - // Different number of properties - if (keys1.length !== keys2.length) return false; + if (object1.type !== object2.type) return false; - // Check that all properties in value1 exist in value2 and are equal + const getRelevantKeys = (obj) => { + return Object.keys(obj).filter((key) => { + const polymorphismProps = ['allOf', 'anyOf', 'oneOf', 'discriminator']; + return polymorphismProps.includes(key); + }); + }; + const keys1 = getRelevantKeys(object1); + const keys2 = getRelevantKeys(object2); for (const key of keys1) { if (!keys2.includes(key)) return false; - // Recursive equality check for nested objects - if (!isDeepEqual(value1[key], value2[key])) return false; + if ( + typeof object1[key] === 'object' && + object1[key] !== null && + typeof object2[key] === 'object' && + object2[key] !== null + ) { + if (!isDeepEqual(object1[key], object2[key])) return false; + } else if (object1.type !== object2.type) { + return false; + } } return true; @@ -39,13 +70,9 @@ export function isDeepEqual(value1, value2) { * @param {string} propertyToRemove The property type to remove (boolean property) * @returns {object} New schema with specified properties removed */ -export function removePropertyByFlag(schema, propertyToRemove) { +export function removePropertiesByFlag(schema, propertyToRemove) { if (!schema || typeof schema !== 'object') return schema; - if (Array.isArray(schema)) { - return schema.map((item) => removePropertyByFlag(item, propertyToRemove)); - } - const result = {}; // Handle regular object properties @@ -59,10 +86,10 @@ export function removePropertyByFlag(schema, propertyToRemove) { for (const [propName, propValue] of Object.entries(value)) { // Skip properties marked with the flag we're removing if (propValue[propertyToRemove] === true) continue; - result[key][propName] = removePropertyByFlag(propValue, propertyToRemove); + result[key][propName] = removePropertiesByFlag(propValue, propertyToRemove); } } else if (typeof value === 'object') { - result[key] = removePropertyByFlag(value, propertyToRemove); + result[key] = removePropertiesByFlag(value, propertyToRemove); } else { result[key] = value; } @@ -72,19 +99,46 @@ export function removePropertyByFlag(schema, propertyToRemove) { } /** - * Recursively removes properties with readOnly: true flag from schema + * Recursively removes specific property keys from schema + * @param {object} schema The schema to process + * @param {...string} propertyNames Property names to remove + * @returns {object} New schema with specified property keys removed + */ +export function removePropertyKeys(schema, ...propertyNames) { + if (!schema || typeof schema !== 'object') return schema; + + const result = {}; + + // Handle regular object properties + for (const [key, value] of Object.entries(schema)) { + // Skip this property if it's in the list of properties to remove + if (propertyNames.includes(key)) continue; + + if (typeof value === 'object') { + result[key] = removePropertyKeys(value, ...propertyNames); + } else { + result[key] = value; + } + } + + return result; +} +/** + * Recursively removes properties for Response schemas * @param {object} schema The schema to process * @returns {object} New schema with readOnly properties removed */ -export function removeReadOnlyProperties(schema) { - return removePropertyByFlag(schema, 'readOnly'); +export function removeResponseProperties(schema) { + let result = removePropertiesByFlag(schema, 'readOnly'); + return removePropertyKeys(result, 'title', 'description', 'required'); } /** - * Recursively removes properties with writeOnly: true flag from schema + * Recursively removes properties for Request schemas * @param {object} schema The schema to process * @returns {object} New schema with writeOnly properties removed */ -export function removeWriteOnlyProperties(schema) { - return removePropertyByFlag(schema, 'writeOnly'); +export function removeRequestProperties(schema) { + let result = removePropertiesByFlag(schema, 'writeOnly'); + return removePropertyKeys(result, 'title', 'description', 'required'); }