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/shaggy-otters-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"swagger-typescript-api": minor
---

Add support for multiple request/response types to be defined as unions
100 changes: 81 additions & 19 deletions src/schema-routes/schema-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,16 +331,55 @@ export class SchemaRoutes {

/* content: { "multipart/form-data": { schema: {...} }, "application/json": { schema: {...} } } */

/* for example: dataType = "multipart/form-data" */
const contentTypes = Object.keys(content);

// if there's only one content type, return it
if (contentTypes.length === 1 && content[contentTypes[0]]?.schema) {
return {
...content[contentTypes[0]].schema,
dataType: contentTypes[0],
};
}

// Check if there are multiple media types with schemas
const schemasWithDataTypes = [];
for (const dataType in content) {
if (content[dataType]?.schema) {
return {
schemasWithDataTypes.push({
...content[dataType].schema,
dataType,
};
});
}
}

// If there's only one schema, return it directly
if (schemasWithDataTypes.length === 1) {
return schemasWithDataTypes[0];
}
Comment on lines +334 to +358
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

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

The logic for handling single content types is duplicated: lines 337-342 handle the case where contentTypes.length === 1, and lines 356-358 handle the case where schemasWithDataTypes.length === 1. These blocks will always produce the same result when there's only one schema. The first check (lines 337-342) should be removed since the loop and subsequent check (lines 346-358) already handle this case correctly.

Copilot uses AI. Check for mistakes.

// If there are multiple schemas with different structures, create a oneOf schema to generate a union type
if (schemasWithDataTypes.length > 1) {
// Check if all schemas are structurally the same
// If they are, just return the first one
const firstSchema = schemasWithDataTypes[0];
const allSchemasAreSame = schemasWithDataTypes.every((schema) =>
lodash.isEqual(
lodash.omit(schema, "dataType"),
lodash.omit(firstSchema, "dataType"),
Comment on lines +365 to +368
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

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

The comparison lodash.omit(schema, 'dataType') is executed repeatedly for every schema in the array, including for firstSchema which is omitted once per iteration. Consider computing lodash.omit(firstSchema, 'dataType') once before the every() call to avoid redundant operations.

Suggested change
const allSchemasAreSame = schemasWithDataTypes.every((schema) =>
lodash.isEqual(
lodash.omit(schema, "dataType"),
lodash.omit(firstSchema, "dataType"),
const firstSchemaOmitted = lodash.omit(firstSchema, "dataType");
const allSchemasAreSame = schemasWithDataTypes.every((schema) =>
lodash.isEqual(
lodash.omit(schema, "dataType"),
firstSchemaOmitted,

Copilot uses AI. Check for mistakes.
),
);

if (allSchemasAreSame) {
return firstSchema;
}

// Otherwise, create a union type
return {
oneOf: schemasWithDataTypes,
dataType: schemasWithDataTypes[0].dataType, // Use the first dataType for compatibility
};
}

return null;
};

Expand All @@ -357,24 +396,47 @@ export class SchemaRoutes {
this.schemaParserFabric.schemaUtils.getSchemaRefType(requestInfo);

if (schema) {
const content = this.schemaParserFabric.getInlineParseContent(
schema,
typeName,
[operationId],
);
const foundedSchemaByName = parsedSchemas.find(
(parsedSchema) =>
this.typeNameFormatter.format(parsedSchema.name) === content,
);
const foundSchemaByContent = parsedSchemas.find((parsedSchema) =>
lodash.isEqual(parsedSchema.content, content),
);
// If we have a oneOf schema (multiple media types), handle it specially
if (schema.oneOf) {
// Process each schema in the oneOf array
const unionTypes = schema.oneOf.map((subSchema) => {
return this.schemaParserFabric.getInlineParseContent(
subSchema,
typeName,
[operationId],
);
});

const foundSchema = foundedSchemaByName || foundSchemaByContent;
// Filter out any duplicates or Any types
const filteredTypes =
this.schemaParserFabric.schemaUtils.filterSchemaContents(
unionTypes,
(content) => content !== this.config.Ts.Keyword.Any,
);

return foundSchema
? this.typeNameFormatter.format(foundSchema.name)
: content;
// Create a union type
return this.config.Ts.UnionType(filteredTypes);
} else {
// Handle single schema as before
const content = this.schemaParserFabric.getInlineParseContent(
schema,
typeName,
[operationId],
);
const foundedSchemaByName = parsedSchemas.find(
(parsedSchema) =>
this.typeNameFormatter.format(parsedSchema.name) === content,
);
const foundSchemaByContent = parsedSchemas.find((parsedSchema) =>
lodash.isEqual(parsedSchema.content, content),
);

const foundSchema = foundedSchemaByName || foundSchemaByContent;

return foundSchema
? this.typeNameFormatter.format(foundSchema.name)
: content;
}
}

if (refTypeInfo) {
Expand Down
54 changes: 32 additions & 22 deletions tests/__snapshots__/extended.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export interface BlockFeed {
id?: string;
}

export interface ChartDataData {
export type ChartDataData = {
/** The names of the columns returned as data. */
columns?: string[];
/** The actual chart data. */
Expand All @@ -228,7 +228,7 @@ export interface ChartDataData {
name?: string;
};
parameters?: object;
}
};

export interface ChartDataParams {
/**
Expand Down Expand Up @@ -678,12 +678,12 @@ export interface GetBlockParams {
username: string;
}

export interface GetCurrentUserThrottleData {
export type GetCurrentUserThrottleData = {
/** Actions taken inside the time window. */
active_data_rate?: number;
/** Max possible actions inside the time window (usually 1 minute). */
data_rate_limit?: number;
}
};

export interface GetCurrentUserThrottleParams {
/** a valid username string */
Expand Down Expand Up @@ -8732,13 +8732,12 @@ export interface SignRequestParams {
test?: number;
}

/** JWT */
export interface SignRetrieveData {
export type SignRetrieveData = {
exp?: number;
field?: string;
/** base64safe encoded public signing key */
sub?: string;
}
};

export type SignRetrieveError = Error;

Expand Down Expand Up @@ -13323,7 +13322,9 @@ export interface ActivityListRepoNotificationsForAuthenticatedUserParams {
since?: string;
}

export type ActivityListReposStarredByAuthenticatedUserData = Repository[];
export type ActivityListReposStarredByAuthenticatedUserData =
| Repository[]
| StarredRepository[];

export interface ActivityListReposStarredByAuthenticatedUserParams {
/**
Expand Down Expand Up @@ -13366,7 +13367,9 @@ export enum ActivityListReposStarredByAuthenticatedUserParams1SortEnum {
Updated = "updated",
}

export type ActivityListReposStarredByUserData = Repository[];
export type ActivityListReposStarredByUserData =
| Repository[]
| StarredRepository[];

export interface ActivityListReposStarredByUserParams {
/**
Expand Down Expand Up @@ -13426,7 +13429,7 @@ export interface ActivityListReposWatchedByUserParams {
username: string;
}

export type ActivityListStargazersForRepoData = SimpleUser[];
export type ActivityListStargazersForRepoData = SimpleUser[] | Stargazer[];

export interface ActivityListStargazersForRepoParams {
owner: string;
Expand Down Expand Up @@ -28162,7 +28165,9 @@ export interface ReposGetCommunityProfileMetricsParams {
repo: string;
}

export type ReposGetContentData = ContentTree;
export type ReposGetContentData =
| ContentTree
| (ContentDirectory | ContentFile | ContentSymlink | ContentSubmodule);

export interface ReposGetContentParams {
owner: string;
Expand Down Expand Up @@ -60092,7 +60097,10 @@ export class Api<
data: ReposCreateForkPayload,
params: RequestParams = {},
) =>
this.request<ReposCreateForkData, BasicError | ValidationError>({
this.request<
ReposCreateForkData,
(BasicError | ScimError) | BasicError | ValidationError
>({
path: \`/repos/\${owner}/\${repo}/forks\`,
method: "POST",
body: data,
Expand Down Expand Up @@ -61604,13 +61612,15 @@ export class Api<
{ owner, repo, ...query }: ReposListCommitsParams,
params: RequestParams = {},
) =>
this.request<ReposListCommitsData, BasicError>({
path: \`/repos/\${owner}/\${repo}/commits\`,
method: "GET",
query: query,
format: "json",
...params,
}),
this.request<ReposListCommitsData, (BasicError | ScimError) | BasicError>(
{
path: \`/repos/\${owner}/\${repo}/commits\`,
method: "GET",
query: query,
format: "json",
...params,
},
),

/**
* @description Users with pull access in a repository can view commit statuses for a given ref. The ref can be a SHA, a branch name, or a tag name. Statuses are returned in reverse chronological order. The first status in the list will be the latest one. This resource is also available via a legacy route: \`GET /repos/:owner/:repo/statuses/:ref\`.
Expand Down Expand Up @@ -61729,7 +61739,7 @@ export class Api<
{ owner, repo, ...query }: ReposListForksParams,
params: RequestParams = {},
) =>
this.request<ReposListForksData, BasicError>({
this.request<ReposListForksData, BasicError | ScimError>({
path: \`/repos/\${owner}/\${repo}/forks\`,
method: "GET",
query: query,
Expand Down Expand Up @@ -62414,7 +62424,7 @@ export class Api<
) =>
this.request<
ReposUpdateInformationAboutPagesSiteData,
BasicError | ValidationError
(BasicError | ScimError) | ValidationError
>({
path: \`/repos/\${owner}/\${repo}/pages\`,
method: "PUT",
Expand Down Expand Up @@ -64787,7 +64797,7 @@ export class Api<
) =>
this.request<
ReposCreateForAuthenticatedUserData,
BasicError | ValidationError
(BasicError | ScimError) | BasicError | ValidationError
>({
path: \`/user/repos\`,
method: "POST",
Expand Down
34 changes: 23 additions & 11 deletions tests/__snapshots__/simple.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -20642,7 +20642,8 @@ export class Api<
documentation_url: string;
message: string;
}
| (ValidationError | ValidationErrorSimple)
| ValidationError
| ValidationErrorSimple
>({
path: \`/orgs/\${org}\`,
method: "PATCH",
Expand Down Expand Up @@ -24462,7 +24463,8 @@ export class Api<
this.request<
ProjectCard,
| BasicError
| (ValidationError | ValidationErrorSimple)
| ValidationError
| ValidationErrorSimple
| {
code?: string;
documentation_url?: string;
Expand Down Expand Up @@ -27970,7 +27972,7 @@ export class Api<
},
params: RequestParams = {},
) =>
this.request<Commit[], BasicError>({
this.request<Commit[], (BasicError | ScimError) | BasicError>({
path: \`/repos/\${owner}/\${repo}/commits\`,
method: "GET",
query: query,
Expand Down Expand Up @@ -28365,7 +28367,11 @@ export class Api<
},
params: RequestParams = {},
) =>
this.request<ContentTree, BasicError>({
this.request<
| ContentTree
| (ContentDirectory | ContentFile | ContentSymlink | ContentSubmodule),
BasicError
>({
path: \`/repos/\${owner}/\${repo}/contents/\${path}\`,
method: "GET",
query: query,
Expand Down Expand Up @@ -28895,7 +28901,7 @@ export class Api<
},
params: RequestParams = {},
) =>
this.request<MinimalRepository[], BasicError>({
this.request<MinimalRepository[], BasicError | ScimError>({
path: \`/repos/\${owner}/\${repo}/forks\`,
method: "GET",
query: query,
Expand All @@ -28920,7 +28926,10 @@ export class Api<
},
params: RequestParams = {},
) =>
this.request<Repository, BasicError | ValidationError>({
this.request<
Repository,
(BasicError | ScimError) | BasicError | ValidationError
>({
path: \`/repos/\${owner}/\${repo}/forks\`,
method: "POST",
body: data,
Expand Down Expand Up @@ -31601,7 +31610,7 @@ export class Api<
},
params: RequestParams = {},
) =>
this.request<void, BasicError | ValidationError>({
this.request<void, (BasicError | ScimError) | ValidationError>({
path: \`/repos/\${owner}/\${repo}/pages\`,
method: "PUT",
body: data,
Expand Down Expand Up @@ -33275,7 +33284,7 @@ export class Api<
},
params: RequestParams = {},
) =>
this.request<SimpleUser[], ValidationError>({
this.request<SimpleUser[] | Stargazer[], ValidationError>({
path: \`/repos/\${owner}/\${repo}/stargazers\`,
method: "GET",
query: query,
Expand Down Expand Up @@ -37318,7 +37327,10 @@ export class Api<
},
params: RequestParams = {},
) =>
this.request<Repository, BasicError | ValidationError>({
this.request<
Repository,
(BasicError | ScimError) | BasicError | ValidationError
>({
path: \`/user/repos\`,
method: "POST",
body: data,
Expand Down Expand Up @@ -37424,7 +37436,7 @@ export class Api<
},
params: RequestParams = {},
) =>
this.request<Repository[], BasicError>({
this.request<Repository[] | StarredRepository[], BasicError>({
path: \`/user/starred\`,
method: "GET",
query: query,
Expand Down Expand Up @@ -38191,7 +38203,7 @@ export class Api<
},
params: RequestParams = {},
) =>
this.request<Repository[], any>({
this.request<Repository[] | StarredRepository[], any>({
path: \`/users/\${username}/starred\`,
method: "GET",
query: query,
Expand Down
Loading