Skip to content

Commit

Permalink
feat: add schema type mismatch rule (#1890)
Browse files Browse the repository at this point in the history
  • Loading branch information
ImBIOS authored Feb 12, 2025
1 parent d449fa4 commit e1c065b
Show file tree
Hide file tree
Showing 13 changed files with 294 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/good-ducks-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@redocly/openapi-core": minor
---

Added the `no-schema-type-mismatch` rule.
1 change: 1 addition & 0 deletions docs/rules/built-in-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ The rules list is split into sections.
- [no-invalid-media-type-examples](./oas/no-invalid-media-type-examples.md): Example request bodies must match the declared schema
- [no-invalid-schema-examples](./oas/no-invalid-schema-examples.md): Schema examples must match declared types
- [no-required-schema-properties-undefined](./oas/no-required-schema-properties-undefined.md): All properties marked as required must be defined
- [no-schema-type-mismatch](./oas/no-schema-type-mismatch.md): Detects schemas with type mismatches between object and items fields, and array and properties fields.
- [request-mime-type](./oas/request-mime-type.md): Configure allowed mime types for requests
- [response-mime-type](./oas/response-mime-type.md): Configure allowed mime types for responses
- [response-contains-header](./oas/response-contains-header.md): List headers that must be included with specific response types
Expand Down
112 changes: 112 additions & 0 deletions docs/rules/oas/no-schema-type-mismatch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
slug: /docs/cli/rules/oas/no-schema-type-mismatch
---

# no-schema-type-mismatch

Ensures that a schema's structural properties match its declared `type`. In particular:

- A schema of type `object` **must not** include an `items` field.
- A schema of type `array` **must not** include a `properties` field.

| OAS | Compatibility |
| --- | ------------- |
| 2.0 ||
| 3.0 ||
| 3.1 ||

```mermaid
flowchart TD
Schema -->|if type is object| CheckItems["'items' field exists?"]
Schema -->|if type is array| CheckProps["'properties' field exists?"]
```

## API design principles

When designing an API schema, the defined `type` should be consistent with its structure:

- **Objects** are collections of key/value pairs. They should be defined using `properties` (or additionalProperties) and must not use `items`.
- **Arrays** are ordered lists of items and must use `items` to define their content. Including `properties` is invalid.

This rule helps catch typos and misconfigurations early in your API definition.

## Configuration

| Option | Type | Description |
| -------- | ------ | --------------------------------------------------------------------------------------------- |
| severity | string | Possible values: `off`, `warn`, `error`. Default is `error` in the recommended configuration. |

Example configuration:

```yaml
rules:
no-schema-type-mismatch: error
```
## Examples
### Incorrect Examples
#### Object type with an `items` field

```yaml
properties:
user:
type: object
properties:
id:
type: string
items:
type: number
```

_Error:_ An `object` type should not include an `items` field.

#### Array type with a `properties` field

```yaml
properties:
tags:
type: array
properties:
name:
type: string
```

_Error:_ An `array` type should not include a `properties` field.

### Correct Examples

#### Object type with proper `properties`

```yaml
properties:
user:
type: object
properties:
id:
type: string
name:
type: string
```

#### Array type with proper `items`

```yaml
properties:
tags:
type: array
items:
type: string
```

## Related rules

- [configurable rules](../configurable-rules.md)
- [no-invalid-media-type-examples](./no-invalid-media-type-examples.md)
- [no-invalid-parameter-examples](./no-invalid-parameter-examples.md)
- [no-invalid-schema-examples](./no-invalid-schema-examples.md)

## Resources

- [Rule source](https://github.com/Redocly/redocly-cli/blob/main/packages/core/src/rules/common/no-schema-type-mismatch.ts)
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ exports[`resolveConfig should ignore minimal from the root and read local file 1
"no-invalid-schema-examples": "off",
"no-path-trailing-slash": "error",
"no-required-schema-properties-undefined": "off",
"no-schema-type-mismatch": "warn",
"no-unresolved-refs": "error",
"operation-2xx-response": "warn",
"operation-4xx-response": "error",
Expand Down Expand Up @@ -114,6 +115,7 @@ exports[`resolveConfig should ignore minimal from the root and read local file 1
"no-invalid-schema-examples": "off",
"no-path-trailing-slash": "error",
"no-required-schema-properties-undefined": "off",
"no-schema-type-mismatch": "warn",
"no-server-example.com": "warn",
"no-server-trailing-slash": "error",
"no-server-variables-empty-enum": "error",
Expand Down Expand Up @@ -173,6 +175,7 @@ exports[`resolveConfig should ignore minimal from the root and read local file 1
"no-invalid-schema-examples": "off",
"no-path-trailing-slash": "error",
"no-required-schema-properties-undefined": "off",
"no-schema-type-mismatch": "warn",
"no-server-example.com": "warn",
"no-server-trailing-slash": "error",
"no-server-variables-empty-enum": "error",
Expand Down Expand Up @@ -285,6 +288,7 @@ exports[`resolveStyleguideConfig should resolve extends with local file config w
"no-invalid-schema-examples": "off",
"no-path-trailing-slash": "error",
"no-required-schema-properties-undefined": "off",
"no-schema-type-mismatch": "warn",
"no-unresolved-refs": "error",
"operation-2xx-response": "error",
"operation-4xx-response": "off",
Expand Down Expand Up @@ -340,6 +344,7 @@ exports[`resolveStyleguideConfig should resolve extends with local file config w
"no-invalid-schema-examples": "off",
"no-path-trailing-slash": "error",
"no-required-schema-properties-undefined": "off",
"no-schema-type-mismatch": "warn",
"no-server-example.com": "warn",
"no-server-trailing-slash": "error",
"no-server-variables-empty-enum": "error",
Expand Down Expand Up @@ -399,6 +404,7 @@ exports[`resolveStyleguideConfig should resolve extends with local file config w
"no-invalid-schema-examples": "off",
"no-path-trailing-slash": "error",
"no-required-schema-properties-undefined": "off",
"no-schema-type-mismatch": "warn",
"no-server-example.com": "warn",
"no-server-trailing-slash": "error",
"no-server-variables-empty-enum": "error",
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/config/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const all: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'error',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'error',
'no-schema-type-mismatch': 'error',
'operation-summary': 'error',
'operation-operationId': 'error',
'operation-operationId-unique': 'error',
Expand Down Expand Up @@ -75,6 +76,7 @@ const all: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'error',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'error',
'no-schema-type-mismatch': 'error',
'no-invalid-media-type-examples': 'error',
'no-server-example.com': 'error',
'no-server-trailing-slash': 'error',
Expand Down Expand Up @@ -141,6 +143,7 @@ const all: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'error',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'error',
'no-schema-type-mismatch': 'error',
'no-invalid-media-type-examples': 'error',
'no-server-example.com': 'error',
'no-server-trailing-slash': 'error',
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/config/minimal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const minimal: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'warn',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'off',
'no-schema-type-mismatch': 'off',
'operation-summary': 'warn',
'operation-operationId': 'warn',
'operation-operationId-unique': 'warn',
Expand Down Expand Up @@ -66,6 +67,7 @@ const minimal: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'warn',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'off',
'no-schema-type-mismatch': 'off',
'no-invalid-media-type-examples': {
severity: 'warn',
allowAdditionalProperties: false,
Expand Down Expand Up @@ -126,6 +128,7 @@ const minimal: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'warn',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'off',
'no-schema-type-mismatch': 'off',
'no-invalid-media-type-examples': 'warn',
'no-server-example.com': 'warn',
'no-server-trailing-slash': 'error',
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/config/recommended-strict.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'error',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'off',
'no-schema-type-mismatch': 'error',
'operation-summary': 'error',
'operation-operationId': 'error',
'operation-operationId-unique': 'error',
Expand Down Expand Up @@ -66,6 +67,7 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'error',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'off',
'no-schema-type-mismatch': 'error',
'no-invalid-media-type-examples': {
severity: 'error',
allowAdditionalProperties: false,
Expand Down Expand Up @@ -126,6 +128,7 @@ const recommendedStrict: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'error',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'off',
'no-schema-type-mismatch': 'error',
'no-invalid-media-type-examples': 'error',
'no-server-example.com': 'error',
'no-server-trailing-slash': 'error',
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/config/recommended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const recommended: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'error',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'off',
'no-schema-type-mismatch': 'warn',
'operation-summary': 'error',
'operation-description': 'off',
'operation-operationId': 'warn',
Expand Down Expand Up @@ -66,6 +67,7 @@ const recommended: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'error',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'off',
'no-schema-type-mismatch': 'warn',
'no-invalid-media-type-examples': {
severity: 'warn',
allowAdditionalProperties: false,
Expand Down Expand Up @@ -126,6 +128,7 @@ const recommended: PluginStyleguideConfig<'built-in'> = {
'no-enum-type-mismatch': 'error',
'no-unresolved-refs': 'error',
'no-required-schema-properties-undefined': 'off',
'no-schema-type-mismatch': 'warn',
'no-invalid-media-type-examples': 'warn',
'no-server-example.com': 'warn',
'no-server-trailing-slash': 'error',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { outdent } from 'outdent';
import { makeConfig, parseYamlToDocument, replaceSourceWithRef } from '../../../../__tests__/utils';
import { lintDocument } from '../../../lint';
import { BaseResolver } from '../../../resolve';

describe('no-schema-type-mismatch rule', () => {
it('should report a warning for object type with items field', async () => {
const yaml = outdent`
openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
items:
type: string
`;

const document = parseYamlToDocument(yaml, 'test.yaml');
const results = await lintDocument({
document,
externalRefResolver: new BaseResolver(),
config: await makeConfig({ rules: { 'no-schema-type-mismatch': 'warn' } }),
});

expect(replaceSourceWithRef(results)).toEqual([
{
location: [
{
pointer: '#/paths/~1test/get/responses/200/content/application~1json/schema/items',
reportOnKey: false,
source: 'test.yaml',
},
],
message: "Schema type mismatch: 'object' type should not contain 'items' field.",
ruleId: 'no-schema-type-mismatch',
severity: 'warn',
suggest: [],
},
]);
});

it('should report a warning for array type with properties field', async () => {
const yaml = outdent`
openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
properties:
name:
type: string
`;

const document = parseYamlToDocument(yaml, 'test.yaml');
const results = await lintDocument({
document,
externalRefResolver: new BaseResolver(),
config: await makeConfig({ rules: { 'no-schema-type-mismatch': 'warn' } }),
});

expect(replaceSourceWithRef(results)).toEqual([
{
location: [
{
pointer: '#/paths/~1test/get/responses/200/content/application~1json/schema/properties',
reportOnKey: false,
source: 'test.yaml',
},
],
message: "Schema type mismatch: 'array' type should not contain 'properties' field.",
ruleId: 'no-schema-type-mismatch',
severity: 'warn',
suggest: [],
},
]);
});

it('should not report a warning for valid schemas', async () => {
const yaml = outdent`
openapi: 3.0.0
info:
title: Test API
version: 1.0.0
paths:
/test:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
name:
type: string
`;

const document = parseYamlToDocument(yaml, 'test.yaml');
const results = await lintDocument({
document,
externalRefResolver: new BaseResolver(),
config: await makeConfig({ rules: { 'no-schema-type-mismatch': 'warn' } }),
});

expect(replaceSourceWithRef(results)).toEqual([]);
});
});
Loading

1 comment on commit e1c065b

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.
Category Percentage Covered / Total
🟡 Statements 78.54% 5062/6445
🟡 Branches 67.24% 2065/3071
🟡 Functions 73.2% 836/1142
🟡 Lines 78.82% 4775/6058

Test suite run success

839 tests passing in 121 suites.

Report generated by 🧪jest coverage report action from e1c065b

Please sign in to comment.