Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions .spectral.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
extends:
- spectral:oas

rules:
# Disable built-in rules already covered by Redocly
info-contact: off
info-description: off
operation-tag-defined: off
oas3-api-servers: off

# ============================================================
# Naming Conventions (README: Naming Conventions)
# ============================================================

# Fields must be camelCase (excludes TestWebhookResponse which mirrors external format)
field-names-camelCase:
description: Schema property names must be camelCase
message: "Property '{{property}}' must be camelCase. See openapi/README.md#field-naming"
severity: error
given: "$.components.schemas[?(@property != 'TestWebhookResponse')].properties"
then:
field: "@key"
function: casing
functionOptions:
type: camel

# Enum values must be UPPER_SNAKE_CASE (dots allowed for webhook namespacing)
enum-values-upper-snake-case:
description: Enum values must be UPPER_SNAKE_CASE
message: "Enum value '{{value}}' must be UPPER_SNAKE_CASE. See openapi/README.md#field-naming"
severity: error
given: "$.components.schemas.*.enum[*]"
then:
function: pattern
functionOptions:
match: "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*(\\.[A-Z][A-Z0-9]*(_[A-Z0-9]+)*)*$"

# Query parameters must be camelCase
query-params-camelCase:
description: Query parameter names must be camelCase
message: "Query parameter '{{value}}' must be camelCase. See openapi/README.md#field-naming"
severity: error
given: "$.paths.*.*.parameters[?(@.in=='query')].name"
then:
function: casing
functionOptions:
type: camel

# Path parameters must be camelCase
path-params-camelCase:
description: Path parameter names must be camelCase
message: "Path parameter '{{value}}' must be camelCase. See openapi/README.md#field-naming"
severity: error
given: "$.paths.*.*.parameters[?(@.in=='path')].name"
then:
function: casing
functionOptions:
type: camel

# ============================================================
# Discriminators and Polymorphism (README: OpenAPI Best Practices)
# ============================================================

# oneOf must include a discriminator
oneOf-must-have-discriminator:
description: oneOf schemas must include a discriminator
message: "oneOf without a discriminator. See openapi/README.md#discriminators-and-polymorphism"
severity: warn
given: "$.components.schemas[?(@.oneOf)]"
then:
field: discriminator
function: truthy

# ============================================================
# No Inline Schemas (README: Avoid Inline Schemas)
# ============================================================

# Request bodies must use $ref, not inline schemas.
# Note: Spectral resolves $refs in the bundled spec, so some component-level
# false positives appear — the real violations are on paths.* entries.
no-inline-request-schema:
description: Request body schemas must use $ref, not inline definitions
message: "Use $ref for request body schema instead of inline definition. See openapi/README.md#avoid-inline-schemas-in-request-and-response-definitions"
severity: error
given: "$.paths[*][get,post,put,patch,delete].requestBody.content[application/json].schema"
then:
field: "$ref"
function: truthy

# Response bodies must use $ref, not inline schemas
no-inline-response-schema:
description: Response body schemas must use $ref, not inline definitions
message: "Use $ref for response schema instead of inline definition. See openapi/README.md#avoid-inline-schemas-in-request-and-response-definitions"
severity: error
given: "$.paths[*][get,post,put,patch,delete].responses[*].content[application/json].schema"
then:
field: "$ref"
function: truthy

# ============================================================
# Pagination (README: Pagination)
# ============================================================

# GET list endpoints returning arrays should use pagination envelope
pagination-envelope-has-data:
description: Paginated responses must include a 'data' array field
message: "List response missing 'data' field. See openapi/README.md#pagination"
severity: warn
given: "$.paths.*.get.responses.200.content.application/json.schema.properties"
then:
field: data
function: truthy

pagination-envelope-has-hasMore:
description: Paginated responses must include a 'hasMore' boolean field
message: "List response missing 'hasMore' field. See openapi/README.md#pagination"
severity: warn
given: "$.paths.*.get.responses.200.content.application/json.schema.properties[?(@.data)]"
then:
field: hasMore
function: truthy

# ============================================================
# HTTP Methods and Status Codes (README: HTTP Methods)
# ============================================================

# DELETE operations should return 204
delete-returns-204:
description: DELETE operations should return 204 No Content
message: "DELETE should return 204. See openapi/README.md#http-methods"
severity: warn
given: "$.paths.*.delete.responses"
then:
field: "204"
function: truthy

# ============================================================
# Documentation (README: Documentation in OpenAPI)
# ============================================================

# Schema properties should have descriptions
schema-properties-have-descriptions:
description: Schema properties should have descriptions
message: "Property '{{property}}' is missing a description. See openapi/README.md#documentation-in-openapi"
severity: warn
given: "$.components.schemas.*.properties.*"
then:
field: description
function: truthy

# Schemas should have examples where appropriate
schema-properties-have-examples:
description: String and number schema properties should have examples
message: "Property is missing an example. See openapi/README.md#documentation-in-openapi"
severity: info
given: "$.components.schemas.*.properties[?(@.type=='string' || @.type=='integer' || @.type=='number')]"
then:
field: example
function: truthy

# ============================================================
# Paths (README: Resources)
# ============================================================

# Paths should use kebab-case (path params in {camelCase} are allowed)
paths-kebab-case:
description: Path segments should use kebab-case
message: "Path should use kebab-case (e.g., /external-accounts not /externalAccounts). See openapi/README.md#resources"
severity: error
given: "$.paths"
then:
field: "@key"
function: pattern
functionOptions:
match: "^(\\/([a-z0-9]+(-[a-z0-9]+)*|\\{[a-zA-Z0-9]+\\}))+$"
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: install build build-openapi mint lint lint-openapi lint-markdown cli-install cli-build cli
.PHONY: install build build-openapi mint lint lint-openapi lint-spectral lint-markdown cli-install cli-build cli

install:
npm install
Expand All @@ -21,11 +21,13 @@ mint:

lint:
npm run lint
Comment on lines 22 to 23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Spectral not wired into the main lint target

The lint Make target calls npm run lint, which maps to only lint:openapi (Redocly). The new lint-spectral target is entirely separate, so CI (.github/workflows/lint.yml runs make lint) will never execute Spectral. This means the rules added in this PR are effectively advisory — a PR introducing a camelCase violation, missing discriminator, or inline schema will pass CI without complaint.

To enforce Spectral in CI, lint-spectral needs to be added as a dependency of lint (or chained in):

Suggested change
lint:
npm run lint
lint:
npm run lint
$(MAKE) lint-spectral

Alternatively, add a lint:spectral script to package.json and extend the lint script: "lint": "npm run lint:openapi && npx spectral lint openapi.yaml --fail-severity=error".

Note also that .spectral.yaml is absent from the paths trigger in .github/workflows/lint.yml, so changes to the rule file alone won't re-run CI.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Makefile
Line: 22-23

Comment:
**Spectral not wired into the main `lint` target**

The `lint` Make target calls `npm run lint`, which maps to only `lint:openapi` (Redocly). The new `lint-spectral` target is entirely separate, so CI (`.github/workflows/lint.yml` runs `make lint`) will never execute Spectral. This means the rules added in this PR are effectively advisory — a PR introducing a camelCase violation, missing discriminator, or inline schema will pass CI without complaint.

To enforce Spectral in CI, `lint-spectral` needs to be added as a dependency of `lint` (or chained in):

```suggestion
lint:
	npm run lint
	$(MAKE) lint-spectral
```

Alternatively, add a `lint:spectral` script to `package.json` and extend the `lint` script: `"lint": "npm run lint:openapi && npx spectral lint openapi.yaml --fail-severity=error"`.

Note also that `.spectral.yaml` is absent from the `paths` trigger in `.github/workflows/lint.yml`, so changes to the rule file alone won't re-run CI.

How can I resolve this? If you propose a fix, please make it concise.

cd mintlify && mint openapi-check openapi.yaml

lint-openapi:
npm run lint:openapi

lint-spectral:
npx spectral lint openapi.yaml --fail-severity=error

lint-markdown:
npm run lint:markdown

Expand All @@ -36,4 +38,4 @@ cli-build:
cd cli && npm run build

cli:
cd cli && npm run dev --
cd cli && npm run dev --
49 changes: 49 additions & 0 deletions openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,55 @@ The discriminator property must be listed in `required` in the **variant** schem

- Use `allOf` for extending base schemas

### Avoid Inline Schemas in Request and Response Definitions

Never define schemas inline within path request bodies or responses. Always use `$ref` to reference a named schema in `components/schemas/`. Inline schemas produce auto-generated names in SDKs based on the operation and HTTP status code, resulting in poor developer experience.

```yaml
# ❌ Wrong — inline schema generates ugly SDK names like
# "CreateCustomerExternalAccount200Response" or "CreateCustomerExternalAccountBody"
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
currency:
type: string
accountInfo:
type: object
responses:
'200':
content:
application/json:
schema:
type: object
properties:
id:
type: string
status:
type: string
```

```yaml
# ✅ Correct — named schemas produce clean SDK types
post:
requestBody:
content:
application/json:
schema:
$ref: '../../components/schemas/external_accounts/ExternalAccountCreateRequest.yaml'
responses:
'200':
content:
application/json:
schema:
$ref: '../../components/schemas/external_accounts/ExternalAccount.yaml'
```

This applies to all request bodies, response bodies, and nested objects within them. If a schema is used only once, it still belongs in `components/schemas/` with a descriptive name.

### Documentation in OpenAPI

- Add `description` to every endpoint, parameter, and schema field
Expand Down
Loading
Loading