Repository: Redocly/redocly-cli
Pull request #1509 (2024-03-29 21:31:33)

Ignore validating example with externalValue against schema
## What/Why/How?

Currently, when an example object has `externalValue` property [`no-invalid-media-type-examples`](https://redocly.com/docs/cli/rules/no-invalid-media-type-examples/) lint rule fails as it is validating the _example_ object (the parent object) against the schema definition.

I could assume there are couple of ways to handle this case. However, as there is already another lint rule ([`no-example-value-and-externalValue`](https://redocly.com/docs/cli/rules/no-example-value-and-externalValue/)), which is kind of related to this one, and its default severity level causes either one of `value`, or `externalValue` properties to exist for a specific example object, I decided to go with the simple exclusion condition, which basically deactivates the validation for example objects that has `externalValue` set. I also intentionally didn't implement the logic to verify the specified URL set for `externalValue` to check whether the target resource actually matches the schema definition or not (by issuing HTTP request for example) as it's both complex to implement properly due to various cases including relative URLs, and for external resources being unpredictable by nature, e.g., the server might be unavailable at the time of linting, or returing a temporary failure due to network issues.

## Reference

I faced the issue while I was trying to add file examples for request, and response bodies of an API according to the official documentation ([here](https://swagger.io/docs/specification/adding-examples/#external)).

## Testing

Relevant tests are updated to cover this scenario.

## Screenshots (optional)

It can be tested by disabling the condition I added to the lint rule.

## Check yourself

- [x] Code changed? - Tested with redoc/reference-docs/workflows (internal)
- [x] All new/updated code is covered with tests
- [ ] New package installed? - Tested in different environments (browser/node)

## Security

- [ ] Security impact of change has been considered
- [ ] Code follows company security practices and guidelines


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 6d28d70a0dfa066d904df093e87f7311db3f4c7a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/xeptore/redocly-cli/new/main?filename=.changeset/giant-llamas-grin.md&value=---%0A%22%40redocly%2Fopenapi-core%22%3A%20patch%0A---%0A%0AIgnore%20validating%20example%20with%20externalValue%20against%20schema%0A)


xeptore: > Also, please add a patch [changeset](https://github.com/Redocly/redocly-cli/blob/main/CONTRIBUTING.md#pull-request-guidelines) for both `core` and `cli` packages describing the changes. Thanks!

Done in 82869150e676cf1d59d35e2f09cc6faf86027474.

> P.S. Please don't forget to run prettier 🙂

Done in cf02cec879c8e9e557fae2f2f7093f802741da80.
tatomyr: Thank you @xeptore for spending your time on this! 👍 

Patch:
diff --git a/.changeset/funny-hornets-agree.md b/.changeset/funny-hornets-agree.md
new file mode 100644
index 00000000..3167f674
--- /dev/null
+++ b/.changeset/funny-hornets-agree.md
@@ -0,0 +1,6 @@
+---
+"@redocly/cli": patch
+"@redocly/openapi-core": patch
+---
+
+Fixed [`no-invalid-media-type-examples`](https://redocly.com/docs/cli/rules/no-invalid-media-type-examples/) rule `externalValue` example validation.
diff --git a/packages/core/src/rules/oas3/no-invalid-media-type-examples.ts b/packages/core/src/rules/oas3/no-invalid-media-type-examples.ts
index 62a72303..240e753f 100644
--- a/packages/core/src/rules/oas3/no-invalid-media-type-examples.ts
+++ b/packages/core/src/rules/oas3/no-invalid-media-type-examples.ts
@@ -35,6 +35,9 @@ export const ValidContentExamples: Oas3Rule = (opts) => {
             location = isMultiple ? resolved.location.child('value') : resolved.location;
             example = resolved.node;
           }
+          if (isMultiple && typeof example.value === 'undefined') {
+            return;
+          }
           validateExample(
             isMultiple ? example.value : example,
             mediaType.schema!,


Test patch:
diff --git a/packages/core/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts b/packages/core/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts
index 057dc24b..bf4a028e 100644
--- a/packages/core/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts
+++ b/packages/core/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts
@@ -470,4 +470,149 @@ describe('no-invalid-media-type-examples', () => {
 
     expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
   });
+
+  it('should not report if only externalValue is set', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+        openapi: 3.0.0
+        paths:
+          /pet:
+            get:
+              responses:
+                '200':
+                  content:
+                    application/json:
+                      schema:
+                        type: object
+                        properties:
+                          a:
+                            type: string
+                          b:
+                            type: number
+                      examples:
+                        first:
+                          externalValue: "https://example.com/example.json"
+      `,
+      'foobar.yaml'
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({ 'no-invalid-media-type-examples': 'error' }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
+  });
+
+  it('should not report if value is valid and externalValue is also set', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+        openapi: 3.0.0
+        paths:
+          /pet:
+            get:
+              responses:
+                '200':
+                  content:
+                    application/json:
+                      schema:
+                        type: object
+                        properties:
+                          a:
+                            type: string
+                          b:
+                            type: number
+                      examples:
+                        first:
+                          externalValue: "https://example.com/example.json"
+                          value:
+                            a: "A"
+                            b: 0
+      `,
+      'foobar.yaml'
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({ 'no-invalid-media-type-examples': 'error' }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
+  });
+
+  it('should report invalid value when externalValue is also set', async () => {
+    const document = parseYamlToDocument(
+      outdent`
+        openapi: 3.0.0
+        paths:
+          /pet:
+            get:
+              responses:
+                '200':
+                  content:
+                    application/json:
+                      schema:
+                        type: object
+                        properties:
+                          a:
+                            type: string
+                          b:
+                            type: number
+                      examples:
+                        first:
+                          externalValue: "https://example.com/example.json"
+                          value:
+                            a: 0
+                            b: "0"
+      `,
+      'foobar.yaml'
+    );
+
+    const results = await lintDocument({
+      externalRefResolver: new BaseResolver(),
+      document,
+      config: await makeConfig({ 'no-invalid-media-type-examples': 'error' }),
+    });
+
+    expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
+      [
+        {
+          "from": {
+            "pointer": "#/paths/~1pet/get/responses/200/content/application~1json",
+            "source": "foobar.yaml",
+          },
+          "location": [
+            {
+              "pointer": "#/paths/~1pet/get/responses/200/content/application~1json/examples/first/value/a",
+              "reportOnKey": false,
+              "source": "foobar.yaml",
+            },
+          ],
+          "message": "Example value must conform to the schema: \`a\` property type must be string.",
+          "ruleId": "no-invalid-media-type-examples",
+          "severity": "error",
+          "suggest": [],
+        },
+        {
+          "from": {
+            "pointer": "#/paths/~1pet/get/responses/200/content/application~1json",
+            "source": "foobar.yaml",
+          },
+          "location": [
+            {
+              "pointer": "#/paths/~1pet/get/responses/200/content/application~1json/examples/first/value/b",
+              "reportOnKey": false,
+              "source": "foobar.yaml",
+            },
+          ],
+          "message": "Example value must conform to the schema: \`b\` property type must be number.",
+          "ruleId": "no-invalid-media-type-examples",
+          "severity": "error",
+          "suggest": [],
+        },
+      ]
+    `);
+  });
 });