# OpenAPI 3.2 in 2026: what changed, and why it matters for SSE and AI agents

OpenAPI 3.2 is a consolidation release, which is exactly what a contract format needed. The 3.0-to-3.1 jump did the disruptive work — full alignment with JSON Schema draft 2020-12, top-level `webhooks`, and a sane null model — and 3.2 tightened the edges. If your documents are still 3.0.3, the migration is small in mechanical terms and large in correctness, especially once you start describing streams and feeding specs to code generators and MCP servers.

This is a practitioner's summary: what actually changes in your YAML, what breaks in old tooling, and how to document server-sent events properly.

## 1. JSON Schema alignment stops the dialect wars

In 3.0, OpenAPI's subset of JSON Schema diverged from the standard — `nullable: true`, a bespoke `exclusiveMinimum` boolean, no `$id`, no `oneOf`-before-`properties` semantics. Validators disagreed, generators disagreed, and any schema library needed an "OpenAPI mode."

Since 3.1, an OpenAPI Schema Object *is* a JSON Schema 2020-12 object with a few extensions (`discriminator`, `example`, `xml`). That means:

-   `prefixItems` describes positional tuple arrays (fixed first item types, then a rest type) — previously faked with `items` as an array, which was never standard.
-   `unevaluatedProperties` and `unevaluatedItems` close the "extends schema and rejects unknown keys" gap that `allOf` left open.
-   `$dynamicRef` / `$dynamicAnchor` make recursive generic envelopes (a paginated wrapper around any resource) expressible without hacks.
-   You can declare `"$schema": "https://json-schema.org/draft/2020-12/schema"` on components and use standard validators directly.

Practical payoff: the same schema file validates live traffic in a standard JSON Schema validator and documents the API in OpenAPI. Before 3.1 those were always slightly different documents.

## 2. Null is a type, not a vendor keyword

Delete every `nullable: true`. The standard form is a type array:

``` yaml
# 3.0 (legacy)
refundedAt:
  type: string
  nullable: true

# 3.1/3.2
refundedAt:
  type: [string, "null"]
  format: date-time
```

Two migration traps show up in real specs. First, `nullable` next to a `$ref` required a broken `allOf` wrapper; the type-array form composes with `$ref` directly (`allOf: [$ref, type: [object, 'null']]`). Second, generators built for 3.0 emit pointer-based optionals (`*string`) or wrapper types (`*Time`) rather than union types; modern generators map `[string, null]` to `string | null` in TypeScript and pointer-or-error patterns in Go. Regenerate SDKs after migration instead of trusting the old output.

## 3. Media types and binary content

`contentMediaType` and `contentEncoding` are now first-class schema keywords. A base64-encoded PDF embedded in JSON is described honestly:

``` yaml
attachment:
  type: string
  contentMediaType: application/pdf
  contentEncoding: base64
```

This replaces the old convention of `type: string, format: byte` with a comment explaining that the bytes are a PDF — generators can now emit typed wrappers instead of `str`.

## 4. Webhooks are top-level citizens

Since 3.1, callbacks that the *server* initiates live under a root `webhooks` map rather than being buried inside individual operations as `callbacks`:

``` yaml
webhooks:
  projectUpdated:
    post:
      summary: Fired when a project is updated
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectEvent'
      responses:
        '200':
          description: Acknowledged
```

This matters more every year: an API with webhooks used to be unusual; in 2026 an API without them is the exception. Root-level webhooks let documentation renderers and MCP generators list events as peers of operations instead of orphans attached to the POST that created the subscription.

## 5. Describing SSE honestly

Server-sent events are HTTP responses with `Content-Type: text/event-stream`, but vanilla OpenAPI stops being useful at that line — an event stream has named events, each with its own payload schema, and clients need that information to parse anything.

Two layers do the job. The standard layer uses the media type and schema:

``` yaml
responses:
  '200':
    description: Project event stream
    content:
      text/event-stream:
        schema:
          type: object
          properties:
            event:
              type: string
              enum: [project.updated, project.deleted]
            data:
              $ref: '#/components/schemas/ProjectEvent'
```

For tooling that needs to *serve* the stream — mock servers, scenario tests, MCP tools — Powerduck documents the protocol contract in an `x-protocol` extension alongside the media type: event names, the item schema per event, heartbeat interval, and close semantics. The extension is additive; validators and renderers that do not know it still see a valid 3.2 document, while the workspace can generate a working mock stream and assert against emitted events in tests. WebSocket operations use the same extension pattern; gRPC stays described in its own interface language and is referenced, not forced into OpenAPI.

## 6. Security and housekeeping

-   `securitySchemes` gained cleaner OpenID Connect handling in the 3.1 line; mutual-TLS schemes are documented without vendor extensions.
-   `example` (singular, schema-level) and `examples` (map, media-type-level) are both still around — standardize on one per document to stop generators randomly picking.
-   The spec document itself should declare `openapi: 3.2.0`; tooling older than 2024 may reject it, so audit your CI validators and code generators before flipping the field. Prism, modern Redoc/Scalar builds, and the Powerduck toolchain all parse 3.2; abandoned generators silently downgrade.

## Migration order that avoids a big-bang weekend

1.  Pin tooling versions first — validator, renderer, generators — and confirm 3.2 support.
2.  Convert `nullable` to type arrays mechanically, then diff the generated SDKs.
3.  Move `callbacks` that are server-push to root `webhooks`.
4.  Replace tuple-array `items` arrays with `prefixItems`; add `contentMediaType` where binary strings hid.
5.  Describe SSE endpoints with `text/event-stream` plus the `x-protocol` extension where mock/test/MCP support matters.
6.  Flip `openapi:` last, once every consumer reads the new document.

The workspace reads and writes 3.2 throughout — design, mock, scenarios, and MCP serving all validate against the same document — and the [quickstart](https://www.powerduck.com/docs/overview/quickstart?ref=powerduck.com) opens a 3.2 sample if you want a known-good reference.

**What to read next:** [debug every protocol in one workspace](https://www.powerduck.com/blog/debug-every-protocol-in-one-workspace/) covers HTTP, SSE, WebSocket, and gRPC side by side, and [your API already describes the tools your agent needs](https://www.powerduck.com/blog/your-api-already-describes-the-tools-your-agent-needs/) explains why 3.2 schemas map directly onto MCP tool inputs.

