diff --git a/packages/openapi3-parser/lib/parser/oas/parseSchemaObject.js b/packages/openapi3-parser/lib/parser/oas/parseSchemaObject.js index 342dd5fb..67622b37 100644 --- a/packages/openapi3-parser/lib/parser/oas/parseSchemaObject.js +++ b/packages/openapi3-parser/lib/parser/oas/parseSchemaObject.js @@ -192,7 +192,10 @@ function parseType(context) { ensureTypesAreUnique, // FIXME support >1 type - R.when(e => e.length > 1, createWarning(namespace, `'${name}' 'type' more than one type is current unsupported`))); + R.unless( + e => e.length === 0 || e.length === 1 || (e.length === 2 && e.contains('null')), + createWarning(namespace, `'${name}' 'type' more than one type is current unsupported`) + )); return R.cond([ [isString, parseStringType], @@ -213,10 +216,10 @@ function parseType(context) { } // Returns whether the given element value matches the provided schema type -const valueMatchesType = (type, value) => { +const valueMatchesType = R.curry((value, type) => { const expectedElementType = typeToElementNameMap[type]; return value.element === expectedElementType; -}; +}); // Returns whether the given element value matches an enumeration of fixed values const valueMatchesEnumerationValues = (enumeration, value) => { @@ -240,9 +243,10 @@ function validateValuesMatchSchema(context, schema) { } const type = schema.getValue('type'); - if (type && !valueMatchesType(type, member.value)) { + if (type && R.none(valueMatchesType(member.value), type)) { + const types = type.map(t => `'${t}'`).join(', '); return createWarning(namespace, - `'${name}' '${member.key.toValue()}' does not match expected type '${type}'`, member.value); + `'${name}' '${member.key.toValue()}' does not match expected type ${types}`, member.value); } return member; @@ -326,13 +330,19 @@ function parseSchema(context) { return element; }; + const parseNullable = R.ifElse( + R.always(context.isOpenAPIVersionMoreThanOrEqual(3, 1)), + createWarning(context.namespace, `'${name}' 'nullable' is removed in OpenAPI 3.1, use 'null' in type`), + parseBoolean(context, name, false) + ); + const parseMember = R.cond([ [hasKey('type'), R.compose(parseType(context), getValue)], [hasKey('enum'), R.compose(parseEnum(context, name), getValue)], [hasKey('properties'), R.compose(parseProperties, getValue)], [hasKey('items'), R.compose(parseSubSchema, getValue)], [hasKey('required'), R.compose(parseRequired, getValue)], - [hasKey('nullable'), parseBoolean(context, name, false)], + [hasKey('nullable'), parseNullable], [hasKey('title'), parseString(context, name, false)], [hasKey('description'), parseString(context, name, false)], [hasKey('default'), e => e.clone()], @@ -375,10 +385,11 @@ function parseSchema(context) { element = constValue; } else if (enumerations) { element = enumerations; + } else if (type.length === 1 || (type.length === 2 && type.includes('null'))) { + const findType = R.find(R.complement(R.equals('nullable'))); + element = constructStructure(namespace, schema, findType(type)); } else if (type.length > 1) { throw new Error('Implementation error: unexpected multiple types'); - } else if (type.length === 1) { - element = constructStructure(namespace, schema, type[0]); } else { element = new namespace.elements.Enum(); element.enumerations = [ @@ -404,8 +415,9 @@ function parseSchema(context) { element.description = description; } + // On OAS 3.0, nullable is a keyword, on OAS 3.1, null goes in type const nullable = schema.getValue('nullable'); - if (nullable) { + if (nullable || (type.includes('null') && element.element !== 'null')) { const typeAttributes = element.attributes.get('typeAttributes') || new namespace.elements.Array(); typeAttributes.push('nullable'); element.attributes.set('typeAttributes', typeAttributes); diff --git a/packages/openapi3-parser/test/unit/parser/oas/parseSchemaObject-test.js b/packages/openapi3-parser/test/unit/parser/oas/parseSchemaObject-test.js index 33180fe5..e865a0ed 100644 --- a/packages/openapi3-parser/test/unit/parser/oas/parseSchemaObject-test.js +++ b/packages/openapi3-parser/test/unit/parser/oas/parseSchemaObject-test.js @@ -201,6 +201,7 @@ describe('Schema Object', () => { const element = parseResult.get(0).content; expect(element).to.be.instanceof(namespace.elements.Null); + expect(element.attributes.getValue('typeAttributes')).to.be.undefined; }); }); @@ -292,6 +293,21 @@ describe('Schema Object', () => { const string = parseResult.get(0).content; expect(string).to.be.instanceof(namespace.elements.String); }); + + it('when type contains a single value with null', () => { + const schema = new namespace.elements.Object({ + type: ['string', 'null'], + }); + const parseResult = parse(context, schema); + + expect(parseResult.length).to.equal(1); + expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); + expect(parseResult).to.not.contain.annotations; + + const string = parseResult.get(0).content; + expect(string).to.be.instanceof(namespace.elements.String); + expect(string.attributes.getValue('typeAttributes')).to.deep.equal(['nullable']); + }); }); }); @@ -686,6 +702,20 @@ describe('Schema Object', () => { }); describe('#nullable', () => { + it('warns when nullable is used with OpenAPI 3.1', () => { + context.openapiVersion = { major: 3, minor: 1 }; + const schema = new namespace.elements.Object({ + nullable: true, + }); + const parseResult = parse(context, schema); + + expect(parseResult.length).to.equal(2); + + expect(parseResult).to.contain.warning( + "'Schema Object' 'nullable' is removed in OpenAPI 3.1, use 'null' in type" + ); + }); + it('warns when nullable is not boolean', () => { const schema = new namespace.elements.Object({ nullable: 1, @@ -860,6 +890,21 @@ describe('Schema Object', () => { expect(element.attributes.get('default').toValue()).to.equal(null); }); + it('allows a null default value with null type on OpenAPI 3.1', () => { + context.openapiVersion = { major: 3, minor: 1 }; + const schema = new namespace.elements.Object({ + type: ['string', 'null'], + default: null, + }); + const parseResult = parse(context, schema); + + expect(parseResult.length).to.equal(1); + expect(parseResult.get(0)).to.be.instanceof(namespace.elements.DataStructure); + + const element = parseResult.get(0).content; + expect(element.attributes.get('default').toValue()).to.equal(null); + }); + it('allows a null default value when nullable is enabled with enum', () => { const schema = new namespace.elements.Object({ enum: ['my default'], @@ -888,6 +933,20 @@ describe('Schema Object', () => { ); }); + it('warns when default does not match expected types on OpenAPI 3.1', () => { + context.openapiVersion = { major: 3, minor: 1 }; + const schema = new namespace.elements.Object({ + type: ['number', 'null'], + default: 'my default', + }); + const parseResult = parse(context, schema); + + expect(parseResult.length).to.equal(2); + expect(parseResult).to.contain.warning( + "'Schema Object' 'default' does not match expected type 'number', 'null'" + ); + }); + it('warns when default does not match enumeration value', () => { const schema = new namespace.elements.Object({ type: 'string',