Skip to content

Commit 8c6c20f

Browse files
committed
feat: support OpenAPI 3.2 itemSchema for streaming media types
1 parent 3f07d0e commit 8c6c20f

File tree

8 files changed

+594
-2
lines changed

8 files changed

+594
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": minor
3+
---
4+
5+
Add OpenAPI 3.2 `itemSchema` support for SSE and streaming responses

packages/openapi-typescript/src/transform/media-type-object.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@ import transformSchemaObject from "./schema-object.js";
66
/**
77
* Transform MediaTypeObject nodes (4.8.14)
88
* @see https://spec.openapis.org/oas/v3.1.0#media-type-object
9+
*
10+
* OpenAPI 3.2 adds `itemSchema` for sequential/streaming media types (e.g. text/event-stream).
11+
* Both `schema` (complete payload) and `itemSchema` (per-item) can coexist on the same object.
12+
* For type generation we prefer `itemSchema` when present, as it describes the shape consumers
13+
* will actually parse per event/chunk.
914
*/
1015
export default function transformMediaTypeObject(
1116
mediaTypeObject: MediaTypeObject,
1217
options: TransformNodeOptions,
1318
): ts.TypeNode {
14-
if (!mediaTypeObject.schema) {
19+
const targetSchema = mediaTypeObject.itemSchema ?? mediaTypeObject.schema;
20+
if (!targetSchema) {
1521
return UNKNOWN;
1622
}
17-
return transformSchemaObject(mediaTypeObject.schema, options);
23+
return transformSchemaObject(targetSchema, options);
1824
}

packages/openapi-typescript/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ export interface RequestBodyObject extends Extensable {
286286
export interface MediaTypeObject extends Extensable {
287287
/** The schema defining the content of the request, response, or parameter. */
288288
schema?: SchemaObject | ReferenceObject;
289+
/** 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. */
290+
itemSchema?: SchemaObject | ReferenceObject;
289291
/** 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. */
290292
example?: any;
291293
/** 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. */
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# itemSchema is an OpenAPI 3.2 feature, but we use 3.1 here because
2+
# @redocly/openapi-core v1 does not support 3.2 validation yet.
3+
openapi: "3.1"
4+
info:
5+
title: SSE Streaming API
6+
version: "1.0"
7+
paths:
8+
/events:
9+
get:
10+
operationId: streamEvents
11+
summary: Stream server-sent events
12+
responses:
13+
"200":
14+
description: SSE event stream
15+
content:
16+
text/event-stream:
17+
itemSchema:
18+
type: object
19+
properties:
20+
event:
21+
type: string
22+
enum:
23+
- message
24+
- heartbeat
25+
- error
26+
data:
27+
type: string
28+
id:
29+
type: integer
30+
timestamp:
31+
type: string
32+
format: date-time
33+
required:
34+
- event
35+
- data
36+
/chat:
37+
post:
38+
operationId: chatStream
39+
summary: Chat with streaming response
40+
requestBody:
41+
required: true
42+
content:
43+
application/json:
44+
schema:
45+
type: object
46+
properties:
47+
message:
48+
type: string
49+
model:
50+
type: string
51+
required:
52+
- message
53+
responses:
54+
"200":
55+
description: Streaming chat response
56+
content:
57+
text/event-stream:
58+
itemSchema:
59+
oneOf:
60+
- type: object
61+
properties:
62+
type:
63+
type: string
64+
enum:
65+
- content
66+
text:
67+
type: string
68+
required:
69+
- type
70+
- text
71+
- type: object
72+
properties:
73+
type:
74+
type: string
75+
enum:
76+
- done
77+
usage:
78+
type: object
79+
properties:
80+
input_tokens:
81+
type: integer
82+
output_tokens:
83+
type: integer
84+
required:
85+
- input_tokens
86+
- output_tokens
87+
required:
88+
- type
89+
- usage
90+
components:
91+
schemas:
92+
SSEEvent:
93+
type: object
94+
properties:
95+
event:
96+
type: string
97+
data:
98+
type: string
99+
required:
100+
- event
101+
- data

packages/openapi-typescript/test/index.test.ts

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,224 @@ export type $defs = Record<string, never>;
11111111
export type operations = Record<string, never>;`,
11121112
},
11131113
],
1114+
[
1115+
"SSE > itemSchema with $ref to component schema",
1116+
{
1117+
given: {
1118+
openapi: "3.1",
1119+
info: { title: "SSE Ref Test", version: "1.0" },
1120+
paths: {
1121+
"/notifications": {
1122+
get: {
1123+
responses: {
1124+
200: {
1125+
description: "Notification stream",
1126+
content: {
1127+
"text/event-stream": {
1128+
itemSchema: {
1129+
$ref: "#/components/schemas/Notification",
1130+
},
1131+
},
1132+
},
1133+
},
1134+
},
1135+
},
1136+
},
1137+
},
1138+
components: {
1139+
schemas: {
1140+
Notification: {
1141+
type: "object",
1142+
properties: {
1143+
id: { type: "string" },
1144+
title: { type: "string" },
1145+
read: { type: "boolean" },
1146+
},
1147+
required: ["id", "title"],
1148+
},
1149+
},
1150+
},
1151+
},
1152+
want: `export interface paths {
1153+
"/notifications": {
1154+
parameters: {
1155+
query?: never;
1156+
header?: never;
1157+
path?: never;
1158+
cookie?: never;
1159+
};
1160+
get: {
1161+
parameters: {
1162+
query?: never;
1163+
header?: never;
1164+
path?: never;
1165+
cookie?: never;
1166+
};
1167+
requestBody?: never;
1168+
responses: {
1169+
/** @description Notification stream */
1170+
200: {
1171+
headers: {
1172+
[name: string]: unknown;
1173+
};
1174+
content: {
1175+
"text/event-stream": components["schemas"]["Notification"];
1176+
};
1177+
};
1178+
};
1179+
};
1180+
put?: never;
1181+
post?: never;
1182+
delete?: never;
1183+
options?: never;
1184+
head?: never;
1185+
patch?: never;
1186+
trace?: never;
1187+
};
1188+
}
1189+
export type webhooks = Record<string, never>;
1190+
export interface components {
1191+
schemas: {
1192+
Notification: {
1193+
id: string;
1194+
title: string;
1195+
read?: boolean;
1196+
};
1197+
};
1198+
responses: never;
1199+
parameters: never;
1200+
requestBodies: never;
1201+
headers: never;
1202+
pathItems: never;
1203+
}
1204+
export type $defs = Record<string, never>;
1205+
export type operations = Record<string, never>;`,
1206+
},
1207+
],
1208+
[
1209+
"OpenAPI 3.2 > SSE streaming with itemSchema",
1210+
{
1211+
given: new URL("./fixtures/sse-stream-test.yaml", import.meta.url),
1212+
want: `export interface paths {
1213+
"/events": {
1214+
parameters: {
1215+
query?: never;
1216+
header?: never;
1217+
path?: never;
1218+
cookie?: never;
1219+
};
1220+
/** Stream server-sent events */
1221+
get: operations["streamEvents"];
1222+
put?: never;
1223+
post?: never;
1224+
delete?: never;
1225+
options?: never;
1226+
head?: never;
1227+
patch?: never;
1228+
trace?: never;
1229+
};
1230+
"/chat": {
1231+
parameters: {
1232+
query?: never;
1233+
header?: never;
1234+
path?: never;
1235+
cookie?: never;
1236+
};
1237+
get?: never;
1238+
put?: never;
1239+
/** Chat with streaming response */
1240+
post: operations["chatStream"];
1241+
delete?: never;
1242+
options?: never;
1243+
head?: never;
1244+
patch?: never;
1245+
trace?: never;
1246+
};
1247+
}
1248+
export type webhooks = Record<string, never>;
1249+
export interface components {
1250+
schemas: {
1251+
SSEEvent: {
1252+
event: string;
1253+
data: string;
1254+
};
1255+
};
1256+
responses: never;
1257+
parameters: never;
1258+
requestBodies: never;
1259+
headers: never;
1260+
pathItems: never;
1261+
}
1262+
export type $defs = Record<string, never>;
1263+
export interface operations {
1264+
streamEvents: {
1265+
parameters: {
1266+
query?: never;
1267+
header?: never;
1268+
path?: never;
1269+
cookie?: never;
1270+
};
1271+
requestBody?: never;
1272+
responses: {
1273+
/** @description SSE event stream */
1274+
200: {
1275+
headers: {
1276+
[name: string]: unknown;
1277+
};
1278+
content: {
1279+
"text/event-stream": {
1280+
/** @enum {string} */
1281+
event: "message" | "heartbeat" | "error";
1282+
data: string;
1283+
id?: number;
1284+
/** Format: date-time */
1285+
timestamp?: string;
1286+
};
1287+
};
1288+
};
1289+
};
1290+
};
1291+
chatStream: {
1292+
parameters: {
1293+
query?: never;
1294+
header?: never;
1295+
path?: never;
1296+
cookie?: never;
1297+
};
1298+
requestBody: {
1299+
content: {
1300+
"application/json": {
1301+
message: string;
1302+
model?: string;
1303+
};
1304+
};
1305+
};
1306+
responses: {
1307+
/** @description Streaming chat response */
1308+
200: {
1309+
headers: {
1310+
[name: string]: unknown;
1311+
};
1312+
content: {
1313+
"text/event-stream": {
1314+
/** @enum {string} */
1315+
type: "content";
1316+
text: string;
1317+
} | {
1318+
/** @enum {string} */
1319+
type: "done";
1320+
usage: {
1321+
input_tokens: number;
1322+
output_tokens: number;
1323+
};
1324+
};
1325+
};
1326+
};
1327+
};
1328+
};
1329+
}`,
1330+
},
1331+
],
11141332
];
11151333

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

0 commit comments

Comments
 (0)