From c73cd9de8e17305b8ca5ff4eb120f0b09f758b9c Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Fri, 10 Jul 2026 11:32:32 +0300 Subject: [PATCH 1/4] chore: add linting for dataValue in example --- .../no-invalid-parameter-examples.test.ts | 152 +++++++++++++ .../common/no-invalid-parameter-examples.ts | 7 +- .../no-invalid-media-type-examples.test.ts | 200 ++++++++++++++++++ .../oas3/no-invalid-media-type-examples.ts | 18 +- packages/core/src/rules/utils.ts | 11 + 5 files changed, 379 insertions(+), 9 deletions(-) diff --git a/packages/core/src/rules/common/__tests__/no-invalid-parameter-examples.test.ts b/packages/core/src/rules/common/__tests__/no-invalid-parameter-examples.test.ts index 62e39b0e13..18d01c4d29 100644 --- a/packages/core/src/rules/common/__tests__/no-invalid-parameter-examples.test.ts +++ b/packages/core/src/rules/common/__tests__/no-invalid-parameter-examples.test.ts @@ -208,4 +208,156 @@ describe('no-invalid-parameter-examples', () => { ] `); }); + + it('should report invalid dataValue in parameter examples (OAS 3.2)', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.2.0 + paths: + /users: + get: + parameters: + - name: filter + in: query + schema: + type: object + properties: + name: + type: string + examples: + invalid: + dataValue: + name: 42 + `, + 'foobar.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ rules: { 'no-invalid-parameter-examples': 'error' } }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "from": { + "pointer": "#/paths/~1users/get/parameters/0", + "source": "foobar.yaml", + }, + "location": [ + { + "pointer": "#/paths/~1users/get/parameters/0/examples/invalid/name", + "reportOnKey": false, + "source": "foobar.yaml", + }, + ], + "message": "Example value must conform to the schema: \`name\` property type must be string.", + "reference": "https://redocly.com/docs/cli/rules/oas/no-invalid-parameter-examples", + "ruleId": "no-invalid-parameter-examples", + "severity": "error", + "suggest": [], + }, + ] + `); + }); + + it('should validate boolean query parameter dataValue and ignore serializedValue (OAS 3.2)', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.2.0 + paths: + /search: + get: + parameters: + - name: exact + in: query + schema: + type: boolean + examples: + invalid: + dataValue: "true" + serializedValue: "true" + `, + 'foobar.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ rules: { 'no-invalid-parameter-examples': 'error' } }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "from": { + "pointer": "#/paths/~1search/get/parameters/0", + "source": "foobar.yaml", + }, + "location": [ + { + "pointer": "#/paths/~1search/get/parameters/0/examples/invalid", + "reportOnKey": false, + "source": "foobar.yaml", + }, + ], + "message": "Example value must conform to the schema: type must be boolean.", + "reference": "https://redocly.com/docs/cli/rules/oas/no-invalid-parameter-examples", + "ruleId": "no-invalid-parameter-examples", + "severity": "error", + "suggest": [], + }, + ] + `); + }); + + it('should still validate legacy value in OAS 3.2 examples (backward compatibility)', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.2.0 + paths: + /users: + get: + parameters: + - name: age + in: query + schema: + type: integer + examples: + invalid: + value: "not-a-number" + `, + 'foobar.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ rules: { 'no-invalid-parameter-examples': 'error' } }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(` + [ + { + "from": { + "pointer": "#/paths/~1users/get/parameters/0", + "source": "foobar.yaml", + }, + "location": [ + { + "pointer": "#/paths/~1users/get/parameters/0/examples/invalid", + "reportOnKey": false, + "source": "foobar.yaml", + }, + ], + "message": "Example value must conform to the schema: type must be integer.", + "reference": "https://redocly.com/docs/cli/rules/oas/no-invalid-parameter-examples", + "ruleId": "no-invalid-parameter-examples", + "severity": "error", + "suggest": [], + }, + ] + `); + }); }); diff --git a/packages/core/src/rules/common/no-invalid-parameter-examples.ts b/packages/core/src/rules/common/no-invalid-parameter-examples.ts index c152364f74..ab52ba8a14 100644 --- a/packages/core/src/rules/common/no-invalid-parameter-examples.ts +++ b/packages/core/src/rules/common/no-invalid-parameter-examples.ts @@ -4,7 +4,7 @@ import { isPlainObject } from '../../utils/is-plain-object.js'; import type { Oas2Rule, Oas3Rule } from '../../visitors.js'; import type { UserContext } from '../../walk.js'; import { AjvValidator } from '../ajv.js'; -import { validateExample } from '../utils.js'; +import { getExampleValueToValidate, validateExample } from '../utils.js'; export const NoInvalidParameterExamples: Oas3Rule | Oas2Rule = (opts) => { const validator = new AjvValidator(); @@ -28,9 +28,10 @@ export const NoInvalidParameterExamples: Oas3Rule | Oas2Rule = (opts) => { if (isPlainObject(parameter.examples)) { for (const [key, example] of Object.entries(parameter.examples)) { - if (isPlainObject(example) && 'value' in example) { + const selected = getExampleValueToValidate(example); + if (selected) { validateExample({ - example: example.value, + example: selected.value, schema: parameter.schema!, options: { location: ctx.location.child(['examples', key]), 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 5eca53839e..4bc4677c53 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 @@ -1025,4 +1025,204 @@ describe('no-invalid-media-type-examples', () => { expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); }); + + it('should report on invalid dataValue in examples (OAS 3.2)', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.2.0 + paths: + /pet: + get: + responses: + '200': + content: + application/json: + schema: + type: object + properties: + a: + type: string + b: + type: number + examples: + first: + dataValue: + a: 0 + b: "0" + `, + 'foobar.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ rules: { '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/dataValue/a", + "reportOnKey": false, + "source": "foobar.yaml", + }, + ], + "message": "Example value must conform to the schema: \`a\` property type must be string.", + "reference": "https://redocly.com/docs/cli/rules/oas/no-invalid-media-type-examples", + "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/dataValue/b", + "reportOnKey": false, + "source": "foobar.yaml", + }, + ], + "message": "Example value must conform to the schema: \`b\` property type must be number.", + "reference": "https://redocly.com/docs/cli/rules/oas/no-invalid-media-type-examples", + "ruleId": "no-invalid-media-type-examples", + "severity": "error", + "suggest": [], + }, + ] + `); + }); + + it('should not report on valid dataValue (OAS 3.2)', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.2.0 + paths: + /pet: + get: + responses: + '200': + content: + application/json: + schema: + type: object + properties: + a: + type: string + b: + type: number + examples: + first: + dataValue: + a: "string" + b: 13 + `, + 'foobar.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ rules: { 'no-invalid-media-type-examples': 'error' } }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); + + it('should validate dataValue referenced via $ref (OAS 3.2)', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.2.0 + components: + examples: + bad: + dataValue: + a: 23 + paths: + /pet: + get: + responses: + '200': + content: + application/json: + schema: + type: object + properties: + a: + type: string + examples: + first: + $ref: '#/components/examples/bad' + `, + 'foobar.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ rules: { '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": "#/components/examples/bad/dataValue/a", + "reportOnKey": false, + "source": "foobar.yaml", + }, + ], + "message": "Example value must conform to the schema: \`a\` property type must be string.", + "reference": "https://redocly.com/docs/cli/rules/oas/no-invalid-media-type-examples", + "ruleId": "no-invalid-media-type-examples", + "severity": "error", + "suggest": [], + }, + ] + `); + }); + + it('should not report a valid binary dataValue (OAS 3.2)', async () => { + const document = parseYamlToDocument( + outdent` + openapi: 3.2.0 + paths: + /pet: + get: + responses: + '200': + content: + image/png: + schema: + type: string + contentEncoding: base64 + contentMediaType: image/png + examples: + first: + dataValue: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8AAAMBAQDJ/pLvAAAAAElFTkSuQmCC" + `, + 'foobar.yaml' + ); + + const results = await lintDocument({ + externalRefResolver: new BaseResolver(), + document, + config: await createConfig({ rules: { 'no-invalid-media-type-examples': 'error' } }), + }); + + expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`); + }); }); 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 83a21af0b1..f2bbc347f3 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 @@ -7,7 +7,7 @@ import { isPlainObject } from '../../utils/is-plain-object.js'; import type { Oas3Rule } from '../../visitors.js'; import type { UserContext } from '../../walk.js'; import { AjvValidator } from '../ajv.js'; -import { validateExample } from '../utils.js'; +import { getExampleValueToValidate, validateExample } from '../utils.js'; export const ValidContentExamples: Oas3Rule = (opts) => { const validator = new AjvValidator(); @@ -25,7 +25,7 @@ export const ValidContentExamples: Oas3Rule = (opts) => { for (const exampleName of Object.keys(mediaType.examples)) { resolveAndValidateExample( mediaType.examples[exampleName], - location.child(['examples', exampleName, 'value']), + location.child(['examples', exampleName]), true ); } @@ -39,14 +39,20 @@ export const ValidContentExamples: Oas3Rule = (opts) => { if (isRef(example)) { const resolved = resolve(example); if (!resolved.location) return; - location = isMultiple ? resolved.location.child('value') : resolved.location; + location = resolved.location; example = resolved.node; } - if (isMultiple && typeof example?.value === 'undefined') { - return; + + let exampleValue = example; + if (isMultiple) { + const selected = getExampleValueToValidate(example); + if (!selected) return; + exampleValue = selected.value; + location = location.child(selected.field); } + validateExample({ - example: isMultiple ? example.value : example, + example: exampleValue, schema: mediaType.schema!, options: { location, diff --git a/packages/core/src/rules/utils.ts b/packages/core/src/rules/utils.ts index e3285fcc76..c389c402e0 100644 --- a/packages/core/src/rules/utils.ts +++ b/packages/core/src/rules/utils.ts @@ -3,6 +3,7 @@ import { default as levenshtein } from 'js-levenshtein'; import { isRef, Location } from '../ref-utils.js'; import type { + Oas3Example, Oas3Schema, Oas3Tag, Oas3_2Tag, @@ -10,6 +11,7 @@ import type { Referenced, } from '../typings/openapi.js'; import type { Oas2Tag } from '../typings/swagger.js'; +import { isDefined } from '../utils/is-defined.js'; import { isPlainObject } from '../utils/is-plain-object.js'; import type { NonUndefined, UserContext } from '../walk.js'; import type { AjvValidator } from './ajv.js'; @@ -166,6 +168,15 @@ export function getSuggest(given: string, variants: string[]): string[] { return distances.map((d) => d.variant); } +export function getExampleValueToValidate( + example: unknown +): { value: unknown; field: 'dataValue' | 'value' } | undefined { + if (!isPlainObject(example)) return undefined; + if (isDefined(example.dataValue)) return { value: example.dataValue, field: 'dataValue' }; + if (isDefined(example.value)) return { value: example.value, field: 'value' }; + return undefined; +} + export function validateExample({ example, schema, From 8822ffbbc81b83971fc2a7c815845c14b1e9540b Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Fri, 10 Jul 2026 13:39:10 +0300 Subject: [PATCH 2/4] docs: update --- .../oas/no-invalid-media-type-examples.md | 51 +++++++++++++++++++ .../oas/no-invalid-parameter-examples.md | 43 ++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/docs/@v2/rules/oas/no-invalid-media-type-examples.md b/docs/@v2/rules/oas/no-invalid-media-type-examples.md index 9f8967e9f9..673e1ed655 100644 --- a/docs/@v2/rules/oas/no-invalid-media-type-examples.md +++ b/docs/@v2/rules/oas/no-invalid-media-type-examples.md @@ -146,6 +146,57 @@ post: color: red ``` +### OpenAPI 3.2 `dataValue` + +In OpenAPI 3.2, provide the structured example in `dataValue`, which is validated against the schema the same way `value` is. +When `dataValue` is present, `value` must be absent (see [spec-example-values](./spec-example-values.md)). + +Example of an **incorrect** `dataValue`: + +```yaml +post: + requestBody: + content: + application/json: + schema: + type: object + properties: + make: + type: string + year: + type: integer + examples: + tesla: + summary: Red Tesla + dataValue: + make: Tesla + year: '2022' +``` + +> This example produces an error because the year is a string instead of an integer. + +Example of a **correct** `dataValue`: + +```yaml +post: + requestBody: + content: + application/json: + schema: + type: object + properties: + make: + type: string + year: + type: integer + examples: + tesla: + summary: Red Tesla + dataValue: + make: Tesla + year: 2022 +``` + ## Related rules - [no-invalid-parameter-examples](./no-invalid-parameter-examples.md) diff --git a/docs/@v2/rules/oas/no-invalid-parameter-examples.md b/docs/@v2/rules/oas/no-invalid-parameter-examples.md index e6c35ff896..82715b8e56 100644 --- a/docs/@v2/rules/oas/no-invalid-parameter-examples.md +++ b/docs/@v2/rules/oas/no-invalid-parameter-examples.md @@ -100,6 +100,49 @@ paths: example: ella ``` +### OpenAPI 3.2 `dataValue` + +In OpenAPI 3.2, provide the structured example in `dataValue`, which is validated against the schema the same way `value` is. +When `dataValue` is present, `value` must be absent (see [spec-example-values](./spec-example-values.md)). + +Example of an **incorrect** `dataValue`: + +```yaml +paths: + /results: + get: + summary: Search Chess Results + operationId: searchChessResult + parameters: + - name: rating + in: query + schema: + type: integer + examples: + grandmaster: + dataValue: '2500' +``` + +> This example produces an error because the rating is a string instead of an integer. + +Example of a **correct** `dataValue`: + +```yaml +paths: + /results: + get: + summary: Search Chess Results + operationId: searchChessResult + parameters: + - name: rating + in: query + schema: + type: integer + examples: + grandmaster: + dataValue: 2500 +``` + ## Related rules - [no-invalid-media-type-examples](./no-invalid-media-type-examples.md) From 4398fb7e66bfd657444f8f906c16fdc2cc974009 Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Fri, 10 Jul 2026 13:52:45 +0300 Subject: [PATCH 3/4] chore: add changeset --- .changeset/public-ravens-taste.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/public-ravens-taste.md diff --git a/.changeset/public-ravens-taste.md b/.changeset/public-ravens-taste.md new file mode 100644 index 0000000000..7875d24b7c --- /dev/null +++ b/.changeset/public-ravens-taste.md @@ -0,0 +1,5 @@ +--- +'@redocly/openapi-core': minor +--- + +Added linting for the OpenAPI 3.2 Example Object `dataValue` field. From 5a2caee3a7465645a440ea02fa00e2da9c2a7f24 Mon Sep 17 00:00:00 2001 From: Daryna Pastushenko Date: Thu, 16 Jul 2026 13:03:33 +0300 Subject: [PATCH 4/4] chore: address to bugbot comment --- .../no-invalid-parameter-examples.test.ts | 10 ++++---- .../common/no-invalid-parameter-examples.ts | 2 +- .../snapshot.txt | 25 ++++++------------- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/core/src/rules/common/__tests__/no-invalid-parameter-examples.test.ts b/packages/core/src/rules/common/__tests__/no-invalid-parameter-examples.test.ts index 18d01c4d29..ed4bb12f4a 100644 --- a/packages/core/src/rules/common/__tests__/no-invalid-parameter-examples.test.ts +++ b/packages/core/src/rules/common/__tests__/no-invalid-parameter-examples.test.ts @@ -100,7 +100,7 @@ describe('no-invalid-parameter-examples', () => { }, "location": [ { - "pointer": "#/paths/~1users/get/parameters/0/examples/invalid/extraProperty", + "pointer": "#/paths/~1users/get/parameters/0/examples/invalid/value/extraProperty", "reportOnKey": true, "source": "foobar.yaml", }, @@ -194,7 +194,7 @@ describe('no-invalid-parameter-examples', () => { }, "location": [ { - "pointer": "#/paths/~1users/get/parameters/0/examples/invalid/readOnlyProp", + "pointer": "#/paths/~1users/get/parameters/0/examples/invalid/value/readOnlyProp", "reportOnKey": false, "source": "foobar.yaml", }, @@ -247,7 +247,7 @@ describe('no-invalid-parameter-examples', () => { }, "location": [ { - "pointer": "#/paths/~1users/get/parameters/0/examples/invalid/name", + "pointer": "#/paths/~1users/get/parameters/0/examples/invalid/dataValue/name", "reportOnKey": false, "source": "foobar.yaml", }, @@ -297,7 +297,7 @@ describe('no-invalid-parameter-examples', () => { }, "location": [ { - "pointer": "#/paths/~1search/get/parameters/0/examples/invalid", + "pointer": "#/paths/~1search/get/parameters/0/examples/invalid/dataValue", "reportOnKey": false, "source": "foobar.yaml", }, @@ -346,7 +346,7 @@ describe('no-invalid-parameter-examples', () => { }, "location": [ { - "pointer": "#/paths/~1users/get/parameters/0/examples/invalid", + "pointer": "#/paths/~1users/get/parameters/0/examples/invalid/value", "reportOnKey": false, "source": "foobar.yaml", }, diff --git a/packages/core/src/rules/common/no-invalid-parameter-examples.ts b/packages/core/src/rules/common/no-invalid-parameter-examples.ts index ab52ba8a14..6c19f5d0e1 100644 --- a/packages/core/src/rules/common/no-invalid-parameter-examples.ts +++ b/packages/core/src/rules/common/no-invalid-parameter-examples.ts @@ -34,7 +34,7 @@ export const NoInvalidParameterExamples: Oas3Rule | Oas2Rule = (opts) => { example: selected.value, schema: parameter.schema!, options: { - location: ctx.location.child(['examples', key]), + location: ctx.location.child(['examples', key, selected.field]), ctx, validator, allowAdditionalProperties: !!opts.allowAdditionalProperties, diff --git a/tests/e2e/lint/no-invalid-parameter-examples-read-write-only/snapshot.txt b/tests/e2e/lint/no-invalid-parameter-examples-read-write-only/snapshot.txt index 674d56be85..0fb6b29cb1 100644 --- a/tests/e2e/lint/no-invalid-parameter-examples-read-write-only/snapshot.txt +++ b/tests/e2e/lint/no-invalid-parameter-examples-read-write-only/snapshot.txt @@ -1,18 +1,13 @@ -[1] openapi.yaml:25:15 at #/paths/~1test/get/parameters/0/examples/invalid_readOnly_present/id +[1] openapi.yaml:27:21 at #/paths/~1test/get/parameters/0/examples/invalid_readOnly_present/value/id Example value must conform to the schema: `id` property must NOT be present in request context. -23 | -24 | invalid_readOnly_present: -25 | summary: readOnly must NOT be present in request (query parameter) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -26 | value: - | ^^^^^^ - … | < 2 more lines > -29 | password: p - | ^^^^^^^^^^^ -30 | -31 | invalid_missing_writeOnly_required: +25 | summary: readOnly must NOT be present in request (query parameter) +26 | value: +27 | id: usr_1 + | ^^^^^ +28 | name: Doe +29 | password: p referenced from openapi.yaml:12:11 at #/paths/~1test/get/parameters/0 @@ -21,16 +16,12 @@ Error was generated by the no-invalid-parameter-examples rule. Reference: https://redocly.com/docs/cli/rules/oas/no-invalid-parameter-examples -[2] openapi.yaml:32:15 at #/paths/~1test/get/parameters/0/examples/invalid_missing_writeOnly_required +[2] openapi.yaml:34:17 at #/paths/~1test/get/parameters/0/examples/invalid_missing_writeOnly_required/value Example value must conform to the schema: must have required property 'password'. -30 | -31 | invalid_missing_writeOnly_required: 32 | summary: missing required writeOnly `password` - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 33 | value: - | ^^^^^^ 34 | name: Doe | ^^^^^^^^^ 35 |