Skip to content

Add OpenAPI 3.2 stream emission support with itemSchema#8888

Merged
baywet merged 37 commits intomainfrom
copilot/add-sse-support-to-openapi
Nov 7, 2025
Merged

Add OpenAPI 3.2 stream emission support with itemSchema#8888
baywet merged 37 commits intomainfrom
copilot/add-sse-support-to-openapi

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Oct 31, 2025

Implementation Plan for Stream Support in OpenAPI 3.2

This PR implements support for streaming responses (including Server-Sent Events) in OpenAPI 3.2 emission and import using the itemSchema field.

Checklist

  • 1. Add itemSchema support to OpenAPI 3.2 MediaType definition in types.ts
  • 2. Create an SSE module (sse-module.ts) following the xml-module.ts pattern
  • 3. Integrate SSE module into the OpenAPI emitter for emission support
  • 4. Add package dependencies for @typespec/streams, @typespec/events, @typespec/sse
  • 5. Use getStreamMetadata from @typespec/http/experimental to detect streams
  • 6. Remove debug console.log statements
  • 7. Tests passing for SSE emission
  • 8. Handle @events decorator to emit proper SSE event structures
  • 9. Handle @contentType decorator on union variants
  • 10. Handle @terminalevent decorator (preserve with @extension)
  • 11. Run formatter and linter
  • 12. Add changelog entry with chronus
  • 13. Generalize stream support for all content types

Changes Made

Type Definitions:

  • Added itemSchema property to OpenAPIMediaType3_2 for streaming event schema definitions

SSE Module (sse-module.ts):

  • Created stream detection and emission logic following xml-module.ts pattern
  • Implements stream detection via @typespec/streams.getStreamOf
  • Generates itemSchema with oneOf variants for @events union members
  • Works with any stream content type (SSE, JSON Lines, etc.)

OpenAPI Emitter Integration:

  • Integrated SSE module as optional dependency alongside XML/JSON Schema
  • Modified getBodyContentEntry to detect all stream responses via getStreamMetadata
  • Routes stream responses through emission path for OpenAPI 3.2 with itemSchema
  • Generalized to support any HttpStream<ContentType, Type>, not just SSE streams

Testing:

  • All stream tests passing
  • Simplified test structure by removing namespaces and extra decorators
  • Used ApiTester base for common libraries with stream-specific imports on top

Diagnostics:

  • Added "streams-not-supported" diagnostic for OpenAPI 3.0/3.1
  • Warning message indicates streams with itemSchema require OpenAPI 3.2.0

Stream Support

This implementation supports any stream type in OpenAPI 3.2.0:

  • Server-Sent Events (SSE) with text/event-stream
  • JSON Lines with application/jsonl
  • Any custom HttpStream<ContentType, Type>

For OpenAPI 3.0/3.1, streams are emitted without itemSchema and a warning is generated.

Original prompt

This section details on the original issue you should resolve

<issue_title>[Feat] OpenAPI - add support for SSE import and emission</issue_title>
<issue_description>### Clear and concise description of the problem

Now that we have support for OpenAPI 3.2.0 (#8828 ) both in emissions and in importing descriptions to TypeSpec, it'd be nice to have support for importing SSE (server-sent events).
@timotheeguerin and I spent the better part of an hour combing through multiple specifications to understand what needs to happen here, so strap in, this is going to be a long issue.

Relevant resources

Import

Scenario 1 : no terminal event

Given the following OpenAPI Response object.

content:
  description: A request body to add a stream of typed data.
  required: true
  content:
    text/event-stream:
      itemSchema:
        type: object
        properties:
          event:
            type: string
          data:
            type: string
        required: [event]
        # Define event types and specific schemas for the corresponding data
        oneOf:
          - properties:
              event:
                const: userconnect
              data:
                contentMediaType: application/json
                contentSchema:
                  type: object
                  required: [username]
                  properties:
                    username:
                      type: string
          - properties:
              event:
                const: usermessage
              data:
                contentMediaType: application/json
                contentSchema:
                  type: object
                  required: [text]
                  properties:
                    text:
                      type: string

I'd expect aresulting TypeSpec description looking like this.

import "@typespec/streams";
import "@typespec/sse";
import "@typespec/events";

using SSE;

model UserConnect {
  username: string;
}
model UserMessage {
  text: string;
}

@TypeSpec.Events.events
union ChannelEvents {
  userconnect: UserConnect,
  usermessage: UserMessage,
}
op subscribeToChannel(): SSEStream<ChannelEvents>;

Note that the terminal event is NOT present, this is ok because it's an invention of some APIs, and is not part of the WHATWG spec. It should remain optional.

Here a couple of things are worth noting:

  • imports for typespec streams/sse/events are added
  • a using for SSE is added
  • a @TypeSpec.Events.events decorator is added to the union type
  • the return type of the operation is now SSEStream<ChannelEvents> (instead of ChannelEvents)
  • The union type discriminator values are obtained by conventions based on the event properties in the schema (name is a convention)
  • the union type member types are defined by convention by the schema of the data property, and the fact the content media type is application/json

Scenario 2: with terminal events

Given the following OpenAPI Response object.

content:
  description: A request body to add a stream of typed data.
  required: true
  content:
    text/event-stream:
      itemSchema:
        type: object
        properties:
          event:
            type: string
          data:
            type: string
        required: [event]
        # Define event types and specific schemas for the corresponding data
        oneOf:
          - properties:
              data:
                contentMediaType: text/plain
                const: "[done]"
                "x-ms-sse-terminal-event": true
          - properties:
              event:
                const: userconnect
              data:
                contentMediaType: app...

</details>

- Fixes microsoft/typespec#8887

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Copilot AI and others added 2 commits October 31, 2025 20:08
Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
Copilot AI changed the title [WIP] Add support for SSE import and emission in OpenAPI Add OpenAPI 3.2 SSE (Server-Sent Events) emission support Oct 31, 2025
Copilot AI requested a review from baywet October 31, 2025 20:26
@baywet
Copy link
Copy Markdown
Member

baywet commented Nov 3, 2025

@copilot cleanup the console logs you've added in this pull request

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 3, 2025

All changed packages have been documented.

  • @typespec/openapi3
Show changes

@typespec/openapi3 - feature ✏️

adds support for emission and import of SSE for OpenAPI 3.2

baywet and others added 4 commits November 3, 2025 14:41
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Nov 3, 2025

@copilot cleanup the console logs you've added in this pull request

Removed all debug console.log statements from openapi.ts and test files in commit 559641d.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • telemetry.astro.build
    • Triggering command: node /home/REDACTED/work/typespec/typespec/website/node_modules/.bin/../astro/astro.js build (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@baywet baywet linked an issue Nov 3, 2025 that may be closed by this pull request
3 tasks
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
@baywet baywet requested a review from timotheeguerin November 7, 2025 17:26
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
@baywet baywet requested a review from timotheeguerin November 7, 2025 19:42
@baywet baywet added this pull request to the merge queue Nov 7, 2025
Merged via the queue into main with commit 2635e48 Nov 7, 2025
22 checks passed
@baywet baywet deleted the copilot/add-sse-support-to-openapi branch November 7, 2025 21:45
github-merge-queue bot pushed a commit that referenced this pull request Nov 10, 2025
SSEStream accepted unions without `@events` decorator, generating
incorrect schemas. This validation ensures proper event stream
definitions at compile time.

## Changes

- **Added validation** in `packages/sse/src/validate.ts`:
  - Traverse all models during validation phase
  - Check streams with `text/event-stream` content-type
  - Verify streamOf type is a union with `@events` decorator
  - Report `sse-stream-union-not-events` diagnostic on failure

- **Added diagnostic** `sse-stream-union-not-events` in
`packages/sse/src/lib.ts`

- **Added tests** covering invalid/valid SSEStream usage and HttpStream
with SSE content-type

## Example

This now fails validation:
```typespec
union BasicUnion {
  userconnect: UserConnect,
}
op subscribe(): SSEStream<BasicUnion>;  // Error: union must have @events
```

This compiles:
```typespec
@events
union BasicUnion {
  userconnect: UserConnect,
}
op subscribe(): SSEStream<BasicUnion>;  // OK
```

Fixes #8888

> [!WARNING]
>
> <details>
> <summary>Firewall rules blocked me from connecting to one or more
addresses (expand for details)</summary>
>
> #### I tried to connect to the following addresses, but was blocked by
firewall rules:
>
> - `telemetry.astro.build`
> - Triggering command: `node
/home/REDACTED/work/typespec/typespec/website/node_modules/.bin/../astro/astro.js
build` (dns block)
>
> If you need me to access, download, or install something from one of
these locations, you can either:
>
> - Configure [Actions setup
steps](https://gh.io/copilot/actions-setup-steps) to set up my
environment, which run before the firewall is enabled
> - Add the appropriate URLs or hosts to the custom allowlist in this
repository's [Copilot coding agent
settings](https://github.com/microsoft/typespec/settings/copilot/coding_agent)
(admins only)
>
> </details>

<!-- START COPILOT CODING AGENT SUFFIX -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>SSEStream should NOT accept a union model without the
events decorator</issue_title>
> <issue_description>This definition
> 
> ```ts
> import "@typespec/streams";
> import "@typespec/sse";
> import "@typespec/events";
> 
> using SSE;
> 
> 
> model UserConnect {
>   name: string;
> }
> 
> union BasicUnion {
>   userconnect: UserConnect,
> }
> op subscribe(): SSEStream<BasicUnion>;
> ```
> 
> Builds but generates the wrong schema (instead of item schema with all
the right properties)
> 
> ```ts
> import "@typespec/streams";
> import "@typespec/sse";
> import "@typespec/events";
> 
> using SSE;
> 
> 
> model UserConnect {
>   name: string;
> }
> 
> @TypeSpec.Events.events
> union BasicUnion {
>   userconnect: UserConnect,
> }
> op subscribe(): SSEStream<BasicUnion>;
> ```
> 
> Generates the correct itemSchema.
> 
> Ultimately this is not a responsibility of the OpenAPI3 emitter to
check for this, but rather the SSE library should fail building when
being passed a union model as a generic type parameter value when that
union model DOES NOT have the required decorator.
> This will be handled in a separate issue/pull request
> 
> _Originally posted by @baywet in
#8888 (comment)
>             </issue_description>
> 
> <agent_instructions>before committing any file, run `npx prettier
--write filePath` to ensure they are formatted correctly.
> once you're done with the changes, run `npx @chronus/chronus add` and
select bugfix to add an entry in the changelog</agent_instructions>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> <comment_new><author>@timotheeguerin</author><body>
> Also note that as `SSEStream` is just defined as `HttpStream<Type,
"text/event-stream">;`
> 
> that also might need to validate when `text/event-stream` is
used</body></comment_new>
> </comments>
> 


</details>

- Fixes #8950

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
Co-authored-by: Vincent Biret <vibiret@microsoft.com>
Co-authored-by: Timothee Guerin <tiguerin@microsoft.com>
Copy link
Copy Markdown
Member

@baywet baywet left a comment

Choose a reason for hiding this comment

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

leaving some comments after testing things locally

Comment on lines +234 to +240
} else if (content.schema && "$ref" in content.schema) {
// Fallback: use schema directly if no itemSchema
const eventUnionType = context.generateTypeFromRefableSchema(
content.schema,
operationScope,
);
return `SSEStream<${eventUnionType}>`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is wrong, we shouldn't fallback on the main schema for streaming as schema is bounded and that does not work with streams which don't have collections bounds

return contents.map(([mediaType, content]) => {
// Special handling for Server-Sent Events
if (mediaType === "text/event-stream") {
context.markSSEUsage();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this should be moved down to the condition when we detected a valid case

path: OpenAPI3PathItem | OpenAPIPathItem3_2,
context: Context,
): void {
if (!("get" in path && path.get && path.get.responses)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is wrong, a streaming response could come from any operation that MAY return a response body

pull bot pushed a commit to AmirulAndalib/typespec that referenced this pull request Nov 11, 2025
follow up to microsoft#8888

---------

Signed-off-by: Vincent Biret <vibiret@microsoft.com>
Co-authored-by: Timothee Guerin <tiguerin@microsoft.com>
github-merge-queue bot pushed a commit that referenced this pull request Dec 29, 2025
follow up to #8888

---------

Signed-off-by: Vincent Biret <vibiret@microsoft.com>
@baywet baywet mentioned this pull request Jan 19, 2026
github-merge-queue bot pushed a commit that referenced this pull request Jan 19, 2026
follow up to #8888

---------

Signed-off-by: Vincent Biret <vibiret@microsoft.com>
@timotheeguerin timotheeguerin added emitter:openapi3 Issues for @typespec/openapi3 emitter openapi3:converter Issues for @typespec/openapi3 openapi to typespec converter labels Mar 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:openapi3 Issues for @typespec/openapi3 emitter openapi3:converter Issues for @typespec/openapi3 openapi to typespec converter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] OpenAPI - add support for SSE import and emission

4 participants