Repository: codekie/openapi-examples-validator (js)

Issue:
Update to > v6 causes `jsonPath` errors
In our project we usw `openapi-example-validator` to validate our OpenAPI specfile example. We have a pretty simple command that worked fine with version 5

```
  "scripts": {
    "openapi-enforcer": "node test/api/openapi-enforcer.js",
    "validate-examples": "openapi-examples-validator src/api/docs/content/specs/main.yaml",
    "test": "npm run openapi-enforcer && npm run validate-examples"
  },
```

However, upgrading to > v6 things started to error at the `jsonPath` call

```
> openapi-examples-validator src/api/docs/content/specs/main.yaml

Validating examples
/home/runner/work/FTL/FTL/node_modules/jsonpath-plus/dist/index-node-cjs.cjs:1976
    throw new Error('jsonPath: ' + e.message + ': ' + code);
          ^

Error: jsonPath: func is not a function: @property.match(/[/+]json/)
    at JSONPath._eval (/home/runner/work/FTL/FTL/node_modules/jsonpath-plus/dist/index-node-cjs.cjs:1976:11)
    at /home/runner/work/FTL/FTL/node_modules/jsonpath-plus/dist/index-node-cjs.cjs:1783:[18](https://github.com/pi-hole/FTL/actions/runs/14548155183/job/40816195602?pr=2373#step:5:19)
    at JSONPath._walk (/home/runner/work/FTL/FTL/node_modules/jsonpath-plus/dist/index-node-cjs.cjs:[19](https://github.com/pi-hole/FTL/actions/runs/14548155183/job/40816195602?pr=2373#step:5:20)06:7)
    at JSONPath._trace (/home/runner/work/FTL/FTL/node_modules/jsonpath-plus/dist/index-node-cjs.cjs:1782:12)
    at JSONPath._trace (/home/runner/work/FTL/FTL/node_modules/jsonpath-plus/dist/index-node-cjs.cjs:1874:17)
    at JSONPath._trace (/home/runner/work/FTL/FTL/node_modules/jsonpath-plus/dist/index-node-cjs.cjs:1726:17)
    at /home/runner/work/FTL/FTL/node_modules/jsonpath-plus/dist/index-node-cjs.cjs:1733:21
    at /home/runner/work/FTL/FTL/node_modules/jsonpath-plus/dist/index-node-cjs.cjs:1910:7
    at Array.forEach (<anonymous>)
    at JSONPath._walk (/home/runner/work/FTL/FTL/node_modules/jsonpath-plus/dist/index-node-cjs.cjs:1909:22)

Node.js v[20](https://github.com/pi-hole/FTL/actions/runs/14548155183/job/40816195602?pr=2373#step:5:21).19.0
```

Any idea how to fix this?

Pull request description:
fix: don't throw an error when there is a property named `content` of the type `array`
The JSONPath selector to select examples does not work when there's a property called `content` and it's of the type `array` for OpenAPI definitions v3 and properties called `examples` in v2.

Fixes #197 

Patch:
diff --git a/src/impl/v2/index.js b/src/impl/v2/index.js
index 1fe1e13..92c4ad4 100644
--- a/src/impl/v2/index.js
+++ b/src/impl/v2/index.js
@@ -8,7 +8,7 @@ const cloneDeep = require('lodash.clonedeep'),
 
 // CONSTANTS
 
-const PATH__EXAMPLES = '$..examples[?(@property.match(/[\/+]json/))]',
+const PATH__EXAMPLES = '$..examples[?(@property && typeof @property === "string" && @property.match(/[\/+]json/))]',
     PROP__SCHEMA = 'schema',
     PROP__EXAMPLES = 'examples';
 
diff --git a/src/impl/v3/index.js b/src/impl/v3/index.js
index 8111dc9..93a3730 100644
--- a/src/impl/v3/index.js
+++ b/src/impl/v3/index.js
@@ -9,8 +9,9 @@ const cloneDeep = require('lodash.clonedeep'),
 
 // CONSTANTS
 
-const RESPONSES = '$..responses..content[?(@property.match(/[\/+]json/))]';
-const REQUEST = '$..requestBody.content[?(@property.match(/[\/+]json/))]';
+// eslint-disable-next-line max-len
+const RESPONSES = '$..responses..content[?(@property && typeof @property === "string" && @property.match(/[\/+]json/))]';
+const REQUEST = '$..requestBody.content[?(@property && typeof @property === "string" && @property.match(/[\/+]json/))]';
 const SINGLE_EXAMPLE = '.example';
 const MANY_EXAMPLES = '.examples.*.value';
 


Test patch:
diff --git a/test/data/v2/response-valid-content-array.yaml b/test/data/v2/response-valid-content-array.yaml
new file mode 100644
index 0000000..297488b
--- /dev/null
+++ b/test/data/v2/response-valid-content-array.yaml
@@ -0,0 +1,32 @@
+swagger: '2.0'
+info:
+  title: API
+  version: 1.0.0
+paths:
+  /metrics:
+    get:
+      summary: Get info
+      operationId: get_info
+      description: |
+        This API hook returns server metrics.
+      produces:
+        - application/json
+      responses:
+        200:
+          description: OK
+          schema:
+            type: object
+            properties:
+              examples:
+                type: array
+                description: Array of valid entries
+                items:
+                  type: object
+                  properties:
+                    type:
+                      type: integer
+                      description: RR type of entry
+          examples:
+            application/json:
+              examples:
+                - type: 0
diff --git a/test/data/v3/response-valid-content-array.yaml b/test/data/v3/response-valid-content-array.yaml
new file mode 100644
index 0000000..a3802df
--- /dev/null
+++ b/test/data/v3/response-valid-content-array.yaml
@@ -0,0 +1,32 @@
+openapi: 3.0.2
+components:
+  paths:
+    metrics:
+      get:
+        summary: Get info
+        operationId: "get_info"
+        description: |
+          This API hook returns server metrics.
+        responses:
+          '200':
+            description: OK
+            content:
+              application/json:
+                schema:
+                  type: object
+                  properties:
+                    content:
+                      type: array
+                      description: Array of valid entries
+                      items:
+                        type: object
+                        properties:
+                          type:
+                            type: integer
+                            description: RR type of entry
+                examples:
+                  metrics:
+                    summary: Server metrics
+                    value:
+                      content:
+                        - type: 0
diff --git a/test/specs/impl/v2/index.js b/test/specs/impl/v2/index.js
index 7772971..3e18085 100644
--- a/test/specs/impl/v2/index.js
+++ b/test/specs/impl/v2/index.js
@@ -4,6 +4,7 @@ const path = require('path'),
     { validateExample, 'default': validateExamples, validateExamplesByMap } = require('../../../../src/index'),
     { ApplicationError, ErrorType } = require('../../../../src/application-error');
 const { prepare } = require('../../../../src/impl/v2');
+const { validateFile } = require('../../../../src');
 
 const PATH__SCHEMA_EXTERNAL_EXAMPLE = '$.paths./.get.responses.200.schema',
     PATH__SCHEMA_EXTERNAL_EXAMPLE_INVALID = '$.hmm.what.am.i.gonna.get.for.lunch',
@@ -27,7 +28,9 @@ const PATH__SCHEMA_EXTERNAL_EXAMPLE = '$.paths./.get.responses.200.schema',
     FILE_PATH__NOT_EXISTS = 'there is no spoon',
     FILE_PATH__EXTERNAL_EXAMPLE_INVALID_TYPE = path.join('test', 'data', 'v2', 'external-examples-invalid-type.json'),
     FILE_PATH__EXTERNAL_EXAMPLE_INVALID_MISSING_LINK = path.join('test', 'data', 'v2',
-        'external-examples-invalid-missing-link.json');
+        'external-examples-invalid-missing-link.json'),
+    FILE_PATH__VALID__RESPONSE__EXAMPLES_CONTENT_ARRAY
+        = path.join(__dirname, '../../../data/v2/response-valid-content-array.yaml');
 
 describe('Main-module, for v2 should', () => {
     describe('recognize', () => {
@@ -421,6 +424,19 @@ describe('Main-module, for v2 should', () => {
             preparedOpenapi.should.deep.equal(loadTestData('v2/valid-with-all-properties-required'));
         });
     });
+    describe('handle elements named "content" which are Arrays', function() {
+        it('should validate successfully', async function() {
+            const { valid, statistics } = structuredClone(
+                await validateFile(FILE_PATH__VALID__RESPONSE__EXAMPLES_CONTENT_ARRAY)
+            );
+            valid.should.equal(true);
+            statistics.should.deep.equal({
+                schemasWithExamples: 1,
+                examplesWithoutSchema: 0,
+                examplesTotal: 1
+            });
+        });
+    });
 });
 
 function removeMapFilePath(errors) {
diff --git a/test/specs/impl/v3/index.js b/test/specs/impl/v3/index.js
index 2343d04..105242f 100644
--- a/test/specs/impl/v3/index.js
+++ b/test/specs/impl/v3/index.js
@@ -80,7 +80,9 @@ const JSON_PATH__CONTEXT_MUTUALLY_EXCLUSIVE = '/paths/~1pets/get/responses/200/c
         = path.join(__dirname, '../../../data/v3/simple-api-with-examples-exclusive-minimum-invalid.json'),
     FILE_PATH__EXAMPLE_NAMES_TO_BE_ESCAPED
         = path.join(__dirname, '../../../data/v3/simple-api-with-example-names-to-be-escaped.json'),
-    FILE_PATH__UNKNOWN_FORMATS = path.join(__dirname, '../../../data/v3/unknown-formats.json');
+    FILE_PATH__UNKNOWN_FORMATS = path.join(__dirname, '../../../data/v3/unknown-formats.json'),
+    FILE_PATH__VALID__RESPONSE__EXAMPLES_CONTENT_ARRAY
+        = path.join(__dirname, '../../../data/v3/response-valid-content-array.yaml');
 
 describe('Main-module, for v3 should', function() {
     describe('recognize', function() {
@@ -514,6 +516,19 @@ unknown format "country-code-2" ignored in schema at path "#/properties/country"
             })).valid.should.equal(true);
         });
     });
+    describe('handle elements named "content" which are Arrays', function() {
+        it('should validate successfully', async function() {
+            const { valid, statistics } = structuredClone(
+                await validateFile(FILE_PATH__VALID__RESPONSE__EXAMPLES_CONTENT_ARRAY)
+            );
+            valid.should.equal(true);
+            statistics.should.deep.equal({
+                schemasWithExamples: 1,
+                examplesWithoutSchema: 0,
+                examplesTotal: 1
+            });
+        });
+    });
 });
 
 function _expandFilePathOfErrors(errors, propertyName) {