Skip to content

Commit 0e7ca25

Browse files
committed
Fix oneOf + empty object parent case
1 parent dc6cbf9 commit 0e7ca25

File tree

6 files changed

+65
-17
lines changed

6 files changed

+65
-17
lines changed

.changeset/nice-avocados-bake.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": patch
3+
---
4+
5+
Fix oneOf handling with empty object parent type

packages/openapi-typescript/examples/github-api-next.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -13959,7 +13959,7 @@ export interface components {
1395913959
* Organization ruleset conditions
1396013960
* @description Conditions for an organization ruleset
1396113961
*/
13962-
"org-ruleset-conditions": Record<string, never> & ((components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-name-target"]) | (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-id-target"]));
13962+
"org-ruleset-conditions": (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-name-target"]) | (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-id-target"]);
1396313963
/**
1396413964
* creation
1396513965
* @description Only allow users with bypass permission to create matching refs.
@@ -14177,7 +14177,7 @@ export interface components {
1417714177
* Repository Rule
1417814178
* @description A repository rule.
1417914179
*/
14180-
"repository-rule": Record<string, never> & (components["schemas"]["repository-rule-creation"] | components["schemas"]["repository-rule-update"] | components["schemas"]["repository-rule-deletion"] | components["schemas"]["repository-rule-required-linear-history"] | components["schemas"]["repository-rule-required-deployments"] | components["schemas"]["repository-rule-required-signatures"] | components["schemas"]["repository-rule-pull-request"] | components["schemas"]["repository-rule-required-status-checks"] | components["schemas"]["repository-rule-non-fast-forward"] | components["schemas"]["repository-rule-commit-message-pattern"] | components["schemas"]["repository-rule-commit-author-email-pattern"] | components["schemas"]["repository-rule-committer-email-pattern"] | components["schemas"]["repository-rule-branch-name-pattern"] | components["schemas"]["repository-rule-tag-name-pattern"]);
14180+
"repository-rule": components["schemas"]["repository-rule-creation"] | components["schemas"]["repository-rule-update"] | components["schemas"]["repository-rule-deletion"] | components["schemas"]["repository-rule-required-linear-history"] | components["schemas"]["repository-rule-required-deployments"] | components["schemas"]["repository-rule-required-signatures"] | components["schemas"]["repository-rule-pull-request"] | components["schemas"]["repository-rule-required-status-checks"] | components["schemas"]["repository-rule-non-fast-forward"] | components["schemas"]["repository-rule-commit-message-pattern"] | components["schemas"]["repository-rule-commit-author-email-pattern"] | components["schemas"]["repository-rule-committer-email-pattern"] | components["schemas"]["repository-rule-branch-name-pattern"] | components["schemas"]["repository-rule-tag-name-pattern"];
1418114181
/**
1418214182
* Repository ruleset
1418314183
* @description A set of rules to apply when specified conditions are met.
@@ -19126,7 +19126,7 @@ export interface components {
1912619126
* Repository Rule
1912719127
* @description A repository rule with ruleset details.
1912819128
*/
19129-
"repository-rule-detailed": Record<string, never> & ((components["schemas"]["repository-rule-creation"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-update"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-deletion"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-linear-history"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-deployments"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-signatures"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-pull-request"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-status-checks"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-non-fast-forward"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-message-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-author-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-committer-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-branch-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-tag-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]));
19129+
"repository-rule-detailed": (components["schemas"]["repository-rule-creation"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-update"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-deletion"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-linear-history"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-deployments"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-signatures"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-pull-request"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-status-checks"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-non-fast-forward"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-message-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-author-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-committer-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-branch-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-tag-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]);
1913019130
"secret-scanning-alert": {
1913119131
number?: components["schemas"]["alert-number"];
1913219132
created_at?: components["schemas"]["alert-created-at"];

packages/openapi-typescript/examples/github-api.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -13156,7 +13156,7 @@ export interface components {
1315613156
* Organization ruleset conditions
1315713157
* @description Conditions for an organization ruleset
1315813158
*/
13159-
"org-ruleset-conditions": Record<string, never> & ((components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-name-target"]) | (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-id-target"]));
13159+
"org-ruleset-conditions": (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-name-target"]) | (components["schemas"]["repository-ruleset-conditions"] & components["schemas"]["repository-ruleset-conditions-repository-id-target"]);
1316013160
/**
1316113161
* creation
1316213162
* @description Only allow users with bypass permission to create matching refs.
@@ -13374,7 +13374,7 @@ export interface components {
1337413374
* Repository Rule
1337513375
* @description A repository rule.
1337613376
*/
13377-
"repository-rule": Record<string, never> & (components["schemas"]["repository-rule-creation"] | components["schemas"]["repository-rule-update"] | components["schemas"]["repository-rule-deletion"] | components["schemas"]["repository-rule-required-linear-history"] | components["schemas"]["repository-rule-required-deployments"] | components["schemas"]["repository-rule-required-signatures"] | components["schemas"]["repository-rule-pull-request"] | components["schemas"]["repository-rule-required-status-checks"] | components["schemas"]["repository-rule-non-fast-forward"] | components["schemas"]["repository-rule-commit-message-pattern"] | components["schemas"]["repository-rule-commit-author-email-pattern"] | components["schemas"]["repository-rule-committer-email-pattern"] | components["schemas"]["repository-rule-branch-name-pattern"] | components["schemas"]["repository-rule-tag-name-pattern"]);
13377+
"repository-rule": components["schemas"]["repository-rule-creation"] | components["schemas"]["repository-rule-update"] | components["schemas"]["repository-rule-deletion"] | components["schemas"]["repository-rule-required-linear-history"] | components["schemas"]["repository-rule-required-deployments"] | components["schemas"]["repository-rule-required-signatures"] | components["schemas"]["repository-rule-pull-request"] | components["schemas"]["repository-rule-required-status-checks"] | components["schemas"]["repository-rule-non-fast-forward"] | components["schemas"]["repository-rule-commit-message-pattern"] | components["schemas"]["repository-rule-commit-author-email-pattern"] | components["schemas"]["repository-rule-committer-email-pattern"] | components["schemas"]["repository-rule-branch-name-pattern"] | components["schemas"]["repository-rule-tag-name-pattern"];
1337813378
/**
1337913379
* Repository ruleset
1338013380
* @description A set of rules to apply when specified conditions are met.
@@ -20981,7 +20981,7 @@ export interface components {
2098120981
* Repository Rule
2098220982
* @description A repository rule with ruleset details.
2098320983
*/
20984-
"repository-rule-detailed": Record<string, never> & ((components["schemas"]["repository-rule-creation"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-update"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-deletion"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-linear-history"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-deployments"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-signatures"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-pull-request"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-status-checks"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-non-fast-forward"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-message-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-author-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-committer-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-branch-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-tag-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]));
20984+
"repository-rule-detailed": (components["schemas"]["repository-rule-creation"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-update"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-deletion"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-linear-history"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-deployments"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-signatures"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-pull-request"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-required-status-checks"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-non-fast-forward"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-message-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-commit-author-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-committer-email-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-branch-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]) | (components["schemas"]["repository-rule-tag-name-pattern"] & components["schemas"]["repository-rule-ruleset-info"]);
2098520985
"secret-scanning-alert": {
2098620986
number?: components["schemas"]["alert-number"];
2098720987
created_at?: components["schemas"]["alert-created-at"];

packages/openapi-typescript/src/transform/schema-object.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
7171
}
7272

7373
// oneOf (no discriminator)
74-
const oneOf = ((typeof schemaObject === "object" && !schemaObject.discriminator && (schemaObject as any).oneOf) || schemaObject.enum || undefined) as (SchemaObject | ReferenceObject)[] | undefined; // note: for objects, treat enum as oneOf
74+
const oneOf = ((typeof schemaObject === "object" && !schemaObject.discriminator && schemaObject.oneOf) || schemaObject.enum || undefined) as (SchemaObject | ReferenceObject)[] | undefined; // note: for objects, treat enum as oneOf
7575
if (oneOf && !oneOf.some((t) => "$ref" in t && ctx.discriminators[t.$ref])) {
7676
const oneOfNormalized = oneOf.map((item) => transformSchemaObject(item, { path, ctx }));
7777
if (schemaObject.nullable) oneOfNormalized.push("null");
@@ -86,7 +86,7 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
8686
const oneOfTypes = oneOfNormalized.some((t) => typeof t === "string" && t.includes("{")) ? tsOneOf(...oneOfNormalized) : tsUnionOf(...oneOfNormalized);
8787

8888
// handle oneOf + object type
89-
if ("type" in schemaObject && schemaObject.type === "object") {
89+
if ("type" in schemaObject && schemaObject.type === "object" && (schemaObject.properties || schemaObject.additionalProperties)) {
9090
return tsIntersectionOf(transformSchemaObject({ ...schemaObject, oneOf: undefined, enum: undefined } as any, { path, ctx }), oneOfTypes);
9191
}
9292

@@ -226,19 +226,19 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
226226
return output;
227227
}
228228
// oneOf (discriminator)
229-
if ("oneOf" in schemaObject && Array.isArray(schemaObject.oneOf)) {
229+
if (Array.isArray(schemaObject.oneOf) && schemaObject.oneOf.length) {
230230
const oneOfType = tsUnionOf(...collectCompositions(schemaObject.oneOf));
231231
finalType = finalType ? tsIntersectionOf(finalType, oneOfType) : oneOfType;
232232
} else {
233233
// allOf
234-
if ("allOf" in schemaObject && Array.isArray(schemaObject.allOf)) {
235-
finalType = tsIntersectionOf(...(finalType ? [finalType] : []), ...collectCompositions(schemaObject.allOf));
234+
if (Array.isArray((schemaObject as any).allOf) && schemaObject.allOf!.length) {
235+
finalType = tsIntersectionOf(...(finalType ? [finalType] : []), ...collectCompositions(schemaObject.allOf!));
236236
if ("required" in schemaObject && Array.isArray(schemaObject.required)) {
237237
finalType = tsWithRequired(finalType, schemaObject.required);
238238
}
239239
}
240240
// anyOf
241-
if ("anyOf" in schemaObject && Array.isArray(schemaObject.anyOf)) {
241+
if (Array.isArray(schemaObject.anyOf) && schemaObject.anyOf.length) {
242242
const anyOfTypes = tsUnionOf(...collectCompositions(schemaObject.anyOf));
243243
finalType = finalType ? tsIntersectionOf(finalType, anyOfTypes) : anyOfTypes;
244244
}

packages/openapi-typescript/src/types.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -432,9 +432,12 @@ export type SchemaObject = {
432432
format?: string;
433433
/** @deprecated in 3.1 (still valid for 3.0) */
434434
nullable?: boolean;
435+
oneOf?: (SchemaObject | ReferenceObject)[];
436+
allOf?: (SchemaObject | ReferenceObject)[];
437+
anyOf?: (SchemaObject | ReferenceObject)[];
438+
required?: string[];
435439
[key: `x-${string}`]: any;
436440
} & (
437-
| { oneOf: (SchemaObject | ReferenceObject)[] }
438441
| StringSubtype
439442
| NumberSubtype
440443
| IntegerSubtype
@@ -443,8 +446,6 @@ export type SchemaObject = {
443446
| NullSubtype
444447
| ObjectSubtype
445448
| { type: ("string" | "number" | "integer" | "array" | "boolean" | "null" | "object")[] }
446-
| { allOf: (SchemaObject | ReferenceObject)[]; anyOf?: (SchemaObject | ReferenceObject)[]; required?: string[] }
447-
| { allOf?: (SchemaObject | ReferenceObject)[]; anyOf: (SchemaObject | ReferenceObject)[]; required?: string[] }
448449
// eslint-disable-next-line @typescript-eslint/ban-types
449450
| {}
450451
);

packages/openapi-typescript/test/schema-object.test.ts

+44-2
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,48 @@ describe("Schema Object", () => {
374374
expect(generated).toBe("0 | 1");
375375
});
376376

377+
test("empty object + oneOf is ignored", () => {
378+
const schema: SchemaObject = {
379+
type: "object",
380+
oneOf: [
381+
{
382+
title: "DetailsId",
383+
type: "object",
384+
required: ["id"],
385+
properties: {
386+
id: { type: "string", description: "The ID of an existing resource that exists before the pipeline is run." },
387+
},
388+
},
389+
{
390+
title: "DetailsFrom",
391+
type: "object",
392+
required: ["from"],
393+
properties: {
394+
from: {
395+
type: "object",
396+
description: "The stage and step to report on.",
397+
required: ["step"],
398+
properties: { stage: { type: "string", description: "An identifier for the stage the step being reported on resides in." }, step: { type: "string", description: "An identifier for the step to be reported on." } },
399+
},
400+
},
401+
},
402+
],
403+
};
404+
const generated = transformSchemaObject(schema, options);
405+
expect(generated).toBe(`OneOf<[{
406+
/** @description The ID of an existing resource that exists before the pipeline is run. */
407+
id: string;
408+
}, {
409+
/** @description The stage and step to report on. */
410+
from: {
411+
/** @description An identifier for the stage the step being reported on resides in. */
412+
stage?: string;
413+
/** @description An identifier for the step to be reported on. */
414+
step: string;
415+
};
416+
}]>`);
417+
});
418+
377419
test("nullable: true", () => {
378420
const schema: SchemaObject = { nullable: true, oneOf: [{ type: "integer" }, { type: "string" }] };
379421
const generated = transformSchemaObject(schema, options);
@@ -719,8 +761,8 @@ describe("ReferenceObject", () => {
719761
properties: { string: { type: "string" } },
720762
"x-extension": true,
721763
},
722-
options
723-
)
764+
options,
765+
),
724766
).toBe("{\n string?: string;\n}");
725767
});
726768
});

0 commit comments

Comments
 (0)