Skip to content

Commit 9837f55

Browse files
pengyingclaude
andauthored
feat: add Spectral lint rules enforcing API design guidelines (#317)
Add Spectral OpenAPI linter with rules matching openapi/README.md: - field-names-camelCase: schema properties must be camelCase (error) - enum-values-upper-snake-case: enum values must be UPPER_SNAKE_CASE (error) - query/path-params-camelCase: parameter names must be camelCase (error) - paths-kebab-case: path segments must be kebab-case (error) - no-inline-request/response-schema: must use $ref not inline (error) - oneOf-must-have-discriminator: oneOf needs discriminator (warn) - pagination-envelope-has-data/hasMore: list responses need envelope (warn) - delete-returns-204: DELETE should return 204 (warn) - schema-properties-have-descriptions: properties need descriptions (warn) - schema-properties-have-examples: properties should have examples (info) Also adds "Avoid Inline Schemas" section to openapi/README.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 334f5bd commit 9837f55

5 files changed

Lines changed: 817 additions & 24 deletions

File tree

.spectral.yaml

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
extends:
2+
- spectral:oas
3+
4+
rules:
5+
# Disable built-in rules already covered by Redocly
6+
info-contact: off
7+
info-description: off
8+
operation-tag-defined: off
9+
oas3-api-servers: off
10+
11+
# ============================================================
12+
# Naming Conventions (README: Naming Conventions)
13+
# ============================================================
14+
15+
# Fields must be camelCase (excludes TestWebhookResponse which mirrors external format)
16+
field-names-camelCase:
17+
description: Schema property names must be camelCase
18+
message: "Property '{{property}}' must be camelCase. See openapi/README.md#field-naming"
19+
severity: error
20+
given: "$.components.schemas[?(@property != 'TestWebhookResponse')].properties"
21+
then:
22+
field: "@key"
23+
function: casing
24+
functionOptions:
25+
type: camel
26+
27+
# Enum values must be UPPER_SNAKE_CASE (dots allowed for webhook namespacing)
28+
enum-values-upper-snake-case:
29+
description: Enum values must be UPPER_SNAKE_CASE
30+
message: "Enum value '{{value}}' must be UPPER_SNAKE_CASE. See openapi/README.md#field-naming"
31+
severity: error
32+
given: "$.components.schemas.*.enum[*]"
33+
then:
34+
function: pattern
35+
functionOptions:
36+
match: "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*(\\.[A-Z][A-Z0-9]*(_[A-Z0-9]+)*)*$"
37+
38+
# Query parameters must be camelCase
39+
query-params-camelCase:
40+
description: Query parameter names must be camelCase
41+
message: "Query parameter '{{value}}' must be camelCase. See openapi/README.md#field-naming"
42+
severity: error
43+
given: "$.paths.*.*.parameters[?(@.in=='query')].name"
44+
then:
45+
function: casing
46+
functionOptions:
47+
type: camel
48+
49+
# Path parameters must be camelCase
50+
path-params-camelCase:
51+
description: Path parameter names must be camelCase
52+
message: "Path parameter '{{value}}' must be camelCase. See openapi/README.md#field-naming"
53+
severity: error
54+
given: "$.paths.*.*.parameters[?(@.in=='path')].name"
55+
then:
56+
function: casing
57+
functionOptions:
58+
type: camel
59+
60+
# ============================================================
61+
# Discriminators and Polymorphism (README: OpenAPI Best Practices)
62+
# ============================================================
63+
64+
# oneOf must include a discriminator
65+
oneOf-must-have-discriminator:
66+
description: oneOf schemas must include a discriminator
67+
message: "oneOf without a discriminator. See openapi/README.md#discriminators-and-polymorphism"
68+
severity: warn
69+
given: "$.components.schemas[?(@.oneOf)]"
70+
then:
71+
field: discriminator
72+
function: truthy
73+
74+
# ============================================================
75+
# No Inline Schemas (README: Avoid Inline Schemas)
76+
# ============================================================
77+
78+
# Request bodies must use $ref, not inline schemas.
79+
# Note: Spectral resolves $refs in the bundled spec, so some component-level
80+
# false positives appear — the real violations are on paths.* entries.
81+
no-inline-request-schema:
82+
description: Request body schemas must use $ref, not inline definitions
83+
message: "Use $ref for request body schema instead of inline definition. See openapi/README.md#avoid-inline-schemas-in-request-and-response-definitions"
84+
severity: error
85+
given: "$.paths[*][get,post,put,patch,delete].requestBody.content[application/json].schema"
86+
then:
87+
field: "$ref"
88+
function: truthy
89+
90+
# Response bodies must use $ref, not inline schemas
91+
no-inline-response-schema:
92+
description: Response body schemas must use $ref, not inline definitions
93+
message: "Use $ref for response schema instead of inline definition. See openapi/README.md#avoid-inline-schemas-in-request-and-response-definitions"
94+
severity: error
95+
given: "$.paths[*][get,post,put,patch,delete].responses[*].content[application/json].schema"
96+
then:
97+
field: "$ref"
98+
function: truthy
99+
100+
# ============================================================
101+
# Pagination (README: Pagination)
102+
# ============================================================
103+
104+
# GET list endpoints returning arrays should use pagination envelope
105+
pagination-envelope-has-data:
106+
description: Paginated responses must include a 'data' array field
107+
message: "List response missing 'data' field. See openapi/README.md#pagination"
108+
severity: warn
109+
given: "$.paths.*.get.responses.200.content.application/json.schema.properties"
110+
then:
111+
field: data
112+
function: truthy
113+
114+
pagination-envelope-has-hasMore:
115+
description: Paginated responses must include a 'hasMore' boolean field
116+
message: "List response missing 'hasMore' field. See openapi/README.md#pagination"
117+
severity: warn
118+
given: "$.paths.*.get.responses.200.content.application/json.schema.properties[?(@.data)]"
119+
then:
120+
field: hasMore
121+
function: truthy
122+
123+
# ============================================================
124+
# HTTP Methods and Status Codes (README: HTTP Methods)
125+
# ============================================================
126+
127+
# DELETE operations should return 204
128+
delete-returns-204:
129+
description: DELETE operations should return 204 No Content
130+
message: "DELETE should return 204. See openapi/README.md#http-methods"
131+
severity: warn
132+
given: "$.paths.*.delete.responses"
133+
then:
134+
field: "204"
135+
function: truthy
136+
137+
# ============================================================
138+
# Documentation (README: Documentation in OpenAPI)
139+
# ============================================================
140+
141+
# Schema properties should have descriptions
142+
schema-properties-have-descriptions:
143+
description: Schema properties should have descriptions
144+
message: "Property '{{property}}' is missing a description. See openapi/README.md#documentation-in-openapi"
145+
severity: warn
146+
given: "$.components.schemas.*.properties.*"
147+
then:
148+
field: description
149+
function: truthy
150+
151+
# Schemas should have examples where appropriate
152+
schema-properties-have-examples:
153+
description: String and number schema properties should have examples
154+
message: "Property is missing an example. See openapi/README.md#documentation-in-openapi"
155+
severity: info
156+
given: "$.components.schemas.*.properties[?(@.type=='string' || @.type=='integer' || @.type=='number')]"
157+
then:
158+
field: example
159+
function: truthy
160+
161+
# ============================================================
162+
# Paths (README: Resources)
163+
# ============================================================
164+
165+
# Paths should use kebab-case (path params in {camelCase} are allowed)
166+
paths-kebab-case:
167+
description: Path segments should use kebab-case
168+
message: "Path should use kebab-case (e.g., /external-accounts not /externalAccounts). See openapi/README.md#resources"
169+
severity: error
170+
given: "$.paths"
171+
then:
172+
field: "@key"
173+
function: pattern
174+
functionOptions:
175+
match: "^(\\/([a-z0-9]+(-[a-z0-9]+)*|\\{[a-zA-Z0-9]+\\}))+$"

Makefile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: install build build-openapi mint lint lint-openapi lint-markdown cli-install cli-build cli
1+
.PHONY: install build build-openapi mint lint lint-openapi lint-spectral lint-markdown cli-install cli-build cli
22

33
install:
44
npm install
@@ -21,11 +21,13 @@ mint:
2121

2222
lint:
2323
npm run lint
24-
cd mintlify && mint openapi-check openapi.yaml
2524

2625
lint-openapi:
2726
npm run lint:openapi
2827

28+
lint-spectral:
29+
npx spectral lint openapi.yaml --fail-severity=error
30+
2931
lint-markdown:
3032
npm run lint:markdown
3133

@@ -36,4 +38,4 @@ cli-build:
3638
cd cli && npm run build
3739

3840
cli:
39-
cd cli && npm run dev --
41+
cd cli && npm run dev --

openapi/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,55 @@ The discriminator property must be listed in `required` in the **variant** schem
417417

418418
- Use `allOf` for extending base schemas
419419

420+
### Avoid Inline Schemas in Request and Response Definitions
421+
422+
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.
423+
424+
```yaml
425+
# ❌ Wrong — inline schema generates ugly SDK names like
426+
# "CreateCustomerExternalAccount200Response" or "CreateCustomerExternalAccountBody"
427+
post:
428+
requestBody:
429+
content:
430+
application/json:
431+
schema:
432+
type: object
433+
properties:
434+
currency:
435+
type: string
436+
accountInfo:
437+
type: object
438+
responses:
439+
'200':
440+
content:
441+
application/json:
442+
schema:
443+
type: object
444+
properties:
445+
id:
446+
type: string
447+
status:
448+
type: string
449+
```
450+
451+
```yaml
452+
# ✅ Correct — named schemas produce clean SDK types
453+
post:
454+
requestBody:
455+
content:
456+
application/json:
457+
schema:
458+
$ref: '../../components/schemas/external_accounts/ExternalAccountCreateRequest.yaml'
459+
responses:
460+
'200':
461+
content:
462+
application/json:
463+
schema:
464+
$ref: '../../components/schemas/external_accounts/ExternalAccount.yaml'
465+
```
466+
467+
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.
468+
420469
### Documentation in OpenAPI
421470

422471
- Add `description` to every endpoint, parameter, and schema field

0 commit comments

Comments
 (0)