Files
khata-ui/react-openapi

react-openapi

Auto-generates an admin panel (CRUD, datatable, relationship management) from an OpenAPI 3.1.0 specification at runtime.

How It Works

  1. You provide a specConfiguration object with the URL to your OpenAPI YAML spec and an auth callback.
  2. AppProvider fetches the spec, validates it against react-openapi's required extensions, and builds an internal resource configuration.
  3. The Admin component renders routes (list, detail, create, edit) for each resource detected in the spec.
  4. All API calls go through an Axios client that injects the Bearer token from your callback.

Installation

npm install react-openapi
# peer dependencies (install these in your app):
npm install react react-dom react-router-dom @mui/material @mui/icons-material @mui/x-data-grid @emotion/react @emotion/styled axios js-yaml

Quick Start

import { AppProvider, Admin } from "react-openapi";
import type { SpecConfiguration } from "react-openapi";

const specConfig: SpecConfiguration = {
  specUrl: "/api/openapi.yaml",
  baseApiUrl: "https://api.example.com/v1",
  title: "Vet Clinic Admin",
  getToken: () => localStorage.getItem("token"),
};

function App() {
  return (
    <AppProvider specConfiguration={specConfig}>
      <Admin basePath="/admin" />
    </AppProvider>
  );
}

Exported API

Components

Export Purpose
AppProvider Fetches the spec, validates it, initializes the API client, and provides context. Wrap your app with this.
Admin Renders the admin layout (sidebar + routes) for all resources. Takes basePath.
ListCellRenderer Renders a cell value in a list/table column. Respects FK display formats, inline refs, enums, booleans.
DetailFieldRenderer Renders a read-only field value in a detail view (label + value pair).
FormFieldRenderer Renders an editable form field based on FieldConfig. Dispatches to FkSelectField, BooleanField, etc.
SseStreamView Renders a live SSE stream card with connection status, event counter, latest event preview, and snackbar.
SseConnectionStatus Renders a green/red dot with "Connected"/"Disconnected" for an SSE resource. Reactive across all components.

Hooks

Export Purpose
useAppContext() Returns { config, resources, schemas, loading, errors, warnings }. Access all resource configs.
useResource(resourceName) Returns { resource, components, list, get, create, update, remove, stream?, loading, error }. CRUD + streaming.

The stream property is only present for SSE resources (resource.streaming === true). It accepts { onEvent, onError?, onOpen? } and returns { close }.

Utilities

Export Purpose
getApi() Returns the configured Axios instance. Useful for custom API calls.
applyDisplayFormat Formats an object using a template string like "{name} - #{id}".

Types

Export Purpose
SpecConfiguration { specUrl, baseApiUrl?, title?, getToken? }
ResourceConfig Internal representation of a resource after spec parsing
FieldConfig Internal representation of a field after spec parsing
FKFieldConfig { resource, prefetch }
ResourceRelationship { fieldName, config, targetSchemaName }
FilterComponentProps { value, onChange, data?, labelOverride? }

ResourceConfig Properties

Property Type Description
name string URL-friendly resource name (e.g. "pets", "calls")
schemaName string OpenAPI schema key (e.g. "Pet", "Call")
displayName string Human-readable name for UI labels (e.g. "Pets", "Calls", "Two Parts")
path string API base path (e.g. "/pets", "/calls")
streaming boolean (optional) true if this resource uses SSE streaming instead of REST CRUD

SSE Streaming (Server-Sent Events)

Resources with x-sse: true on their OpenAPI spec path object are treated as streaming resources. Instead of CRUD, they expose:

  • stream() — accepts { onEvent, onError?, onOpen? } and returns { close }.
  • Connection status — reactive across SseConnectionStatus and SseStreamView.
  • Auto-reconnect — SSE automatically reconnects on connection loss (browser-native).

OpenAPI extension:

/pets/events:
  x-sse: true
  get:
    summary: Subscribe to Pet events
    responses:
      200:
        description: SSE stream of Pet change events
        content:
          text/event-stream:
            schema:
              type: object

When x-sse: true is set, the resource is excluded from sidebars, list/detail routes, and CRUD hooks. It only appears in the streaming resource collection.

Custom UI example:

import { useAppContext, SseStreamView } from 'react-openapi';

function MyStreamPage() {
  const { resources } = useAppContext();
  const res = resources.find(r => r.name === 'calls');
  if (!res) return null;
  return <SseStreamView resource={res} />;
}

Loading Custom Components from Resources

Pass per-resource overrides via specConfiguration:

<AppProvider config={config} resourceComponents={{
  pets: { list: PetList, detail: PetDetail, form: PetForm },
  calls: { detail: CallStream },    // SSE: only detail is used
}} />

Within resource components, use components from useResource():

const { components } = useResource('pets');
const row = data.map(item => <components.ListCell field={field} value={item[field.name]} />);

OpenAPI Spec Authoring Guide

react-openapi uses custom x- extensions embedded in your OpenAPI 3.1.0 spec. This section is the single source of truth for writing a compatible spec.

Quick Reference

openapi: 3.1.0
info:
  title: My API
  version: 1.0.0

servers:
  - url: https://api.example.com/v1

components:
  schemas:

    # --- Error schemas ---
    ErrorBody:
      type: object
      properties:
        detail:
          type: string
      required: [detail]

    HTTPValidationError:
      type: object
      properties:
        detail:
          type: array
          items:
            $ref: '#/components/schemas/ValidationError'

    ValidationError:
      type: object
      properties:
        loc:
          type: array
          items:
            type: string
        msg:
          type: string
        type:
          type: string
      required: [loc, msg, type]

    # --- Non-resource schema (skipped by validator) ---
    Metadata:
      type: object
      properties:
        createdOn:
          type: string
          format: date-time
        updatedOn:
          type: string
          format: date-time

    # --- Resource schema ---
    Pet:
      type: object
      x-resource: pets                    # REQUIRED - maps to /pets path
      x-primary-key: id                   # REQUIRED
      x-display-format: "{name} - #{id}" # REQUIRED
      x-list-columns: [name, species, age] # REQUIRED
      properties:
        id:
          type: integer
          readOnly: true
          x-order: 0                     # REQUIRED on every property
          x-hidden: { form: true, list: true }
          x-label: "ID"                  # REQUIRED on every property
        name:
          type: string
          x-order: 1
          x-label: "Pet Name"
          x-description: "Name of the pet"   # optional
          x-filterable: true                 # optional
          x-sortable: true                   # optional
        species:
          type: string
          enum: [dog, cat, bird]             # renders as select dropdown
          x-order: 2
          x-label: "Species"
        weight:
          type: number
          format: float
          x-order: 3
          x-label: "Weight"
        birthDate:
          type: string
          format: date                        # renders as date picker
          x-order: 4
          x-label: "Date of Birth"
        photo:
          type: string
          format: binary
          x-ui-type: image                    # renders as file upload
          x-upload-url: /pets/{id}/photo      # POST endpoint for upload
          x-order: 5
          x-label: "Photo"
        owner:
          $ref: '#/components/schemas/Parent'
          x-fk:                               # renders as FK dropdown
            resource: parents
            prefetch: true                    # load all options on mount
          x-order: 6
          x-label: "Owner"
        siblings:
          type: array
          items:
            $ref: '#/components/schemas/Pet'
          x-fk:                               # renders as multi-select
            resource: pets
            prefetch: false                   # lazy-load on focus
          x-order: 7
          x-label: "Siblings"
        metadata:
          $ref: '#/components/schemas/Metadata'
          # NO x-fk — renders as inline read-only display
          x-order: 8
          x-label: "Metadata"
      required: [id, name, owner]

  # --- Error response schemas (REQUIRED for spec validity) ---
  responses:
    Unauthorized:
      description: Not authenticated
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'
    ValidationError:
      description: Validation Error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/HTTPValidationError'
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorBody'

paths:
  # --- Collection paths ---
  /pets:
    get:
      summary: List pets (paginated)
      parameters:
        - in: query
          name: limit
          schema: { type: integer, default: 20 }   # default is REQUIRED if pagination params exist
        - in: query
          name: offset
          schema: { type: integer, default: 0 }
      responses:
        '200':
          description: Paginated list of pets
          content:
            application/json:
              schema:
                type: object
                properties:
                  total:
                    type: integer
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/Pet'
        # --- Error responses (RECOMMENDED on every operation) ---
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '500': { $ref: '#/components/responses/InternalServerError' }
    post:
      summary: Create a pet
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Pet'
      responses:
        '201':
          description: Pet created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  # --- Item paths ---
  /pets/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Single pet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '500': { $ref: '#/components/responses/InternalServerError' }
    put:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Pet'
      responses:
        '200':
          description: Pet updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '500': { $ref: '#/components/responses/InternalServerError' }
    delete:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '204':
          description: Pet deleted
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '500': { $ref: '#/components/responses/InternalServerError' }

Schema-Level Extensions

These go on the schema object itself and mark it as a resource that generates the admin UI.

x-resource (REQUIRED)

Pet:
  type: object
  x-resource: pets    # maps to the /pets path in paths
  • Value must be a string matching the collection path segment (e.g., pets/pets).
  • Schemas without x-resource are skipped entirely — no CRUD routes, no extension validation. Use this for shared schemas like Metadata, ErrorBody, etc.
  • Validator error if the path /pets does not exist in paths.

x-primary-key (REQUIRED)

x-primary-key: id
  • The name of the property that uniquely identifies each record.
  • Used as the :id path parameter in routes and API calls.
  • Must reference a property that exists in properties.
  • Validator error if missing.

x-display-format (REQUIRED)

x-display-format: "{name}"
x-display-format: "Dr. {name}"
x-display-format: "Apt #{id} - {date}"
  • Template string with {propertyName} placeholders.
  • Used to display a human-readable label for a record (list rows, detail title, FK dropdown options).
  • Property names in {braces} are replaced with the record's property values at render time.
  • Validator error if missing.

x-list-columns (REQUIRED)

x-list-columns: [name, species, age, owner]
  • Array of property names to display as columns in the datatable.
  • Each name must exist in properties.
  • Column order follows the array order.
  • The display value for each cell is determined by the x-display-format of the property's target resource (for FK fields) or the parent resource (for direct fields).
  • Validator error if missing or if any column name references a non-existent property.

Property-Level Extensions

These go on individual properties inside properties.

x-label (REQUIRED on every property)

x-label: "Pet Name"
  • Human-readable label used in form fields, table headers, and detail views.
  • Validator error if missing on any property of a resource schema.

x-order (REQUIRED on every property)

x-order: 1
  • Integer determining field ordering in forms and detail views.
  • Lower values appear first. Ties are broken by alphabetical property name.
  • Validator error if missing or null on any property of a resource schema.

x-description (optional)

x-description: "Name of the pet"
  • Shown as helper text below form fields and as placeholder text.
  • Falls back to x-label if not provided.

x-hidden (optional)

x-hidden: { form: true, list: true, detail: true }
x-hidden: { form: true }         # only hide in form
  • Controls visibility in each view. Valid keys: form, list, detail.
  • Any missing key defaults to false (visible).
  • Useful for hiding auto-generated fields like id from forms.

x-filterable (optional)

x-filterable: true
  • Marks the field as filterable in the datatable (UI integration TBD — reserved for future use).
  • Default: false.

x-sortable (optional)

x-sortable: true
  • Adds sort controls to the datatable column header.
  • When clicked, sends ?sort=fieldName (ascending) or ?sort=-fieldName (descending) to the list endpoint.
  • Default: false.

x-fk (optional)

owner:
  $ref: '#/components/schemas/Parent'
  x-fk:
    resource: parents
    prefetch: true                 # load all options on mount

Marks a $ref property as a foreign key. The form renders a dropdown (single) or multi-select (if type: array, items: $ref).

Sub-field Type Default Description
resource string (required) The x-resource value of the target schema. Uses the resource name, not the schema name.
prefetch boolean false If true, fetch all FK options on mount. If false, fetch on focus (lazy).

How FK options are fetched:

  1. The library calls GET /{targetResource}?limit=0 (for paginated targets — limit=0 is a server convention meaning "return all") or GET /{targetResource} (for non-paginated).
  2. The response is parsed strictly by the target resource's pagination config.
  3. Each item is formatted using the target resource's x-display-format.

Validator rules:

  • The resource value must match some schema's x-resource in the spec.
  • The target schema must have x-display-format and x-primary-key.

$ref without x-fk — Inline Display

metadata:
  $ref: '#/components/schemas/Metadata'
  # no x-fk here

A $ref property without x-fk renders as read-only inline display:

  • In list and detail views: uses InlineRefField which shows key-value chips (or the referenced resource's x-display-format if the target is a resource schema).
  • In forms: shows a disabled TextField with JSON.stringify(value, null, 2).

The validator emits an info message for these to remind you they won't be editable.

x-ui-type (optional)

photo:
  type: string
  format: binary
  x-ui-type: image
  x-upload-url: /pets/{id}/photo

Currently, supports only "image". Changes the form field to a file upload component.

  • If the record has an id and x-upload-url is set: POSTs the file to x-upload-url (with {id} replaced by the record's ID), then updates the field value with the response URL.
  • If no id or no uploadUrl: falls back to base64 data-URL via FileReader.

x-upload-url (optional, used with x-ui-type: image)

x-upload-url: /pets/{id}/photo

POST endpoint for file upload. The {id} placeholder is replaced with the current record ID.

Property Types That Trigger Special Renderers

Condition Form Renderer List/Detail Renderer
x-fk exists, type is NOT array FkSelectField (dropdown) ListCellRenderer with applyDisplayFormat
x-fk exists, type: array FkMultiSelectField (Autocomplete multi) ListCellRenderer with chips
enum is defined EnumField (select) Chip
type: boolean BooleanField (Switch) Chip with "Yes"/"No"
type: integer or type: number NumberField Typography
format: date DateField (date picker) Typography
format: date-time DateField (datetime-local picker) Typography
x-ui-type: image ImageField (upload button + preview) Avatar
$ref without x-fk Disabled TextField with JSON InlineRefField (chips or displayFormat)
None of the above (default) StringField (text input) Typography

Path Conventions

Collection Path

Format: /{x-resource} (e.g., /pets)

Operation Required? Purpose
GET Yes List endpoint — populates the datatable
POST Yes Create endpoint

Item Path

Format: /{x-resource}/{id} (e.g., /pets/{id})

Operation Required? Purpose
GET Yes Detail view
PUT Yes Edit form
DELETE Yes Delete action

Response Shapes

The library expects strict response shapes that match the spec. No flexible fallbacks.

Paginated List (GET with limit/offset params)

{
  "total": 42,
  "items": [{ "id": 1, "name": "Fido" }, ...]
}
  • total is optional; falls back to items.length if absent.
  • items is required and must be an array.
  • The library throws if the response is not { total, items } with an array items.

Non-Paginated List (GET without limit/offset params)

[{ "id": 1, "name": "Vaccination" }, ...]
  • Response must be a plain array.
  • The library throws if the response is not an array.

Single Item (GET /{id})

{ "id": 1, "name": "Fido", ... }
  • Returns the resource object directly.
  • No special validation beyond standard API error handling.

Create / Update (POST / PUT)

{ "id": 1, "name": "Fido", ... }
  • Returns the created/updated resource object directly.

Delete

  • Returns HTTP 204 with no body.

Error Response Format

The library's parseError() handles two formats:

FastAPI ValidationError (422):

[
  { "loc": ["body", "name"], "msg": "field required", "type": "value_error" }
]

→ Rendered as: "field required"

Generic ErrorBody:

{ "detail": "Not found" }

→ Rendered as: "Not found"

Pagination Behavior

Detecting Pagination

A resource is considered paginated if its collection GET path has both a limit and offset query parameter. The limit parameter must have a schema.default value:

parameters:
  - in: query
    name: limit
    schema: { type: integer, default: 20 }   # default is REQUIRED
  - in: query
    name: offset
    schema: { type: integer, default: 0 }

If only one of limit/offset is present, pagination is not detected (the path is treated as non-paginated).

Datatable Pagination

The datatable renders an MUI TablePagination component when resource.pagination is non-null. It sends ?limit={rowsPerPage}&offset={page * rowsPerPage} with each list request.

FK Options — Paginated Targets

When fetching FK options for a paginated target resource, the library sends ?limit=0. This is a server convention meaning "return all records" — not documented in the spec. The server must interpret limit=0 as "no limit."

Spec Validation

The validator (spec-validator.ts) runs at mount time and categorizes issues as:

Level Meaning Impact
error Missing required extension Admin panel shows error screen — will not render
warning Missing optional configuration Admin panel renders, warning shown in snackbar
info Informational note (e.g., non-FK $ref) Shown alongside warnings

Complete Validation Rules

For each schema with x-resource:

  1. x-primary-key must be present.
  2. x-display-format must be present.
  3. x-list-columns must be an array, and every entry must reference a real property.
  4. Every property must have x-label.
  5. Every property must have x-order.
  6. If a property uses $ref without x-fkinfo message.
  7. If a property uses x-fk, the referenced resource must exist as another schema's x-resource, and that target must have x-display-format and x-primary-key.
  8. The x-resource path must exist in paths.
  9. The collection path must have GET (list) and POST (create).
  10. The item path /{resource}/{id} must exist with GET, PUT, and DELETE.
  11. If the collection GET has pagination params (limit/offset), the limit param must have a schema.default.

Auth

Authentication is handled via the getToken callback in SpecConfiguration:

const specConfig: SpecConfiguration = {
  specUrl: "/api/openapi.yaml",
  getToken: () => localStorage.getItem("token"),  // called on every request
};
  • The token is injected as Authorization: Bearer {token} on every API call.
  • If a 401 response is received and getToken returns a token, the token is cleared from localStorage (for token refresh flows — customize in your wrapper).

Design Decisions

  • Spec loaded at runtime — not built into the bundle. A single built UI works with any backend serving the spec at a known URL.
  • Schema-to-path mapping is explicit — the x-resource value maps directly to the path segment. No magic pluralization or guessing.
  • No fallbacks for required extensions — if the spec is missing x-label, x-order, x-primary-key, x-display-format, or x-list-columns, the library throws at transform time rather than silently defaulting.
  • FK options use ?limit=0 for paginated resources — server convention, not spec-documented.
  • Response parsing is strict{ total, items } for paginated, [] for non-paginated. No fallback chains.

Example: Complete Minimal Spec

openapi: 3.1.0
info:
  title: Minimal API
  version: 1.0.0
servers:
  - url: https://api.example.com/v1
components:
  schemas:
    ErrorBody:
      type: object
      properties:
        detail: { type: string }
      required: [detail]
    HTTPValidationError:
      type: object
      properties:
        detail:
          type: array
          items:
            $ref: '#/components/schemas/ValidationError'
    ValidationError:
      type: object
      properties:
        loc:
          type: array
          items: { type: string }
        msg: { type: string }
        type: { type: string }
      required: [loc, msg, type]
    Widget:
      type: object
      x-resource: widgets
      x-primary-key: id
      x-display-format: "{name}"
      x-list-columns: [name, status]
      properties:
        id:
          type: integer
          readOnly: true
          x-order: 0
          x-hidden: { form: true, list: true }
          x-label: "ID"
        name:
          type: string
          x-order: 1
          x-label: "Widget Name"
        status:
          type: string
          enum: [active, inactive, archived]
          x-order: 2
          x-label: "Status"
      required: [id, name]
  responses:
    Unauthorized:
      description: Not authenticated
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorBody' }
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorBody' }
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorBody' }
    ValidationError:
      description: Validation Error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/HTTPValidationError' }
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorBody' }
paths:
  /widgets:
    get:
      summary: List widgets
      responses:
        '200':
          description: List of widgets
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Widget'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '500': { $ref: '#/components/responses/InternalServerError' }
    post:
      summary: Create a widget
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Widget'
      responses:
        '201':
          description: Widget created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Widget'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '500': { $ref: '#/components/responses/InternalServerError' }
  /widgets/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Single widget
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Widget'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '500': { $ref: '#/components/responses/InternalServerError' }
    put:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Widget'
      responses:
        '200':
          description: Widget updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Widget'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '500': { $ref: '#/components/responses/InternalServerError' }
    delete:
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        '204':
          description: Widget deleted
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '500': { $ref: '#/components/responses/InternalServerError' }