Repository: Redocly/openapi-cli
Pull request #289 (2021-05-04 02:59:42)

[Update] enum item validation with nullable property 
## What/Why/How?

The linter will report an error when an enum item doesn't match the corresponding schema type. However, I believe there should be an exception when an item has a value of `- null` and `nullable: true` is set. This PR reflects this exception -- adding the logic necessary to evaluate the nullable property for both `Oas2Schema` and `Oas3Schema`

## Reference

https://swagger.io/docs/specification/data-models/enums/#nullable

## Testing

Test was included to show that the linter should not report an error when `nullable: true` and enum items contain a `null`

## Check yourself

- [X] Code is linted
- [ ] Tested with redoc/reference-docs/workflows
- [X] All new/updated code is covered with tests



Patch:
diff --git a/packages/core/src/rules/common/no-enum-type-mismatch.ts b/packages/core/src/rules/common/no-enum-type-mismatch.ts
index e5892d76..6195bbc0 100644
--- a/packages/core/src/rules/common/no-enum-type-mismatch.ts
+++ b/packages/core/src/rules/common/no-enum-type-mismatch.ts
@@ -9,7 +9,7 @@ export const NoEnumTypeMismatch: Oas3Rule | Oas2Rule = () => {
     Schema(schema: Oas2Schema | Oas3Schema, { report, location }: UserContext) {
       if (schema.enum && schema.type) {
         const typeMismatchedValues = schema.enum.filter(
-          (item) => !matchesJsonSchemaType(item, schema.type as string),
+          (item) => !matchesJsonSchemaType(item, schema.type as string, schema.nullable as boolean),
         );
         for (const mismatchedValue of typeMismatchedValues) {
           report({
diff --git a/packages/core/src/rules/common/spec.ts b/packages/core/src/rules/common/spec.ts
index 18c8fc6b..a92c38a8 100644
--- a/packages/core/src/rules/common/spec.ts
+++ b/packages/core/src/rules/common/spec.ts
@@ -75,7 +75,7 @@ export const OasSpec: Oas3Rule | Oas2Rule = () => {
               suggest: getSuggest(propValue, propSchema.enum),
             });
           }
-        } else if (propSchema.type && !matchesJsonSchemaType(propValue, propSchema.type)) {
+        } else if (propSchema.type && !matchesJsonSchemaType(propValue, propSchema.type, false)) {
           report({
             message: `Expected type \`${propSchema.type}\` but got \`${propValueType}\`.`,
             location: propLocation,
@@ -84,7 +84,7 @@ export const OasSpec: Oas3Rule | Oas2Rule = () => {
           const itemsType = propSchema.items?.type;
           for (let i = 0; i < propValue.length; i++) {
             const item = propValue[i];
-            if (!matchesJsonSchemaType(item, itemsType)) {
+            if (!matchesJsonSchemaType(item, itemsType, false)) {
               report({
                 message: `Expected type \`${itemsType}\` but got \`${oasTypeOf(item)}\`.`,
                 location: propLocation.child([i]),
diff --git a/packages/core/src/rules/utils.ts b/packages/core/src/rules/utils.ts
index 4802822f..f63c45ff 100644
--- a/packages/core/src/rules/utils.ts
+++ b/packages/core/src/rules/utils.ts
@@ -18,7 +18,11 @@ export function oasTypeOf(value: unknown) {
  * @param {JSONSchemaType} type - JSON Schema type
  * @returns boolean
  */
-export function matchesJsonSchemaType(value: unknown, type: string): boolean {
+export function matchesJsonSchemaType(value: unknown, type: string, nullable: boolean): boolean {
+  if (nullable && value === null) {
+    return value === null
+  }
+
   switch (type) {
     case 'array':
       return Array.isArray(value);
diff --git a/packages/core/src/typings/swagger.ts b/packages/core/src/typings/swagger.ts
index 66c5f235..9a5d3343 100644
--- a/packages/core/src/typings/swagger.ts
+++ b/packages/core/src/typings/swagger.ts
@@ -150,7 +150,7 @@ export interface Oas2Schema {
   format?: string;
   externalDocs?: Oas2ExternalDocs;
   discriminator?: string;
-
+  nullable?: boolean;
   allOf?: Oas2Schema[];
 
   title?: string;


Test patch:
diff --git a/packages/core/src/rules/common/__tests__/no-enum-type-mismatch.test.ts b/packages/core/src/rules/common/__tests__/no-enum-type-mismatch.test.ts
index fc540b6f..cdb87723 100644
--- a/packages/core/src/rules/common/__tests__/no-enum-type-mismatch.test.ts
+++ b/packages/core/src/rules/common/__tests__/no-enum-type-mismatch.test.ts
@@ -36,6 +36,39 @@ describe('Oas3 typed enum', () => {
     expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
   });
 
+  it('should not report on enum object if all items match type and enum is nullable', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+          openapi: 3.0.0
+          paths:
+            /some:
+              get:
+                responses:
+                  '200':
+                    description: A paged array of pets
+                    content:
+                      application/json:
+                        schema:
+                          type: string
+                          nullable: true
+                          enum:
+                            - A
+                            - B
+                            - C
+                            - null
+        `,
+      'foobar.yaml',
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: new LintConfig({ extends: [], rules: { 'no-enum-type-mismatch': 'error' } }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
+  });
+
   it('should report on enum object if not all items match type', async () => {
     const document = parseYamlToDocument(
       outdent`