DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

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:

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

# 3.1/3.2
refundedAt:
  type: [string, "null"]
  format: date-time
Enter fullscreen mode Exit fullscreen mode

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:

attachment:
  type: string
  contentMediaType: application/pdf
  contentEncoding: base64
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

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'
Enter fullscreen mode Exit fullscreen mode

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 opens a 3.2 sample if you want a known-good reference.

What to read next: 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 explains why 3.2 schemas map directly onto MCP tool inputs.

Top comments (0)