react-query-lightbase-codegen 3.1.6 → 3.1.8

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.
@@ -2,6 +2,33 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.generateApiClient = generateApiClient;
4
4
  const utils_1 = require("../utils");
5
+ /**
6
+ * Walks a request body schema (including allOf/oneOf/anyOf composition) and returns
7
+ * the union of property names contributed by all branches. Used to pick body fields
8
+ * out of the merged Params object so path/query/header values aren't sent as the body.
9
+ */
10
+ function collectBodyPropertyNames(schema, spec, visitedRefs = new Set()) {
11
+ if (!schema)
12
+ return [];
13
+ if ("$ref" in schema) {
14
+ if (visitedRefs.has(schema.$ref))
15
+ return [];
16
+ const next = new Set(visitedRefs).add(schema.$ref);
17
+ return collectBodyPropertyNames((0, utils_1.resolveSchema)(schema, spec), spec, next);
18
+ }
19
+ const names = new Set();
20
+ if (schema.properties)
21
+ Object.keys(schema.properties).forEach((k) => names.add(k));
22
+ for (const key of ["allOf", "oneOf", "anyOf"]) {
23
+ const branches = schema[key];
24
+ if (!branches)
25
+ continue;
26
+ for (const branch of branches) {
27
+ collectBodyPropertyNames(branch, spec, visitedRefs).forEach((n) => names.add(n));
28
+ }
29
+ }
30
+ return Array.from(names);
31
+ }
5
32
  function generateAxiosMethod(operation, spec) {
6
33
  const { method, path, operationId, summary, description, deprecated, parameters, requestBody, responses } = operation;
7
34
  // Generate JSDoc
@@ -59,9 +86,13 @@ function generateAxiosMethod(operation, spec) {
59
86
  : undefined;
60
87
  const requestBodyContent = requestBody && "content" in requestBody ? (0, utils_1.getContentSchema)(requestBody.content) : undefined;
61
88
  const requestBodySchema = requestBodyContent ? (0, utils_1.resolveSchema)(requestBodyContent, spec) : undefined;
89
+ // Resolve property names contributed by the body schema, walking allOf/oneOf/anyOf so
90
+ // composition-based bodies are still treated as object bodies (not "primitive").
91
+ const bodyPropertyNames = requestBodySchema ? collectBodyPropertyNames(requestBodySchema, spec) : [];
92
+ const hasObjectBody = bodyPropertyNames.length > 0;
62
93
  // Check if request body is a primitive type (string, number, boolean)
63
94
  const isPrimitiveRequestBody = requestBodySchema &&
64
- !requestBodySchema.properties &&
95
+ !hasObjectBody &&
65
96
  !requestBodySchema.type?.includes("object") &&
66
97
  !requestBodySchema.type?.includes("array");
67
98
  // Add request body type if it exists
@@ -98,11 +129,9 @@ function generateAxiosMethod(operation, spec) {
98
129
  ${queryParams.map((p) => `["${p.name}"]: data["${p.name}"]`).join(",\n ")}
99
130
  };`
100
131
  : "",
101
- requestBodySchema?.properties && !formDataSchema?.properties
132
+ hasObjectBody && !formDataSchema?.properties
102
133
  ? `const bodyData = {
103
- ${Object.entries(requestBodySchema.properties)
104
- .map(([key]) => `["${key}"]: data["${key}"]`)
105
- .join(",\n ")}
134
+ ${bodyPropertyNames.map((key) => `["${key}"]: data["${key}"]`).join(",\n ")}
106
135
  };`
107
136
  : "",
108
137
  formDataSchema?.properties
@@ -129,8 +158,8 @@ function generateAxiosMethod(operation, spec) {
129
158
  // Note: Don't set Content-Type for FormData - Axios will set it automatically with the correct boundary
130
159
  requestBody
131
160
  ? responseType === "void"
132
- ? `await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || requestBodySchema?.properties ? "bodyData" : "data"}, axiosConfig);`
133
- : `const res = await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || requestBodySchema?.properties ? "bodyData" : "data"}, axiosConfig);`
161
+ ? `await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || hasObjectBody ? "bodyData" : "data"}, axiosConfig);`
162
+ : `const res = await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || hasObjectBody ? "bodyData" : "data"}, axiosConfig);`
134
163
  : responseType === "void"
135
164
  ? `await apiClient.${method}<${responseType}>(url, axiosConfig);`
136
165
  : `const res = await apiClient.${method}<${responseType}>(url, axiosConfig);`,
@@ -18,10 +18,10 @@ function formatParamProperty(param, forceRequired = false) {
18
18
  function generateTypeDefinition(name, schema) {
19
19
  const description = !("$ref" in schema) && schema.description ? `/**\n * ${schema.description}\n */\n` : "";
20
20
  const typeValue = (0, utils_1.getTypeFromSchema)(schema);
21
- // Use 'type' for primitives, unions, and simple types
22
- // Use 'interface' only for complex objects with properties
23
- const isInterface = !("$ref" in schema) && schema.type === "object" && schema.properties;
24
- return isInterface
21
+ // Only emit `interface` when the body is a plain object literal. Composition
22
+ // (allOf/oneOf/anyOf) produces references or intersections that must use a type alias.
23
+ const canBeInterface = typeValue?.trimStart().startsWith("{");
24
+ return canBeInterface
25
25
  ? `${description}export interface ${(0, utils_1.sanitizeTypeName)(name)} ${typeValue}\n\n`
26
26
  : `${description}export type ${(0, utils_1.sanitizeTypeName)(name)} = ${typeValue}\n\n`;
27
27
  }
@@ -39,82 +39,67 @@ function generateTypeDefinitions(spec) {
39
39
  output += generateTypeDefinition(name, schema);
40
40
  generatedTypes.add(name);
41
41
  }
42
- // Generate request/response types
43
- if (spec.paths) {
44
- for (const [path, pathItem] of Object.entries(spec.paths)) {
45
- for (const [method, operation] of Object.entries(pathItem)) {
46
- if (method === "$ref")
47
- continue;
48
- const operationObject = operation;
49
- if (!operationObject)
50
- continue;
51
- const { operationId: badOperationId, requestBody, responses, parameters } = operationObject;
52
- const operationId = `${(0, utils_1.sanitizeTypeName)(badOperationId || `${path.replace(/\W+/g, "_")}`)}`;
53
- // Generate request body type
54
- if (requestBody) {
55
- const content = requestBody.content;
56
- const requestSchema = (0, utils_1.getContentSchema)(content);
57
- if (requestSchema) {
58
- const typeName = `${operationId}Request`;
59
- output += generateTypeDefinition(typeName, requestSchema);
60
- }
61
- }
62
- // Generate response types
63
- const errorTypes = [];
64
- if (responses) {
65
- for (const [code, response] of Object.entries(responses)) {
66
- const responseObj = response;
67
- const responseSchema = (0, utils_1.getContentSchema)(responseObj.content);
68
- if (responseSchema) {
69
- const typeName = `${operationId}Response${code}`;
70
- output += generateTypeDefinition(typeName, responseSchema);
71
- // Track non-2xx responses for error union type
72
- if (!code.startsWith("2")) {
73
- errorTypes.push(typeName);
74
- }
75
- }
76
- }
77
- }
78
- // Generate error union type if there are error responses
79
- if (errorTypes.length > 0) {
80
- output += `export type ${(0, utils_1.pascalCase)(operationId)}Error = ${errorTypes.join(" | ")};\n\n`;
81
- }
82
- // Build data type parts
83
- const dataProps = [];
84
- const urlParams = (parameters?.filter((p) => "in" in p && p.in === "path") ||
85
- []);
86
- const queryParams = (parameters?.filter((p) => "in" in p && p.in === "query") ||
87
- []);
88
- const headerParams = (parameters?.filter((p) => "in" in p && p.in === "header") ||
89
- []);
90
- const cookieParams = (parameters?.filter((p) => "in" in p && p.in === "cookie") ||
91
- []);
92
- // Add path, query, header, and cookie parameters
93
- urlParams.forEach((p) => dataProps.push(formatParamProperty(p, true))); // Path params always required
94
- queryParams.forEach((p) => dataProps.push(formatParamProperty(p)));
95
- headerParams.forEach((p) => dataProps.push(formatParamProperty(p)));
96
- cookieParams.forEach((p) => dataProps.push(formatParamProperty(p)));
97
- // Add request body type if it exists
98
- const hasData = (parameters && parameters.length > 0) || requestBody;
99
- let dataType = "undefined";
100
- const namedType = (0, utils_1.pascalCase)(operationId);
101
- if (hasData) {
102
- if (requestBody && dataProps.length > 0) {
103
- dataType = `${namedType}Request & { ${dataProps.join("; ")} }`;
104
- }
105
- else if (requestBody) {
106
- dataType = `${namedType}Request`;
107
- }
108
- else if (dataProps.length > 0) {
109
- dataType = `{ ${dataProps.join("; ")} }`;
110
- }
111
- else {
112
- dataType = "Record<string, never>";
42
+ // Generate request/response types. Use the same operation-collection helper as the
43
+ // client generator so pathItem-level parameters and $ref parameters stay in sync.
44
+ for (const { operationId, parameters, requestBody, responses } of (0, utils_1.collectOperations)(spec)) {
45
+ // Generate request body type
46
+ if (requestBody) {
47
+ const requestSchema = (0, utils_1.getContentSchema)(requestBody.content);
48
+ if (requestSchema) {
49
+ const typeName = `${operationId}Request`;
50
+ output += generateTypeDefinition(typeName, requestSchema);
51
+ }
52
+ }
53
+ // Generate response types
54
+ const errorTypes = [];
55
+ if (responses) {
56
+ for (const [code, response] of Object.entries(responses)) {
57
+ const responseObj = response;
58
+ const responseSchema = (0, utils_1.getContentSchema)(responseObj.content);
59
+ if (responseSchema) {
60
+ const typeName = `${operationId}Response${code}`;
61
+ output += generateTypeDefinition(typeName, responseSchema);
62
+ // Track non-2xx responses for error union type
63
+ if (!code.startsWith("2")) {
64
+ errorTypes.push(typeName);
113
65
  }
114
- output += `\n\nexport type ${(0, utils_1.pascalCase)(operationId)}Params = ${dataType};\n\n`;
115
66
  }
116
67
  }
117
68
  }
69
+ // Generate error union type if there are error responses
70
+ if (errorTypes.length > 0) {
71
+ output += `export type ${(0, utils_1.pascalCase)(operationId)}Error = ${errorTypes.join(" | ")};\n\n`;
72
+ }
73
+ // Build data type parts
74
+ const dataProps = [];
75
+ const urlParams = parameters.filter((p) => p.in === "path");
76
+ const queryParams = parameters.filter((p) => p.in === "query");
77
+ const headerParams = parameters.filter((p) => p.in === "header");
78
+ const cookieParams = parameters.filter((p) => p.in === "cookie");
79
+ // Add path, query, header, and cookie parameters
80
+ urlParams.forEach((p) => dataProps.push(formatParamProperty(p, true))); // Path params always required
81
+ queryParams.forEach((p) => dataProps.push(formatParamProperty(p)));
82
+ headerParams.forEach((p) => dataProps.push(formatParamProperty(p)));
83
+ cookieParams.forEach((p) => dataProps.push(formatParamProperty(p)));
84
+ // Add request body type if it exists
85
+ const hasData = parameters.length > 0 || requestBody;
86
+ let dataType = "undefined";
87
+ const namedType = (0, utils_1.pascalCase)(operationId);
88
+ if (hasData) {
89
+ if (requestBody && dataProps.length > 0) {
90
+ dataType = `${namedType}Request & { ${dataProps.join("; ")} }`;
91
+ }
92
+ else if (requestBody) {
93
+ dataType = `${namedType}Request`;
94
+ }
95
+ else if (dataProps.length > 0) {
96
+ dataType = `{ ${dataProps.join("; ")} }`;
97
+ }
98
+ else {
99
+ dataType = "Record<string, never>";
100
+ }
101
+ output += `\n\nexport type ${(0, utils_1.pascalCase)(operationId)}Params = ${dataType};\n\n`;
102
+ }
118
103
  }
119
104
  return output;
120
105
  }
package/dist/utils.js CHANGED
@@ -145,6 +145,36 @@ function specTitle(spec) {
145
145
  }
146
146
  return camelCase(title.toLowerCase().replace(/\s+/g, "-"));
147
147
  }
148
+ /**
149
+ * Builds the object-literal fragment ({...} or Record<...>) from a schema's own
150
+ * `properties` or `additionalProperties`, ignoring composition keywords. Returns
151
+ * undefined when the schema contributes no own object shape.
152
+ */
153
+ function getOwnObjectFragment(schema) {
154
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
155
+ const properties = Object.entries(schema.properties)
156
+ .map(([key, prop]) => {
157
+ const isRequired = schema.required?.includes(key);
158
+ const propertyType = getTypeFromSchema(prop);
159
+ const safeName = sanitizePropertyName(key);
160
+ const isDeprecated = "deprecated" in prop && prop.deprecated;
161
+ const hasDescription = "description" in prop && prop.description;
162
+ const desc = hasDescription || isDeprecated
163
+ ? `/**${hasDescription ? `\n * ${prop.description}` : ""}${isDeprecated ? "\n * @deprecated" : ""}\n */\n`
164
+ : "";
165
+ return `${desc}${safeName}${isRequired ? "" : "?"}: ${propertyType};`;
166
+ })
167
+ .join("\n");
168
+ return `{${properties}\n}`;
169
+ }
170
+ if (schema.additionalProperties) {
171
+ const valueType = typeof schema.additionalProperties === "boolean"
172
+ ? "unknown"
173
+ : getTypeFromSchema(schema.additionalProperties);
174
+ return `Record<string, ${valueType}>`;
175
+ }
176
+ return undefined;
177
+ }
148
178
  /**
149
179
  * Converts an OpenAPI schema object into a TypeScript type string.
150
180
  *
@@ -174,29 +204,36 @@ function getTypeFromSchema(schema) {
174
204
  }
175
205
  // Add "| null" for nullable types
176
206
  const nullable = "nullable" in schema && schema.nullable ? " | null" : "";
207
+ // Sibling properties / additionalProperties may appear next to allOf/oneOf/anyOf.
208
+ // Per the OpenAPI spec they constrain the parent and must be intersected with the composition.
209
+ const ownObjectFragment = getOwnObjectFragment(schema);
177
210
  // Handle allOf (intersection types)
178
211
  if ("allOf" in schema && schema.allOf) {
179
- const types = schema.allOf.map((s) => getTypeFromSchema(s)).filter(Boolean);
180
- if (types.length === 0)
212
+ const parts = schema.allOf.map((s) => getTypeFromSchema(s)).filter(Boolean);
213
+ if (ownObjectFragment)
214
+ parts.push(ownObjectFragment);
215
+ if (parts.length === 0)
181
216
  return "unknown";
182
- const result = types.length === 1 ? types[0] : `(${types.join(" & ")})`;
217
+ const result = parts.length === 1 ? parts[0] : `(${parts.join(" & ")})`;
183
218
  return `${result}${nullable}`;
184
219
  }
185
220
  // Handle oneOf (union types - exactly one)
186
221
  if ("oneOf" in schema && schema.oneOf) {
187
222
  const types = schema.oneOf.map((s) => getTypeFromSchema(s)).filter(Boolean);
188
- if (types.length === 0)
223
+ if (types.length === 0 && !ownObjectFragment)
189
224
  return "unknown";
190
- const result = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
191
- return `${result}${nullable}`;
225
+ const union = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
226
+ const composed = ownObjectFragment ? `(${union} & ${ownObjectFragment})` : union;
227
+ return `${composed}${nullable}`;
192
228
  }
193
229
  // Handle anyOf (union types - one or more)
194
230
  if ("anyOf" in schema && schema.anyOf) {
195
231
  const types = schema.anyOf.map((s) => getTypeFromSchema(s)).filter(Boolean);
196
- if (types.length === 0)
232
+ if (types.length === 0 && !ownObjectFragment)
197
233
  return "unknown";
198
- const result = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
199
- return `${result}${nullable}`;
234
+ const union = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
235
+ const composed = ownObjectFragment ? `(${union} & ${ownObjectFragment})` : union;
236
+ return `${composed}${nullable}`;
200
237
  }
201
238
  // Handle enums as union types
202
239
  if ("enum" in schema && schema.enum) {
@@ -242,33 +279,13 @@ function getTypeFromSchema(schema) {
242
279
  const itemType = getTypeFromSchema(schema.items);
243
280
  return `Array<${itemType}>${nullable}`;
244
281
  }
245
- case "object":
246
- // Handle objects with defined properties
247
- if (schema.properties) {
248
- const properties = Object.entries(schema.properties)
249
- .map(([key, prop]) => {
250
- const isRequired = schema.required?.includes(key);
251
- const propertyType = getTypeFromSchema(prop);
252
- const safeName = sanitizePropertyName(key);
253
- const isDeprecated = "deprecated" in prop && prop.deprecated;
254
- const hasDescription = "description" in prop && prop.description;
255
- const desc = hasDescription || isDeprecated
256
- ? `/**${hasDescription ? `\n * ${prop.description}` : ""}${isDeprecated ? "\n * @deprecated" : ""}\n */\n`
257
- : "";
258
- return `${desc}${safeName}${isRequired ? "" : "?"}: ${propertyType};`;
259
- })
260
- .join("\n");
261
- return `{${properties}\n}${nullable}`;
262
- }
263
- // Handle objects with additionalProperties
264
- if (schema.additionalProperties) {
265
- const valueType = typeof schema.additionalProperties === "boolean"
266
- ? "unknown"
267
- : getTypeFromSchema(schema.additionalProperties);
268
- return `Record<string, ${valueType}>${nullable}`;
269
- }
282
+ case "object": {
283
+ const fragment = getOwnObjectFragment(schema);
284
+ if (fragment)
285
+ return `${fragment}${nullable}`;
270
286
  // Default object type when no properties specified
271
287
  return `Record<string, unknown>${nullable}`;
288
+ }
272
289
  default:
273
290
  return `unknown${nullable}`;
274
291
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-query-lightbase-codegen",
3
- "version": "3.1.6",
3
+ "version": "3.1.8",
4
4
  "license": "MIT",
5
5
  "description": "Generate Axios API clients and React Query options from OpenAPI specifications",
6
6
  "exports": "./dist/index.js",
@@ -9,6 +9,34 @@ import {
9
9
  specTitle,
10
10
  } from "../utils";
11
11
 
12
+ /**
13
+ * Walks a request body schema (including allOf/oneOf/anyOf composition) and returns
14
+ * the union of property names contributed by all branches. Used to pick body fields
15
+ * out of the merged Params object so path/query/header values aren't sent as the body.
16
+ */
17
+ function collectBodyPropertyNames(
18
+ schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
19
+ spec: OpenAPIV3.Document,
20
+ visitedRefs: Set<string> = new Set()
21
+ ): string[] {
22
+ if (!schema) return [];
23
+ if ("$ref" in schema) {
24
+ if (visitedRefs.has(schema.$ref)) return [];
25
+ const next = new Set(visitedRefs).add(schema.$ref);
26
+ return collectBodyPropertyNames(resolveSchema(schema, spec), spec, next);
27
+ }
28
+ const names = new Set<string>();
29
+ if (schema.properties) Object.keys(schema.properties).forEach((k) => names.add(k));
30
+ for (const key of ["allOf", "oneOf", "anyOf"] as const) {
31
+ const branches = schema[key];
32
+ if (!branches) continue;
33
+ for (const branch of branches) {
34
+ collectBodyPropertyNames(branch, spec, visitedRefs).forEach((n) => names.add(n));
35
+ }
36
+ }
37
+ return Array.from(names);
38
+ }
39
+
12
40
  function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document): string {
13
41
  const { method, path, operationId, summary, description, deprecated, parameters, requestBody, responses } =
14
42
  operation;
@@ -76,10 +104,15 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
76
104
  requestBody && "content" in requestBody ? getContentSchema(requestBody.content) : undefined;
77
105
  const requestBodySchema = requestBodyContent ? resolveSchema(requestBodyContent, spec) : undefined;
78
106
 
107
+ // Resolve property names contributed by the body schema, walking allOf/oneOf/anyOf so
108
+ // composition-based bodies are still treated as object bodies (not "primitive").
109
+ const bodyPropertyNames = requestBodySchema ? collectBodyPropertyNames(requestBodySchema, spec) : [];
110
+ const hasObjectBody = bodyPropertyNames.length > 0;
111
+
79
112
  // Check if request body is a primitive type (string, number, boolean)
80
113
  const isPrimitiveRequestBody =
81
114
  requestBodySchema &&
82
- !requestBodySchema.properties &&
115
+ !hasObjectBody &&
83
116
  !requestBodySchema.type?.includes("object") &&
84
117
  !requestBodySchema.type?.includes("array");
85
118
 
@@ -123,11 +156,9 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
123
156
  };`
124
157
  : "",
125
158
 
126
- requestBodySchema?.properties && !formDataSchema?.properties
159
+ hasObjectBody && !formDataSchema?.properties
127
160
  ? `const bodyData = {
128
- ${Object.entries(requestBodySchema.properties)
129
- .map(([key]) => `["${key}"]: data["${key}"]`)
130
- .join(",\n ")}
161
+ ${bodyPropertyNames.map((key) => `["${key}"]: data["${key}"]`).join(",\n ")}
131
162
  };`
132
163
  : "",
133
164
 
@@ -155,8 +186,8 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
155
186
  // Note: Don't set Content-Type for FormData - Axios will set it automatically with the correct boundary
156
187
  requestBody
157
188
  ? responseType === "void"
158
- ? `await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || requestBodySchema?.properties ? "bodyData" : "data"}, axiosConfig);`
159
- : `const res = await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || requestBodySchema?.properties ? "bodyData" : "data"}, axiosConfig);`
189
+ ? `await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || hasObjectBody ? "bodyData" : "data"}, axiosConfig);`
190
+ : `const res = await apiClient.${method}<${responseType}>(url, ${formDataSchema?.properties || hasObjectBody ? "bodyData" : "data"}, axiosConfig);`
160
191
  : responseType === "void"
161
192
  ? `await apiClient.${method}<${responseType}>(url, axiosConfig);`
162
193
  : `const res = await apiClient.${method}<${responseType}>(url, axiosConfig);`,
@@ -1,5 +1,6 @@
1
1
  import type { OpenAPIV3 } from "openapi-types";
2
2
  import {
3
+ collectOperations,
3
4
  getContentSchema,
4
5
  getTypeFromSchema,
5
6
  pascalCase,
@@ -31,11 +32,11 @@ function generateTypeDefinition(
31
32
  const description = !("$ref" in schema) && schema.description ? `/**\n * ${schema.description}\n */\n` : "";
32
33
  const typeValue = getTypeFromSchema(schema);
33
34
 
34
- // Use 'type' for primitives, unions, and simple types
35
- // Use 'interface' only for complex objects with properties
36
- const isInterface = !("$ref" in schema) && schema.type === "object" && schema.properties;
35
+ // Only emit `interface` when the body is a plain object literal. Composition
36
+ // (allOf/oneOf/anyOf) produces references or intersections that must use a type alias.
37
+ const canBeInterface = typeValue?.trimStart().startsWith("{");
37
38
 
38
- return isInterface
39
+ return canBeInterface
39
40
  ? `${description}export interface ${sanitizeTypeName(name)} ${typeValue}\n\n`
40
41
  : `${description}export type ${sanitizeTypeName(name)} = ${typeValue}\n\n`;
41
42
  }
@@ -56,86 +57,71 @@ export function generateTypeDefinitions(spec: OpenAPIV3.Document): string {
56
57
  generatedTypes.add(name);
57
58
  }
58
59
 
59
- // Generate request/response types
60
- if (spec.paths) {
61
- for (const [path, pathItem] of Object.entries(spec.paths)) {
62
- for (const [method, operation] of Object.entries(pathItem as OpenAPIV3.PathItemObject)) {
63
- if (method === "$ref") continue;
64
-
65
- const operationObject = operation as OpenAPIV3.OperationObject;
66
- if (!operationObject) continue;
67
- const { operationId: badOperationId, requestBody, responses, parameters } = operationObject;
68
- const operationId = `${sanitizeTypeName(badOperationId || `${path.replace(/\W+/g, "_")}`)}`;
69
-
70
- // Generate request body type
71
- if (requestBody) {
72
- const content = (requestBody as OpenAPIV3.RequestBodyObject).content;
73
- const requestSchema = getContentSchema(content);
74
- if (requestSchema) {
75
- const typeName = `${operationId}Request`;
76
- output += generateTypeDefinition(typeName, requestSchema as OpenAPIV3.SchemaObject);
77
- }
78
- }
60
+ // Generate request/response types. Use the same operation-collection helper as the
61
+ // client generator so pathItem-level parameters and $ref parameters stay in sync.
62
+ for (const { operationId, parameters, requestBody, responses } of collectOperations(spec)) {
63
+ // Generate request body type
64
+ if (requestBody) {
65
+ const requestSchema = getContentSchema(requestBody.content);
66
+ if (requestSchema) {
67
+ const typeName = `${operationId}Request`;
68
+ output += generateTypeDefinition(typeName, requestSchema as OpenAPIV3.SchemaObject);
69
+ }
70
+ }
79
71
 
80
- // Generate response types
81
- const errorTypes: string[] = [];
82
- if (responses) {
83
- for (const [code, response] of Object.entries(responses)) {
84
- const responseObj = response as OpenAPIV3.ResponseObject;
85
- const responseSchema = getContentSchema(responseObj.content);
86
- if (responseSchema) {
87
- const typeName = `${operationId}Response${code}`;
88
- output += generateTypeDefinition(typeName, responseSchema as OpenAPIV3.SchemaObject);
89
-
90
- // Track non-2xx responses for error union type
91
- if (!code.startsWith("2")) {
92
- errorTypes.push(typeName);
93
- }
94
- }
72
+ // Generate response types
73
+ const errorTypes: string[] = [];
74
+ if (responses) {
75
+ for (const [code, response] of Object.entries(responses)) {
76
+ const responseObj = response as OpenAPIV3.ResponseObject;
77
+ const responseSchema = getContentSchema(responseObj.content);
78
+ if (responseSchema) {
79
+ const typeName = `${operationId}Response${code}`;
80
+ output += generateTypeDefinition(typeName, responseSchema as OpenAPIV3.SchemaObject);
81
+
82
+ // Track non-2xx responses for error union type
83
+ if (!code.startsWith("2")) {
84
+ errorTypes.push(typeName);
95
85
  }
96
86
  }
87
+ }
88
+ }
97
89
 
98
- // Generate error union type if there are error responses
99
- if (errorTypes.length > 0) {
100
- output += `export type ${pascalCase(operationId)}Error = ${errorTypes.join(" | ")};\n\n`;
101
- }
90
+ // Generate error union type if there are error responses
91
+ if (errorTypes.length > 0) {
92
+ output += `export type ${pascalCase(operationId)}Error = ${errorTypes.join(" | ")};\n\n`;
93
+ }
102
94
 
103
- // Build data type parts
104
- const dataProps: string[] = [];
105
-
106
- const urlParams = (parameters?.filter((p) => "in" in p && p.in === "path") ||
107
- []) as OpenAPIV3.ParameterObject[];
108
- const queryParams = (parameters?.filter((p) => "in" in p && p.in === "query") ||
109
- []) as OpenAPIV3.ParameterObject[];
110
- const headerParams = (parameters?.filter((p) => "in" in p && p.in === "header") ||
111
- []) as OpenAPIV3.ParameterObject[];
112
- const cookieParams = (parameters?.filter((p) => "in" in p && p.in === "cookie") ||
113
- []) as OpenAPIV3.ParameterObject[];
114
-
115
- // Add path, query, header, and cookie parameters
116
- urlParams.forEach((p) => dataProps.push(formatParamProperty(p, true))); // Path params always required
117
- queryParams.forEach((p) => dataProps.push(formatParamProperty(p)));
118
- headerParams.forEach((p) => dataProps.push(formatParamProperty(p)));
119
- cookieParams.forEach((p) => dataProps.push(formatParamProperty(p)));
120
-
121
- // Add request body type if it exists
122
- const hasData = (parameters && parameters.length > 0) || requestBody;
123
-
124
- let dataType = "undefined";
125
- const namedType = pascalCase(operationId);
126
- if (hasData) {
127
- if (requestBody && dataProps.length > 0) {
128
- dataType = `${namedType}Request & { ${dataProps.join("; ")} }`;
129
- } else if (requestBody) {
130
- dataType = `${namedType}Request`;
131
- } else if (dataProps.length > 0) {
132
- dataType = `{ ${dataProps.join("; ")} }`;
133
- } else {
134
- dataType = "Record<string, never>";
135
- }
136
- output += `\n\nexport type ${pascalCase(operationId)}Params = ${dataType};\n\n`;
137
- }
95
+ // Build data type parts
96
+ const dataProps: string[] = [];
97
+
98
+ const urlParams = parameters.filter((p) => p.in === "path");
99
+ const queryParams = parameters.filter((p) => p.in === "query");
100
+ const headerParams = parameters.filter((p) => p.in === "header");
101
+ const cookieParams = parameters.filter((p) => p.in === "cookie");
102
+
103
+ // Add path, query, header, and cookie parameters
104
+ urlParams.forEach((p) => dataProps.push(formatParamProperty(p, true))); // Path params always required
105
+ queryParams.forEach((p) => dataProps.push(formatParamProperty(p)));
106
+ headerParams.forEach((p) => dataProps.push(formatParamProperty(p)));
107
+ cookieParams.forEach((p) => dataProps.push(formatParamProperty(p)));
108
+
109
+ // Add request body type if it exists
110
+ const hasData = parameters.length > 0 || requestBody;
111
+
112
+ let dataType = "undefined";
113
+ const namedType = pascalCase(operationId);
114
+ if (hasData) {
115
+ if (requestBody && dataProps.length > 0) {
116
+ dataType = `${namedType}Request & { ${dataProps.join("; ")} }`;
117
+ } else if (requestBody) {
118
+ dataType = `${namedType}Request`;
119
+ } else if (dataProps.length > 0) {
120
+ dataType = `{ ${dataProps.join("; ")} }`;
121
+ } else {
122
+ dataType = "Record<string, never>";
138
123
  }
124
+ output += `\n\nexport type ${pascalCase(operationId)}Params = ${dataType};\n\n`;
139
125
  }
140
126
  }
141
127
 
package/src/utils.ts CHANGED
@@ -170,6 +170,41 @@ export function specTitle(spec: OpenAPIV3.Document): string {
170
170
  return camelCase(title.toLowerCase().replace(/\s+/g, "-"));
171
171
  }
172
172
 
173
+ /**
174
+ * Builds the object-literal fragment ({...} or Record<...>) from a schema's own
175
+ * `properties` or `additionalProperties`, ignoring composition keywords. Returns
176
+ * undefined when the schema contributes no own object shape.
177
+ */
178
+ function getOwnObjectFragment(schema: OpenAPIV3.SchemaObject): string | undefined {
179
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
180
+ const properties = Object.entries(schema.properties)
181
+ .map(([key, prop]) => {
182
+ const isRequired = schema.required?.includes(key);
183
+ const propertyType = getTypeFromSchema(prop);
184
+ const safeName = sanitizePropertyName(key);
185
+ const isDeprecated = "deprecated" in prop && prop.deprecated;
186
+ const hasDescription = "description" in prop && prop.description;
187
+ const desc =
188
+ hasDescription || isDeprecated
189
+ ? `/**${hasDescription ? `\n * ${prop.description}` : ""}${isDeprecated ? "\n * @deprecated" : ""}\n */\n`
190
+ : "";
191
+ return `${desc}${safeName}${isRequired ? "" : "?"}: ${propertyType};`;
192
+ })
193
+ .join("\n");
194
+ return `{${properties}\n}`;
195
+ }
196
+
197
+ if (schema.additionalProperties) {
198
+ const valueType =
199
+ typeof schema.additionalProperties === "boolean"
200
+ ? "unknown"
201
+ : getTypeFromSchema(schema.additionalProperties);
202
+ return `Record<string, ${valueType}>`;
203
+ }
204
+
205
+ return undefined;
206
+ }
207
+
173
208
  /**
174
209
  * Converts an OpenAPI schema object into a TypeScript type string.
175
210
  *
@@ -202,28 +237,35 @@ export function getTypeFromSchema(
202
237
  // Add "| null" for nullable types
203
238
  const nullable = "nullable" in schema && schema.nullable ? " | null" : "";
204
239
 
240
+ // Sibling properties / additionalProperties may appear next to allOf/oneOf/anyOf.
241
+ // Per the OpenAPI spec they constrain the parent and must be intersected with the composition.
242
+ const ownObjectFragment = getOwnObjectFragment(schema as OpenAPIV3.SchemaObject);
243
+
205
244
  // Handle allOf (intersection types)
206
245
  if ("allOf" in schema && schema.allOf) {
207
- const types = schema.allOf.map((s) => getTypeFromSchema(s)).filter(Boolean) as string[];
208
- if (types.length === 0) return "unknown";
209
- const result = types.length === 1 ? types[0] : `(${types.join(" & ")})`;
246
+ const parts = schema.allOf.map((s) => getTypeFromSchema(s)).filter(Boolean) as string[];
247
+ if (ownObjectFragment) parts.push(ownObjectFragment);
248
+ if (parts.length === 0) return "unknown";
249
+ const result = parts.length === 1 ? parts[0] : `(${parts.join(" & ")})`;
210
250
  return `${result}${nullable}`;
211
251
  }
212
252
 
213
253
  // Handle oneOf (union types - exactly one)
214
254
  if ("oneOf" in schema && schema.oneOf) {
215
255
  const types = schema.oneOf.map((s) => getTypeFromSchema(s)).filter(Boolean) as string[];
216
- if (types.length === 0) return "unknown";
217
- const result = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
218
- return `${result}${nullable}`;
256
+ if (types.length === 0 && !ownObjectFragment) return "unknown";
257
+ const union = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
258
+ const composed = ownObjectFragment ? `(${union} & ${ownObjectFragment})` : union;
259
+ return `${composed}${nullable}`;
219
260
  }
220
261
 
221
262
  // Handle anyOf (union types - one or more)
222
263
  if ("anyOf" in schema && schema.anyOf) {
223
264
  const types = schema.anyOf.map((s) => getTypeFromSchema(s)).filter(Boolean) as string[];
224
- if (types.length === 0) return "unknown";
225
- const result = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
226
- return `${result}${nullable}`;
265
+ if (types.length === 0 && !ownObjectFragment) return "unknown";
266
+ const union = types.length === 1 ? types[0] : `(${types.join(" | ")})`;
267
+ const composed = ownObjectFragment ? `(${union} & ${ownObjectFragment})` : union;
268
+ return `${composed}${nullable}`;
227
269
  }
228
270
 
229
271
  // Handle enums as union types
@@ -274,37 +316,13 @@ export function getTypeFromSchema(
274
316
  return `Array<${itemType}>${nullable}`;
275
317
  }
276
318
 
277
- case "object":
278
- // Handle objects with defined properties
279
- if (schema.properties) {
280
- const properties = Object.entries(schema.properties)
281
- .map(([key, prop]) => {
282
- const isRequired = schema.required?.includes(key);
283
- const propertyType = getTypeFromSchema(prop);
284
- const safeName = sanitizePropertyName(key);
285
- const isDeprecated = "deprecated" in prop && prop.deprecated;
286
- const hasDescription = "description" in prop && prop.description;
287
- const desc =
288
- hasDescription || isDeprecated
289
- ? `/**${hasDescription ? `\n * ${prop.description}` : ""}${isDeprecated ? "\n * @deprecated" : ""}\n */\n`
290
- : "";
291
- return `${desc}${safeName}${isRequired ? "" : "?"}: ${propertyType};`;
292
- })
293
- .join("\n");
294
- return `{${properties}\n}${nullable}`;
295
- }
296
-
297
- // Handle objects with additionalProperties
298
- if (schema.additionalProperties) {
299
- const valueType =
300
- typeof schema.additionalProperties === "boolean"
301
- ? "unknown"
302
- : getTypeFromSchema(schema.additionalProperties);
303
- return `Record<string, ${valueType}>${nullable}`;
304
- }
319
+ case "object": {
320
+ const fragment = getOwnObjectFragment(schema);
321
+ if (fragment) return `${fragment}${nullable}`;
305
322
 
306
323
  // Default object type when no properties specified
307
324
  return `Record<string, unknown>${nullable}`;
325
+ }
308
326
 
309
327
  default:
310
328
  return `unknown${nullable}`;