@pikku/openapi-parser 0.12.5 → 0.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @pikku/openapi-parser
2
2
 
3
+ ## 0.12.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 5dd1996: Fix credentials command crash when state.credentials is undefined, and add --credential flag to `pikku new addon` for per-user credential wiring (apikey, bearer, oauth2).
8
+
9
+ ## 0.12.6
10
+
11
+ ### Patch Changes
12
+
13
+ - a57ff11: Add Swagger 2.0 support: extract requestBody from body parameters, responseSchema from direct response schema, and component schemas from definitions. Fix duplicate .describe() on request body properties.
14
+
3
15
  ## 0.12.5
4
16
 
5
17
  ### Patch Changes
package/dist/codegen.d.ts CHANGED
@@ -14,6 +14,7 @@ interface AddonVars {
14
14
  interface CodegenFlags {
15
15
  oauth: boolean;
16
16
  secret: boolean;
17
+ credential?: 'apikey' | 'bearer' | 'oauth2';
17
18
  mcp?: boolean;
18
19
  }
19
20
  export declare function generateAddonFromOpenAPI(spec: ParsedSpec, vars: AddonVars, flags: CodegenFlags): Record<string, string>;
package/dist/codegen.js CHANGED
@@ -119,7 +119,10 @@ function generateFunctionFile(named, parsed, vars, ctx, spec, flags) {
119
119
  const outputName = `${pascalName}Output`;
120
120
  // Determine error imports needed
121
121
  const errorClasses = getErrorClassesForResponses(parsed.errorResponses);
122
- lines.push("import { z } from 'zod'");
122
+ const needsZod = hasInput || !!parsed.responseSchema;
123
+ if (needsZod) {
124
+ lines.push("import { z } from 'zod'");
125
+ }
123
126
  lines.push("import { pikkuSessionlessFunc } from '#pikku'");
124
127
  if (errorClasses.length > 0) {
125
128
  lines.push(`import { ${errorClasses.join(', ')} } from '@pikku/core/errors'`);
@@ -183,10 +186,7 @@ function buildInputSchema(parsed, ctx) {
183
186
  continue;
184
187
  const isOptional = !requiredSet.has(key);
185
188
  const zodCode = schemaToZod(propSchema, ctx, { optional: isOptional });
186
- const withDesc = propSchema.description
187
- ? `${zodCode}.describe(${JSON.stringify(propSchema.description)})`
188
- : zodCode;
189
- props.push(` ${safeKey(key)}: ${withDesc},`);
189
+ props.push(` ${safeKey(key)}: ${zodCode},`);
190
190
  }
191
191
  }
192
192
  else {
@@ -235,7 +235,10 @@ function generateServiceFile(spec, opPairs, vars, flags) {
235
235
  const lines = [];
236
236
  // Always import all error classes used in the switch statement
237
237
  const allErrorClasses = new Set(Object.values(STATUS_TO_ERROR));
238
- if (flags.oauth) {
238
+ if (flags.credential && flags.credential !== 'oauth2') {
239
+ // Per-user credential: no special imports needed, creds passed via constructor
240
+ }
241
+ else if (flags.oauth || flags.credential === 'oauth2') {
239
242
  lines.push("import { OAuth2Client } from '@pikku/core/oauth2'");
240
243
  lines.push("import type { TypedSecretService } from '#pikku/secrets/pikku-secrets.gen.js'");
241
244
  }
@@ -286,7 +289,14 @@ function generateServiceFile(spec, opPairs, vars, flags) {
286
289
  // Class declaration
287
290
  lines.push(`export class ${pascalName}Service {`);
288
291
  lines.push(' private baseUrl: string');
289
- if (flags.oauth) {
292
+ if (flags.credential && flags.credential !== 'oauth2') {
293
+ const credField = flags.credential === 'bearer' ? 'token' : 'apiKey';
294
+ lines.push('');
295
+ lines.push(` constructor(private creds: { ${credField}: string }, variables: TypedVariablesService) {`);
296
+ lines.push(` this.baseUrl = variables.get('${screamingName}_BASE_URL') as string`);
297
+ lines.push(' }');
298
+ }
299
+ else if (flags.oauth || flags.credential === 'oauth2') {
290
300
  lines.push(' private oauth: OAuth2Client');
291
301
  lines.push('');
292
302
  lines.push(` constructor(secrets: TypedSecretService, variables: TypedVariablesService) {`);
@@ -359,7 +369,29 @@ function generateServiceFile(spec, opPairs, vars, flags) {
359
369
  lines.push(' url.searchParams.set(key, value)');
360
370
  lines.push(' }');
361
371
  lines.push('');
362
- if (flags.oauth) {
372
+ if (flags.credential && flags.credential !== 'oauth2') {
373
+ // Per-user credential: use creds from wire.getCredentials()
374
+ if (flags.credential === 'bearer') {
375
+ lines.push(' headers.Authorization = `Bearer ${this.creds.token}`');
376
+ }
377
+ else {
378
+ // apikey: check spec for custom header name
379
+ const apiKeyScheme = Object.values(spec.securitySchemes).find((s) => s.type === 'apiKey');
380
+ if (apiKeyScheme?.name && apiKeyScheme?.in === 'header') {
381
+ lines.push(` headers[${JSON.stringify(apiKeyScheme.name)}] = this.creds.apiKey`);
382
+ }
383
+ else {
384
+ lines.push(' headers.Authorization = `Bearer ${this.creds.apiKey}`');
385
+ }
386
+ }
387
+ lines.push('');
388
+ lines.push(' const response = await fetch(url.toString(), {');
389
+ lines.push(' method,');
390
+ lines.push(' headers,');
391
+ lines.push(' body: body ? JSON.stringify(body) : undefined,');
392
+ lines.push(' })');
393
+ }
394
+ else if (flags.oauth || flags.credential === 'oauth2') {
363
395
  lines.push(' const response = await this.oauth.request(url.toString(), {');
364
396
  lines.push(' method,');
365
397
  lines.push(' headers,');
@@ -38,12 +38,11 @@ export async function parseOpenAPISpec(filePath) {
38
38
  const authType = detectAuthType(doc);
39
39
  const securitySchemes = extractSecuritySchemes(doc);
40
40
  const tagDescriptions = extractTagDescriptions(doc);
41
- // Extract component schemas
41
+ // Extract component schemas (OpenAPI 3.x: components.schemas, Swagger 2.x: definitions)
42
42
  const componentSchemas = {};
43
- if (doc.components?.schemas) {
44
- for (const [name, schema] of Object.entries(doc.components.schemas)) {
45
- componentSchemas[name] = schema;
46
- }
43
+ const rawSchemas = doc.components?.schemas ?? doc.definitions ?? {};
44
+ for (const [name, schema] of Object.entries(rawSchemas)) {
45
+ componentSchemas[name] = schema;
47
46
  }
48
47
  // Extract operations (skip deprecated)
49
48
  const operations = [];
@@ -59,7 +58,9 @@ export async function parseOpenAPISpec(filePath) {
59
58
  if (op.deprecated === true)
60
59
  continue;
61
60
  const allParams = [...sharedParams, ...(op.parameters ?? [])];
62
- const body = op.requestBody;
61
+ // OpenAPI 3.x: op.requestBody, Swagger 2.x: parameters[].in === 'body'
62
+ const body3 = op.requestBody;
63
+ const body2 = allParams.find((p) => p.in === 'body');
63
64
  operations.push({
64
65
  operationId: op.operationId,
65
66
  method,
@@ -71,8 +72,8 @@ export async function parseOpenAPISpec(filePath) {
71
72
  queryParams: extractParams(allParams, 'query'),
72
73
  headerParams: extractParams(allParams, 'header'),
73
74
  requestBody: extractRequestBody(op),
74
- requestBodyDescription: body?.description,
75
- requestBodyRequired: body?.required,
75
+ requestBodyDescription: body3?.description ?? body2?.description,
76
+ requestBodyRequired: body3?.required ?? body2?.required,
76
77
  responseSchema: extractResponseSchema(op),
77
78
  responseDescription: extractResponseDescription(op),
78
79
  errorResponses: extractErrorResponses(op),
@@ -184,20 +185,25 @@ function extractParams(params, location) {
184
185
  }));
185
186
  }
186
187
  function extractRequestBody(op) {
188
+ // OpenAPI 3.x: op.requestBody.content['application/json'].schema
187
189
  const body = op.requestBody;
188
- if (!body)
189
- return undefined;
190
- const content = body.content;
191
- if (!content)
192
- return undefined;
193
- // Prefer JSON
194
- const jsonContent = content['application/json'];
195
- if (jsonContent?.schema)
196
- return jsonContent.schema;
197
- // Fallback to first content type
198
- const firstKey = Object.keys(content)[0];
199
- if (firstKey && content[firstKey]?.schema) {
200
- return content[firstKey].schema;
190
+ if (body) {
191
+ const content = body.content;
192
+ if (content) {
193
+ const jsonContent = content['application/json'];
194
+ if (jsonContent?.schema)
195
+ return jsonContent.schema;
196
+ const firstKey = Object.keys(content)[0];
197
+ if (firstKey && content[firstKey]?.schema) {
198
+ return content[firstKey].schema;
199
+ }
200
+ }
201
+ }
202
+ // Swagger 2.x: parameters[].in === 'body' → schema
203
+ if (op.parameters) {
204
+ const bodyParam = op.parameters.find((p) => p.in === 'body');
205
+ if (bodyParam?.schema)
206
+ return bodyParam.schema;
201
207
  }
202
208
  return undefined;
203
209
  }
@@ -221,24 +227,32 @@ function extractResponseSchema(op) {
221
227
  const resp = responses[code];
222
228
  if (!resp)
223
229
  continue;
230
+ // OpenAPI 3.x: resp.content['application/json'].schema
224
231
  const content = resp.content;
225
- if (!content)
226
- continue;
227
- const jsonContent = content['application/json'];
228
- if (jsonContent?.schema)
229
- return jsonContent.schema;
230
- const firstKey = Object.keys(content)[0];
231
- if (firstKey && content[firstKey]?.schema) {
232
- return content[firstKey].schema;
232
+ if (content) {
233
+ const jsonContent = content['application/json'];
234
+ if (jsonContent?.schema)
235
+ return jsonContent.schema;
236
+ const firstKey = Object.keys(content)[0];
237
+ if (firstKey && content[firstKey]?.schema) {
238
+ return content[firstKey].schema;
239
+ }
233
240
  }
241
+ // Swagger 2.x: resp.schema directly
242
+ if (resp.schema)
243
+ return resp.schema;
234
244
  }
235
245
  // Fallback: any 2xx
236
246
  for (const [code, resp] of Object.entries(responses)) {
237
- if (code.startsWith('2') && resp.content) {
247
+ if (!code.startsWith('2'))
248
+ continue;
249
+ if (resp.content) {
238
250
  const jsonContent = resp.content['application/json'];
239
251
  if (jsonContent?.schema)
240
252
  return jsonContent.schema;
241
253
  }
254
+ if (resp.schema)
255
+ return resp.schema;
242
256
  }
243
257
  return undefined;
244
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/openapi-parser",
3
- "version": "0.12.5",
3
+ "version": "0.12.7",
4
4
  "description": "OpenAPI spec parser and code generator for Pikku",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
package/src/codegen.ts CHANGED
@@ -32,6 +32,7 @@ interface AddonVars {
32
32
  interface CodegenFlags {
33
33
  oauth: boolean
34
34
  secret: boolean
35
+ credential?: 'apikey' | 'bearer' | 'oauth2'
35
36
  mcp?: boolean
36
37
  }
37
38
 
@@ -211,7 +212,10 @@ function generateFunctionFile(
211
212
  // Determine error imports needed
212
213
  const errorClasses = getErrorClassesForResponses(parsed.errorResponses)
213
214
 
214
- lines.push("import { z } from 'zod'")
215
+ const needsZod = hasInput || !!parsed.responseSchema
216
+ if (needsZod) {
217
+ lines.push("import { z } from 'zod'")
218
+ }
215
219
  lines.push("import { pikkuSessionlessFunc } from '#pikku'")
216
220
 
217
221
  if (errorClasses.length > 0) {
@@ -301,10 +305,7 @@ function buildInputSchema(
301
305
  if (propSchema.readOnly) continue
302
306
  const isOptional = !requiredSet.has(key)
303
307
  const zodCode = schemaToZod(propSchema, ctx, { optional: isOptional })
304
- const withDesc = propSchema.description
305
- ? `${zodCode}.describe(${JSON.stringify(propSchema.description)})`
306
- : zodCode
307
- props.push(` ${safeKey(key)}: ${withDesc},`)
308
+ props.push(` ${safeKey(key)}: ${zodCode},`)
308
309
  }
309
310
  } else {
310
311
  const bodyZod = schemaToZod(parsed.requestBody, ctx)
@@ -386,7 +387,9 @@ function generateServiceFile(
386
387
  // Always import all error classes used in the switch statement
387
388
  const allErrorClasses = new Set<string>(Object.values(STATUS_TO_ERROR))
388
389
 
389
- if (flags.oauth) {
390
+ if (flags.credential && flags.credential !== 'oauth2') {
391
+ // Per-user credential: no special imports needed, creds passed via constructor
392
+ } else if (flags.oauth || flags.credential === 'oauth2') {
390
393
  lines.push("import { OAuth2Client } from '@pikku/core/oauth2'")
391
394
  lines.push(
392
395
  "import type { TypedSecretService } from '#pikku/secrets/pikku-secrets.gen.js'"
@@ -458,7 +461,17 @@ function generateServiceFile(
458
461
  lines.push(`export class ${pascalName}Service {`)
459
462
  lines.push(' private baseUrl: string')
460
463
 
461
- if (flags.oauth) {
464
+ if (flags.credential && flags.credential !== 'oauth2') {
465
+ const credField = flags.credential === 'bearer' ? 'token' : 'apiKey'
466
+ lines.push('')
467
+ lines.push(
468
+ ` constructor(private creds: { ${credField}: string }, variables: TypedVariablesService) {`
469
+ )
470
+ lines.push(
471
+ ` this.baseUrl = variables.get('${screamingName}_BASE_URL') as string`
472
+ )
473
+ lines.push(' }')
474
+ } else if (flags.oauth || flags.credential === 'oauth2') {
462
475
  lines.push(' private oauth: OAuth2Client')
463
476
  lines.push('')
464
477
  lines.push(
@@ -548,7 +561,30 @@ function generateServiceFile(
548
561
  lines.push(' }')
549
562
  lines.push('')
550
563
 
551
- if (flags.oauth) {
564
+ if (flags.credential && flags.credential !== 'oauth2') {
565
+ // Per-user credential: use creds from wire.getCredentials()
566
+ if (flags.credential === 'bearer') {
567
+ lines.push(' headers.Authorization = `Bearer ${this.creds.token}`')
568
+ } else {
569
+ // apikey: check spec for custom header name
570
+ const apiKeyScheme = Object.values(spec.securitySchemes).find(
571
+ (s) => s.type === 'apiKey'
572
+ )
573
+ if (apiKeyScheme?.name && apiKeyScheme?.in === 'header') {
574
+ lines.push(
575
+ ` headers[${JSON.stringify(apiKeyScheme.name)}] = this.creds.apiKey`
576
+ )
577
+ } else {
578
+ lines.push(' headers.Authorization = `Bearer ${this.creds.apiKey}`')
579
+ }
580
+ }
581
+ lines.push('')
582
+ lines.push(' const response = await fetch(url.toString(), {')
583
+ lines.push(' method,')
584
+ lines.push(' headers,')
585
+ lines.push(' body: body ? JSON.stringify(body) : undefined,')
586
+ lines.push(' })')
587
+ } else if (flags.oauth || flags.credential === 'oauth2') {
552
588
  lines.push(
553
589
  ' const response = await this.oauth.request(url.toString(), {'
554
590
  )
@@ -1,6 +1,9 @@
1
1
  import { strict as assert } from 'assert'
2
2
  import { describe, test } from 'node:test'
3
- import { computeContractHash } from './parse-openapi.js'
3
+ import { writeFile, rm, mkdtemp } from 'fs/promises'
4
+ import { join } from 'path'
5
+ import { tmpdir } from 'os'
6
+ import { computeContractHash, parseOpenAPISpec } from './parse-openapi.js'
4
7
  import type { ParsedSpec } from './parse-openapi.js'
5
8
 
6
9
  function makeSpec(overrides: Partial<ParsedSpec> = {}): ParsedSpec {
@@ -148,3 +151,151 @@ describe('computeContractHash', () => {
148
151
  )
149
152
  })
150
153
  })
154
+
155
+ describe('parseOpenAPISpec — Swagger 2.0', () => {
156
+ const swagger2Spec = {
157
+ swagger: '2.0',
158
+ info: { title: 'Test API', version: '1.0.0' },
159
+ host: 'api.example.com',
160
+ basePath: '/v1',
161
+ schemes: ['https'],
162
+ definitions: {
163
+ Pet: {
164
+ type: 'object',
165
+ required: ['name'],
166
+ properties: {
167
+ id: { type: 'integer', format: 'int64' },
168
+ name: { type: 'string' },
169
+ status: { type: 'string', enum: ['available', 'sold'], description: 'pet status' },
170
+ },
171
+ },
172
+ },
173
+ securityDefinitions: {
174
+ api_key: { type: 'apiKey', name: 'api_key', in: 'header' },
175
+ },
176
+ paths: {
177
+ '/pets': {
178
+ get: {
179
+ operationId: 'listPets',
180
+ summary: 'List all pets',
181
+ parameters: [
182
+ { in: 'query', name: 'status', type: 'string', description: 'Filter by status' },
183
+ ],
184
+ responses: {
185
+ '200': {
186
+ description: 'A list of pets',
187
+ schema: { type: 'array', items: { $ref: '#/definitions/Pet' } },
188
+ },
189
+ },
190
+ },
191
+ post: {
192
+ operationId: 'createPet',
193
+ summary: 'Create a pet',
194
+ parameters: [
195
+ {
196
+ in: 'body',
197
+ name: 'body',
198
+ description: 'Pet to create',
199
+ required: true,
200
+ schema: { $ref: '#/definitions/Pet' },
201
+ },
202
+ ],
203
+ responses: {
204
+ '201': {
205
+ description: 'Created',
206
+ schema: { $ref: '#/definitions/Pet' },
207
+ },
208
+ '400': { description: 'Bad request' },
209
+ },
210
+ },
211
+ },
212
+ '/pets/{petId}': {
213
+ get: {
214
+ operationId: 'getPet',
215
+ parameters: [
216
+ { in: 'path', name: 'petId', type: 'integer', required: true },
217
+ ],
218
+ responses: {
219
+ '200': {
220
+ description: 'A pet',
221
+ schema: { $ref: '#/definitions/Pet' },
222
+ },
223
+ },
224
+ },
225
+ },
226
+ },
227
+ }
228
+
229
+ let tmpDir: string
230
+ let specPath: string
231
+
232
+ test('setup', async () => {
233
+ tmpDir = await mkdtemp(join(tmpdir(), 'openapi-test-'))
234
+ specPath = join(tmpDir, 'spec.json')
235
+ await writeFile(specPath, JSON.stringify(swagger2Spec))
236
+ })
237
+
238
+ test('extracts component schemas from definitions', async () => {
239
+ const spec = await parseOpenAPISpec(specPath)
240
+ assert.ok(spec.componentSchemas.Pet)
241
+ assert.equal(spec.componentSchemas.Pet.type, 'object')
242
+ assert.ok(spec.componentSchemas.Pet.properties?.name)
243
+ })
244
+
245
+ test('extracts server URLs from host/basePath/schemes', async () => {
246
+ const spec = await parseOpenAPISpec(specPath)
247
+ assert.deepEqual(spec.serverUrls, ['https://api.example.com/v1'])
248
+ })
249
+
250
+ test('detects apiKey auth from securityDefinitions', async () => {
251
+ const spec = await parseOpenAPISpec(specPath)
252
+ assert.equal(spec.authType, 'apiKey')
253
+ })
254
+
255
+ test('extracts requestBody from body parameter', async () => {
256
+ const spec = await parseOpenAPISpec(specPath)
257
+ const createPet = spec.operations.find(op => op.operationId === 'createPet')
258
+ assert.ok(createPet?.requestBody)
259
+ assert.equal(createPet.requestBody.type, 'object')
260
+ assert.ok(createPet.requestBody.properties?.name)
261
+ assert.equal(createPet.requestBodyRequired, true)
262
+ assert.equal(createPet.requestBodyDescription, 'Pet to create')
263
+ })
264
+
265
+ test('extracts responseSchema from Swagger 2.0 response', async () => {
266
+ const spec = await parseOpenAPISpec(specPath)
267
+ const createPet = spec.operations.find(op => op.operationId === 'createPet')
268
+ assert.ok(createPet?.responseSchema)
269
+ assert.equal(createPet.responseSchema.type, 'object')
270
+
271
+ const listPets = spec.operations.find(op => op.operationId === 'listPets')
272
+ assert.ok(listPets?.responseSchema)
273
+ assert.equal(listPets.responseSchema.type, 'array')
274
+ })
275
+
276
+ test('extracts query params', async () => {
277
+ const spec = await parseOpenAPISpec(specPath)
278
+ const listPets = spec.operations.find(op => op.operationId === 'listPets')
279
+ assert.equal(listPets?.queryParams.length, 1)
280
+ assert.equal(listPets?.queryParams[0].name, 'status')
281
+ })
282
+
283
+ test('extracts path params', async () => {
284
+ const spec = await parseOpenAPISpec(specPath)
285
+ const getPet = spec.operations.find(op => op.operationId === 'getPet')
286
+ assert.equal(getPet?.pathParams.length, 1)
287
+ assert.equal(getPet?.pathParams[0].name, 'petId')
288
+ assert.equal(getPet?.pathParams[0].required, true)
289
+ })
290
+
291
+ test('extracts error responses', async () => {
292
+ const spec = await parseOpenAPISpec(specPath)
293
+ const createPet = spec.operations.find(op => op.operationId === 'createPet')
294
+ assert.equal(createPet?.errorResponses.length, 1)
295
+ assert.equal(createPet?.errorResponses[0].statusCode, 400)
296
+ })
297
+
298
+ test('cleanup', async () => {
299
+ await rm(tmpDir, { recursive: true, force: true })
300
+ })
301
+ })
@@ -106,12 +106,11 @@ export async function parseOpenAPISpec(filePath: string): Promise<ParsedSpec> {
106
106
  const securitySchemes = extractSecuritySchemes(doc)
107
107
  const tagDescriptions = extractTagDescriptions(doc)
108
108
 
109
- // Extract component schemas
109
+ // Extract component schemas (OpenAPI 3.x: components.schemas, Swagger 2.x: definitions)
110
110
  const componentSchemas: Record<string, OpenAPISchema> = {}
111
- if (doc.components?.schemas) {
112
- for (const [name, schema] of Object.entries(doc.components.schemas)) {
113
- componentSchemas[name] = schema as OpenAPISchema
114
- }
111
+ const rawSchemas = doc.components?.schemas ?? doc.definitions ?? {}
112
+ for (const [name, schema] of Object.entries(rawSchemas)) {
113
+ componentSchemas[name] = schema as OpenAPISchema
115
114
  }
116
115
 
117
116
  // Extract operations (skip deprecated)
@@ -132,7 +131,9 @@ export async function parseOpenAPISpec(filePath: string): Promise<ParsedSpec> {
132
131
 
133
132
  const allParams: any[] = [...sharedParams, ...(op.parameters ?? [])]
134
133
 
135
- const body = op.requestBody
134
+ // OpenAPI 3.x: op.requestBody, Swagger 2.x: parameters[].in === 'body'
135
+ const body3 = op.requestBody
136
+ const body2 = allParams.find((p: any) => p.in === 'body')
136
137
 
137
138
  operations.push({
138
139
  operationId: op.operationId,
@@ -145,8 +146,8 @@ export async function parseOpenAPISpec(filePath: string): Promise<ParsedSpec> {
145
146
  queryParams: extractParams(allParams, 'query'),
146
147
  headerParams: extractParams(allParams, 'header'),
147
148
  requestBody: extractRequestBody(op),
148
- requestBodyDescription: body?.description,
149
- requestBodyRequired: body?.required,
149
+ requestBodyDescription: body3?.description ?? body2?.description,
150
+ requestBodyRequired: body3?.required ?? body2?.required,
150
151
  responseSchema: extractResponseSchema(op),
151
152
  responseDescription: extractResponseDescription(op),
152
153
  errorResponses: extractErrorResponses(op),
@@ -266,20 +267,25 @@ function extractParams(
266
267
  }
267
268
 
268
269
  function extractRequestBody(op: any): OpenAPISchema | undefined {
270
+ // OpenAPI 3.x: op.requestBody.content['application/json'].schema
269
271
  const body = op.requestBody
270
- if (!body) return undefined
271
-
272
- const content = body.content
273
- if (!content) return undefined
272
+ if (body) {
273
+ const content = body.content
274
+ if (content) {
275
+ const jsonContent = content['application/json']
276
+ if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
274
277
 
275
- // Prefer JSON
276
- const jsonContent = content['application/json']
277
- if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
278
+ const firstKey = Object.keys(content)[0]
279
+ if (firstKey && content[firstKey]?.schema) {
280
+ return content[firstKey].schema as OpenAPISchema
281
+ }
282
+ }
283
+ }
278
284
 
279
- // Fallback to first content type
280
- const firstKey = Object.keys(content)[0]
281
- if (firstKey && content[firstKey]?.schema) {
282
- return content[firstKey].schema as OpenAPISchema
285
+ // Swagger 2.x: parameters[].in === 'body' → schema
286
+ if (op.parameters) {
287
+ const bodyParam = op.parameters.find((p: any) => p.in === 'body')
288
+ if (bodyParam?.schema) return bodyParam.schema as OpenAPISchema
283
289
  }
284
290
 
285
291
  return undefined
@@ -304,24 +310,30 @@ function extractResponseSchema(op: any): OpenAPISchema | undefined {
304
310
  const resp = responses[code]
305
311
  if (!resp) continue
306
312
 
313
+ // OpenAPI 3.x: resp.content['application/json'].schema
307
314
  const content = resp.content
308
- if (!content) continue
309
-
310
- const jsonContent = content['application/json']
311
- if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
315
+ if (content) {
316
+ const jsonContent = content['application/json']
317
+ if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
312
318
 
313
- const firstKey = Object.keys(content)[0]
314
- if (firstKey && content[firstKey]?.schema) {
315
- return content[firstKey].schema as OpenAPISchema
319
+ const firstKey = Object.keys(content)[0]
320
+ if (firstKey && content[firstKey]?.schema) {
321
+ return content[firstKey].schema as OpenAPISchema
322
+ }
316
323
  }
324
+
325
+ // Swagger 2.x: resp.schema directly
326
+ if (resp.schema) return resp.schema as OpenAPISchema
317
327
  }
318
328
 
319
329
  // Fallback: any 2xx
320
330
  for (const [code, resp] of Object.entries(responses) as [string, any][]) {
321
- if (code.startsWith('2') && resp.content) {
331
+ if (!code.startsWith('2')) continue
332
+ if (resp.content) {
322
333
  const jsonContent = resp.content['application/json']
323
334
  if (jsonContent?.schema) return jsonContent.schema as OpenAPISchema
324
335
  }
336
+ if (resp.schema) return resp.schema as OpenAPISchema
325
337
  }
326
338
 
327
339
  return undefined