#11010·swagger-ui

readOnly schema attribute rendering in request/response bodies depends on expansion order

Author: cmilatinovCreated Aug 13, 2026Updated Aug 13, 2026

Q&A

  • OS: Windows
  • Browser: Chrome
  • Version: 151
  • Method of installation: npm
  • Swagger-UI version: 5.32.13
  • Swagger/OpenAPI version: OpenAPI 3.1.0

Content & configuration

Read-only (readOnly: true) property visibility in Swagger UI is not computed per operation/context. Instead it behaves like a single shared/global state that is set by the order in which operations are expanded, and it applies to every operation — even operations that reference different component schemas.

Example Swagger/OpenAPI definition:

json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Read-only repro",
    "version": "1.0.0",
    "description": "Minimal spec to reproduce Swagger UI read-only rendering. A single shared schema (Widget) with one readOnly property (id) and one regular property (name) is referenced by a POST request body and a GET response. Expected: the POST body hides `id` (read-only), the GET response shows it."
  },
  "paths": {
    "/widgets": {
      "post": {
        "summary": "Create widget (request body — read-only `id` should be HIDDEN)",
        "operationId": "createWidget",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/Widget" }
            }
          }
        },
        "responses": {
          "201": { "description": "Created" }
        }
      }
    },
    "/widgets/{widgetId}": {
      "get": {
        "summary": "Get widget (response — read-only `id` should be SHOWN)",
        "operationId": "getWidget",
        "parameters": [
          {
            "name": "widgetId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Widget" }
              }
            }
          }
        }
      }
    },
    "/gadgets": {
      "post": {
        "summary": "Create gadget (separate schema — read-only `serial` should be HIDDEN)",
        "operationId": "createGadget",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/Gadget" }
            }
          }
        },
        "responses": {
          "201": { "description": "Created" }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Widget": {
        "type": "object",
        "additionalProperties": false,
        "required": ["name"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Regular writable attribute."
          },
          "id": {
            "type": "string",
            "readOnly": true,
            "description": "Read-only attribute — expected HIDDEN in POST body, SHOWN in GET response."
          }
        }
      },
      "Gadget": {
        "type": "object",
        "additionalProperties": false,
        "required": ["label"],
        "properties": {
          "label": {
            "type": "string",
            "description": "Regular writable attribute."
          },
          "serial": {
            "type": "string",
            "readOnly": true,
            "description": "Read-only attribute on a separate schema — expected HIDDEN in the POST body."
          }
        }
      }
    }
  }
}

Swagger-UI configuration options:

javascript
SwaggerUI({
  dom_id: '#swagger-ui-container',
  url: '<url to spec endpoint>',
})

Describe the bug you're encountering

Swagger UI 5.32.13 (OpenAPI 3.1) computes readOnly visibility as a single global state instead of per operation. The first operation expanded after page load sets read-only visibility for every operation:

  • Expand a GET (response) first → readOnly fields are shown everywhere, including POST request bodies (wrong).
  • Expand a POST (request) first → readOnly fields are hidden everywhere, including GET responses (wrong).

This leaks across unrelated schemas too: expanding POST /gadgets (schema Gadget) first, then GET /widgets (schema Widget), removes the read-only field from the GET response even though they share no schema.

To reproduce...

  1. Load the spec in Swagger UI. All operations collapsed by default.
  2. Case A: i. expand GET /widgets/{widgetId} first, ii. then expand POST /widgets and POST /gadgets. → id and serial appear in the POST request bodies (should be hidden).
  3. Reload. Case B: i. expand POST /widgets first, ii. then expand GET /widgets/{widgetId}. → id is missing from the GET response model (should be shown).
  4. Cross-schema: i. expand POST /gadgets (schema Gadget) first ii. then expand GET /widgets/{widgetId} (schema Widget). → the read-only id is removed from the GET response, even though the two operations use different schemas.

Expected behavior

Read-only visibility must be computed independently per rendered model, from that operation's own context:

  • Request body models → hide readOnly properties.
  • Response models → show readOnly properties.
  • The result must not depend on expansion order, nor on other operations, nor on other schemas.

Screenshots

Expanding GET first: Image Then POST with GET already expanded: Image The readOnly attribute is shown in both.

Expanding POST first: Image Then GET with POST already expanded: Image The readOnly attribute is hidden in both.