Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
115 changes: 76 additions & 39 deletions specs/account-management.openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -673,45 +673,28 @@ paths:
schema:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
type:
type: string
access_level:
type: integer
resources:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
type:
type: string
access_level:
type: integer
resources:
type: array
items:
nullable: true
type: object
$ref: '#/components/schemas/PermissionResourceNode'
example:
- id: 4001
name: My First Project
type: project
access_level: 1
- id: 1774100
name: Account
type: account
access_level: 100
resources:
- id: 3816
name: My First Inbox
type: inbox
- id: 1774100
name: Templates
type: email_template_permission_scope
access_level: 100
resources: []
- id: 2550469
name: My Project
type: project
access_level: 100
resources:
- id: 4117907
name: My Inbox
type: inbox
access_level: 100
resources: []
'401':
$ref: '#/components/responses/UNAUTHENTICATED'
'403':
Expand Down Expand Up @@ -1061,7 +1044,13 @@ paths:
'401':
$ref: '#/components/responses/UNAUTHENTICATED'
'403':
$ref: '#/components/responses/PERMISSION_DENIED'
description: Insufficient organization permissions or wrong organization.
content:
application/json:
schema:
$ref: '#/components/schemas/PermissionsDeniedResponse'
example:
errors: Access forbidden
post:
operationId: createOrganizationSubAccount
summary: Create organization sub account
Expand Down Expand Up @@ -1117,7 +1106,13 @@ paths:
'401':
$ref: '#/components/responses/UNAUTHENTICATED'
'403':
$ref: '#/components/responses/PERMISSION_DENIED'
description: Insufficient organization permissions or wrong organization.
content:
application/json:
schema:
$ref: '#/components/schemas/PermissionsDeniedResponse'
example:
errors: Access forbidden
'422':
description: Unprocessable entity - validation errors.
content:
Expand All @@ -1141,6 +1136,25 @@ components:
type: string
example: Development Team Account
description: The name of the sub account
PermissionResourceNode:
title: Permission resource node
description: |
Nested resource tree for permission management. `type` values include `account`, `project`, `inbox`,
`sending_domain`, `email_template_permission_scope`, `email_campaign_permission_scope`, and others.
type: object
properties:
id:
type: integer
name:
type: string
type:
type: string
access_level:
type: integer
resources:
type: array
items:
$ref: '#/components/schemas/PermissionResourceNode'
AccountAccess:
title: AccountAccess
description: Assigns resource-specific permissions to a specifier.
Expand Down Expand Up @@ -1220,6 +1234,7 @@ components:
enum:
- account
- billing
- organization
- project
- inbox
- sending_domain
Expand Down Expand Up @@ -1259,14 +1274,20 @@ components:
type: string
description: Error message
example: Not Found
example:
error: Not Found
PermissionsDeniedResponse:
title: PermissionsDeniedResponse
type: object
properties:
errors:
type: string
description: Error message
example: Access forbidden
description: |
Human-readable reason. Common values include `Account access forbidden` (e.g. wrong account)
and `Access forbidden` (e.g. insufficient rights on an organization).
example: Account access forbidden
example:
errors: Account access forbidden
UnauthenticatedResponse:
title: UnauthenticatedResponse
type: object
Expand All @@ -1275,6 +1296,8 @@ components:
type: string
description: Error message
example: Incorrect API token
example:
error: Incorrect API token
ErrorResponse:
title: ErrorResponse
type: object
Expand All @@ -1290,6 +1313,14 @@ components:
errors:
type: object
description: Validation errors per attribute. Entire record errors are under `base` key.
additionalProperties:
type: array
items:
type: string
example:
errors:
base:
- No permissions selected. Please choose at least one.
ApiToken:
type: object
properties:
Expand Down Expand Up @@ -1459,18 +1490,24 @@ components:
application/json:
schema:
$ref: '#/components/schemas/UnauthenticatedResponse'
example:
error: Incorrect API token
PERMISSION_DENIED:
description: Returns forbidden error message. Check your permissions.
content:
application/json:
schema:
$ref: '#/components/schemas/PermissionsDeniedResponse'
example:
errors: Account access forbidden
NOT_FOUND:
description: Returns not found error message
content:
application/json:
schema:
$ref: '#/components/schemas/NotFoundResponse'
example:
error: Not Found
UNPROCESSABLE_ENTITY:
description: Validation error or other business rule violation
content:
Expand Down
49 changes: 43 additions & 6 deletions specs/contacts.openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,14 @@ paths:
'403':
$ref: '#/components/responses/PERMISSION_DENIED'
'404':
$ref: '#/components/responses/NOT_FOUND'
description: |-
No contact for the given identifier. The JSON body uses an `errors` string (not the `error` key used by some other 404 responses).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
errors: Contact(s) not found
'429':
$ref: '#/components/responses/LIMIT_EXCEEDED'
'500':
Expand Down Expand Up @@ -1787,15 +1794,15 @@ paths:
example:
- id: 6730
name: First name
type: text
data_type: text
merge_tag: first_name
- id: 6731
name: Birthdate
type: date
data_type: date
merge_tag: birthdate
- id: 6732
name: Cats count
type: integer
data_type: integer
merge_tag: cats_count
'401':
$ref: '#/components/responses/UNAUTHENTICATED'
Expand Down Expand Up @@ -2735,6 +2742,8 @@ components:
type: string
description: Error message
example: Unexpected error
example:
errors: Unexpected error
NotFoundResponse:
title: NotFoundResponse
type: object
Expand All @@ -2743,14 +2752,20 @@ components:
type: string
description: Error message
example: Not Found
example:
error: Not Found
PermissionsDeniedResponse:
title: PermissionsDeniedResponse
type: object
properties:
errors:
type: string
description: Error message
example: Access forbidden
description: |
Human-readable reason. Common values include `Account access forbidden` (e.g. wrong account)
and `Access forbidden` when the token lacks rights for the operation.
example: Account access forbidden
example:
errors: Account access forbidden
RateLimitExceededResponse:
title: RateLimitExceededResponse
type: object
Expand All @@ -2759,6 +2774,8 @@ components:
type: string
description: Error message
example: Rate limit exceeded
example:
errors: Rate limit exceeded
UnauthenticatedResponse:
title: UnauthenticatedResponse
type: object
Expand All @@ -2767,13 +2784,23 @@ components:
type: string
description: Error message
example: Incorrect API token
example:
error: Incorrect API token
UnprocessableEntity:
title: UnprocessableEntity
type: object
properties:
errors:
type: object
description: Validation errors per attribute. Entire record errors are under `base` key.
additionalProperties:
type: array
items:
type: string
example:
errors:
email:
- can't be blank
examples: {}
Comment on lines 2774 to 2788
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python -m pip install --quiet pyyaml jsonschema >/dev/null
python - <<'PY'
import yaml
from jsonschema import Draft202012Validator
from pathlib import Path

spec = yaml.safe_load(Path("specs/contacts.openapi.yml").read_text())
schema = spec["components"]["schemas"]["UnprocessableEntity"]
validator = Draft202012Validator(schema)

targets = [
    (
        "PATCH /api/accounts/{account_id}/contacts/{contact_identifier} 422",
        spec["paths"]["/api/accounts/{account_id}/contacts/{contact_identifier}"]["patch"]["responses"]["422"]["content"]["application/json"]["example"],
    ),
    (
        "POST /api/accounts/{account_id}/contacts/{contact_identifier}/events 422",
        spec["paths"]["/api/accounts/{account_id}/contacts/{contact_identifier}/events"]["post"]["responses"]["422"]["content"]["application/json"]["example"],
    ),
    (
        "POST /api/accounts/{account_id}/contacts/exports 422",
        spec["paths"]["/api/accounts/{account_id}/contacts/exports"]["post"]["responses"]["422"]["content"]["application/json"]["example"],
    ),
    (
        "POST /api/accounts/{account_id}/contacts/fields 422",
        spec["paths"]["/api/accounts/{account_id}/contacts/fields"]["post"]["responses"]["422"]["content"]["application/json"]["example"],
    ),
    (
        "PATCH /api/accounts/{account_id}/contacts/fields/{field_id} 422",
        spec["paths"]["/api/accounts/{account_id}/contacts/fields/{field_id}"]["patch"]["responses"]["422"]["content"]["application/json"]["example"],
    ),
]

for name, example in targets:
    errors = list(validator.iter_errors(example))
    if not errors:
        print(f"{name}: OK")
        continue

    print(f"{name}: INVALID")
    for err in errors[:3]:
        path = "/".join(str(p) for p in err.path) or "<root>"
        print(f"  - {path}: {err.message}")
PY

Repository: mailtrap/mailtrap-openapi

Length of output: 1393


Fix incompatibility between UnprocessableEntity schema and 422 response examples

The UnprocessableEntity schema restricts errors.<field> to string[], but multiple 422 response examples in this file violate this constraint. For example:

  • PATCH /contacts/{contact_identifier}: errors.list_id_included/0 shown as nested array ['contains invalid data'] instead of string
  • POST /contacts/fields: errors.merge_tag/0 and errors.name/0 shown as nested arrays
  • POST /contacts/exports: errors.filters shown as scalar string 'invalid' instead of array

Normalize all 422 examples to conform to the schema, or adjust the schema to accept the current structures. Per coding guidelines, specifications must be compliant with OpenAPI 3.1 standard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/contacts.openapi.yml` around lines 2790 - 2804, The UnprocessableEntity
schema defines errors.<field> as string[] but several 422 response examples
violate this; update the examples for the endpoints PATCH
/contacts/{contact_identifier} (errors.list_id_included/0), POST
/contacts/fields (errors.merge_tag/0 and errors.name/0), and POST
/contacts/exports (errors.filters) so each error value is an array of strings
(e.g., ["contains invalid data"] or ["invalid"]) to match the
UnprocessableEntity schema, or alternatively update the UnprocessableEntity
schema to accept nested arrays/scalars if you intentionally want those
shapes—ensure the chosen fix is applied consistently and the specs remain
OpenAPI 3.1 compliant.

responses:
UNAUTHENTICATED:
Expand All @@ -2782,30 +2809,40 @@ components:
application/json:
schema:
$ref: '#/components/schemas/UnauthenticatedResponse'
example:
error: Incorrect API token
LIMIT_EXCEEDED:
description: Rate limit exceeded for the current account.
content:
application/json:
schema:
$ref: '#/components/schemas/RateLimitExceededResponse'
example:
errors: Rate limit exceeded
PERMISSION_DENIED:
description: Returns forbidden error message. Check your permissions.
content:
application/json:
schema:
$ref: '#/components/schemas/PermissionsDeniedResponse'
example:
errors: Account access forbidden
INTERNAL_ERROR:
description: Internal error. Retry later or contact support.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
errors: Unexpected error
NOT_FOUND:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/NotFoundResponse'
example:
error: Not Found
parameters:
account_id:
description: Unique account ID
Expand Down
Loading
Loading