Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-itemschema-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-typescript": minor
---

Add OpenAPI 3.2 `itemSchema` support for SSE and streaming responses
10 changes: 8 additions & 2 deletions packages/openapi-typescript/src/transform/media-type-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ import transformSchemaObject from "./schema-object.js";
/**
* Transform MediaTypeObject nodes (4.8.14)
* @see https://spec.openapis.org/oas/v3.1.0#media-type-object
*
* OpenAPI 3.2 adds `itemSchema` for sequential/streaming media types (e.g. text/event-stream).
* Both `schema` (complete payload) and `itemSchema` (per-item) can coexist on the same object.
* For type generation we prefer `itemSchema` when present, as it describes the shape consumers
* will actually parse per event/chunk.
*/
export default function transformMediaTypeObject(
mediaTypeObject: MediaTypeObject,
options: TransformNodeOptions,
): ts.TypeNode {
if (!mediaTypeObject.schema) {
const targetSchema = mediaTypeObject.itemSchema ?? mediaTypeObject.schema;
if (!targetSchema) {
return UNKNOWN;
}
return transformSchemaObject(mediaTypeObject.schema, options);
return transformSchemaObject(targetSchema, options);
}
2 changes: 2 additions & 0 deletions packages/openapi-typescript/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ export interface RequestBodyObject extends Extensable {
export interface MediaTypeObject extends Extensable {
/** The schema defining the content of the request, response, or parameter. */
schema?: SchemaObject | ReferenceObject;
/** OAS 3.2: The schema defining the content of individual items in a streaming response (e.g. text/event-stream). When present, takes precedence over schema for type generation. */
itemSchema?: SchemaObject | ReferenceObject;
/** Example of the media type. The example object SHOULD be in the correct format as specified by the media type. The example field is mutually exclusive of the examples field. Furthermore, if referencing a schema which contains an example, the example value SHALL override the example provided by the schema. */
example?: any;
/** Examples of the media type. Each example object SHOULD match the media type and specified schema if present. The examples field is mutually exclusive of the example field. Furthermore, if referencing a schema which contains an example, the examples value SHALL override the example provided by the schema. */
Expand Down
101 changes: 101 additions & 0 deletions packages/openapi-typescript/test/fixtures/sse-stream-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# itemSchema is an OpenAPI 3.2 feature, but we use 3.1 here because
# @redocly/openapi-core v1 does not support 3.2 validation yet.
openapi: "3.1"
info:
title: SSE Streaming API
version: "1.0"
paths:
/events:
get:
operationId: streamEvents
summary: Stream server-sent events
responses:
"200":
description: SSE event stream
content:
text/event-stream:
itemSchema:
type: object
properties:
event:
type: string
enum:
- message
- heartbeat
- error
data:
type: string
id:
type: integer
timestamp:
type: string
format: date-time
required:
- event
- data
/chat:
post:
operationId: chatStream
summary: Chat with streaming response
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
message:
type: string
model:
type: string
required:
- message
responses:
"200":
description: Streaming chat response
content:
text/event-stream:
itemSchema:
oneOf:
- type: object
properties:
type:
type: string
enum:
- content
text:
type: string
required:
- type
- text
- type: object
properties:
type:
type: string
enum:
- done
usage:
type: object
properties:
input_tokens:
type: integer
output_tokens:
type: integer
required:
- input_tokens
- output_tokens
required:
- type
- usage
components:
schemas:
SSEEvent:
type: object
properties:
event:
type: string
data:
type: string
required:
- event
- data
218 changes: 218 additions & 0 deletions packages/openapi-typescript/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,224 @@ export type $defs = Record<string, never>;
export type operations = Record<string, never>;`,
},
],
[
"SSE > itemSchema with $ref to component schema",
{
given: {
openapi: "3.1",
info: { title: "SSE Ref Test", version: "1.0" },
paths: {
"/notifications": {
get: {
responses: {
200: {
description: "Notification stream",
content: {
"text/event-stream": {
itemSchema: {
$ref: "#/components/schemas/Notification",
},
},
},
},
},
},
},
},
components: {
schemas: {
Notification: {
type: "object",
properties: {
id: { type: "string" },
title: { type: "string" },
read: { type: "boolean" },
},
required: ["id", "title"],
},
},
},
},
want: `export interface paths {
"/notifications": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Notification stream */
200: {
headers: {
[name: string]: unknown;
};
content: {
"text/event-stream": components["schemas"]["Notification"];
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
Notification: {
id: string;
title: string;
read?: boolean;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;`,
},
],
[
"OpenAPI 3.2 > SSE streaming with itemSchema",
{
given: new URL("./fixtures/sse-stream-test.yaml", import.meta.url),
want: `export interface paths {
"/events": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Stream server-sent events */
get: operations["streamEvents"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/chat": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Chat with streaming response */
post: operations["chatStream"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
SSEEvent: {
event: string;
data: string;
};
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export interface operations {
streamEvents: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description SSE event stream */
200: {
headers: {
[name: string]: unknown;
};
content: {
"text/event-stream": {
/** @enum {string} */
event: "message" | "heartbeat" | "error";
data: string;
id?: number;
/** Format: date-time */
timestamp?: string;
};
};
};
};
};
chatStream: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
message: string;
model?: string;
};
};
};
responses: {
/** @description Streaming chat response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"text/event-stream": {
/** @enum {string} */
type: "content";
text: string;
} | {
/** @enum {string} */
type: "done";
usage: {
input_tokens: number;
output_tokens: number;
};
};
};
};
};
};
}`,
},
],
];

for (const [testName, { given, want, options, ci }] of tests) {
Expand Down
Loading
Loading