diff --git a/.changeset/nice-avocados-bake.md b/.changeset/nice-avocados-bake.md new file mode 100644 index 000000000..c49c822d2 --- /dev/null +++ b/.changeset/nice-avocados-bake.md @@ -0,0 +1,5 @@ +--- +"openapi-typescript": patch +--- + +Fix oneOf handling with empty object parent type diff --git a/packages/openapi-typescript/examples/github-api-next.ts b/packages/openapi-typescript/examples/github-api-next.ts index 99b658a3c..cd57963de 100644 --- a/packages/openapi-typescript/examples/github-api-next.ts +++ b/packages/openapi-typescript/examples/github-api-next.ts @@ -13959,7 +13959,7 @@ export interface components { * Organization ruleset conditions * @description Conditions for an organization ruleset */ - "org-ruleset-conditions": Record & ((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"])); + "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"]); /** * creation * @description Only allow users with bypass permission to create matching refs. @@ -14177,7 +14177,7 @@ export interface components { * Repository Rule * @description A repository rule. */ - "repository-rule": Record & (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"]); + "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"]; /** * Repository ruleset * @description A set of rules to apply when specified conditions are met. @@ -19126,7 +19126,7 @@ export interface components { * Repository Rule * @description A repository rule with ruleset details. */ - "repository-rule-detailed": Record & ((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"])); + "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"]); "secret-scanning-alert": { number?: components["schemas"]["alert-number"]; created_at?: components["schemas"]["alert-created-at"]; diff --git a/packages/openapi-typescript/examples/github-api.ts b/packages/openapi-typescript/examples/github-api.ts index 1b7d74c21..01cae6ab9 100644 --- a/packages/openapi-typescript/examples/github-api.ts +++ b/packages/openapi-typescript/examples/github-api.ts @@ -13156,7 +13156,7 @@ export interface components { * Organization ruleset conditions * @description Conditions for an organization ruleset */ - "org-ruleset-conditions": Record & ((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"])); + "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"]); /** * creation * @description Only allow users with bypass permission to create matching refs. @@ -13374,7 +13374,7 @@ export interface components { * Repository Rule * @description A repository rule. */ - "repository-rule": Record & (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"]); + "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"]; /** * Repository ruleset * @description A set of rules to apply when specified conditions are met. @@ -20981,7 +20981,7 @@ export interface components { * Repository Rule * @description A repository rule with ruleset details. */ - "repository-rule-detailed": Record & ((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"])); + "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"]); "secret-scanning-alert": { number?: components["schemas"]["alert-number"]; created_at?: components["schemas"]["alert-created-at"]; diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 1664d6b99..c05e77f15 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -71,7 +71,7 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere } // oneOf (no discriminator) - 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 + const oneOf = ((typeof schemaObject === "object" && !schemaObject.discriminator && schemaObject.oneOf) || schemaObject.enum || undefined) as (SchemaObject | ReferenceObject)[] | undefined; // note: for objects, treat enum as oneOf if (oneOf && !oneOf.some((t) => "$ref" in t && ctx.discriminators[t.$ref])) { const oneOfNormalized = oneOf.map((item) => transformSchemaObject(item, { path, ctx })); if (schemaObject.nullable) oneOfNormalized.push("null"); @@ -86,7 +86,7 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere const oneOfTypes = oneOfNormalized.some((t) => typeof t === "string" && t.includes("{")) ? tsOneOf(...oneOfNormalized) : tsUnionOf(...oneOfNormalized); // handle oneOf + object type - if ("type" in schemaObject && schemaObject.type === "object") { + if ("type" in schemaObject && schemaObject.type === "object" && (schemaObject.properties || schemaObject.additionalProperties)) { return tsIntersectionOf(transformSchemaObject({ ...schemaObject, oneOf: undefined, enum: undefined } as any, { path, ctx }), oneOfTypes); } @@ -226,19 +226,19 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere return output; } // oneOf (discriminator) - if ("oneOf" in schemaObject && Array.isArray(schemaObject.oneOf)) { + if (Array.isArray(schemaObject.oneOf) && schemaObject.oneOf.length) { const oneOfType = tsUnionOf(...collectCompositions(schemaObject.oneOf)); finalType = finalType ? tsIntersectionOf(finalType, oneOfType) : oneOfType; } else { // allOf - if ("allOf" in schemaObject && Array.isArray(schemaObject.allOf)) { - finalType = tsIntersectionOf(...(finalType ? [finalType] : []), ...collectCompositions(schemaObject.allOf)); + if (Array.isArray((schemaObject as any).allOf) && schemaObject.allOf!.length) { + finalType = tsIntersectionOf(...(finalType ? [finalType] : []), ...collectCompositions(schemaObject.allOf!)); if ("required" in schemaObject && Array.isArray(schemaObject.required)) { finalType = tsWithRequired(finalType, schemaObject.required); } } // anyOf - if ("anyOf" in schemaObject && Array.isArray(schemaObject.anyOf)) { + if (Array.isArray(schemaObject.anyOf) && schemaObject.anyOf.length) { const anyOfTypes = tsUnionOf(...collectCompositions(schemaObject.anyOf)); finalType = finalType ? tsIntersectionOf(finalType, anyOfTypes) : anyOfTypes; } diff --git a/packages/openapi-typescript/src/types.ts b/packages/openapi-typescript/src/types.ts index cd416d463..721596d33 100644 --- a/packages/openapi-typescript/src/types.ts +++ b/packages/openapi-typescript/src/types.ts @@ -432,9 +432,12 @@ export type SchemaObject = { format?: string; /** @deprecated in 3.1 (still valid for 3.0) */ nullable?: boolean; + oneOf?: (SchemaObject | ReferenceObject)[]; + allOf?: (SchemaObject | ReferenceObject)[]; + anyOf?: (SchemaObject | ReferenceObject)[]; + required?: string[]; [key: `x-${string}`]: any; } & ( - | { oneOf: (SchemaObject | ReferenceObject)[] } | StringSubtype | NumberSubtype | IntegerSubtype @@ -443,8 +446,6 @@ export type SchemaObject = { | NullSubtype | ObjectSubtype | { type: ("string" | "number" | "integer" | "array" | "boolean" | "null" | "object")[] } - | { allOf: (SchemaObject | ReferenceObject)[]; anyOf?: (SchemaObject | ReferenceObject)[]; required?: string[] } - | { allOf?: (SchemaObject | ReferenceObject)[]; anyOf: (SchemaObject | ReferenceObject)[]; required?: string[] } // eslint-disable-next-line @typescript-eslint/ban-types | {} ); diff --git a/packages/openapi-typescript/test/schema-object.test.ts b/packages/openapi-typescript/test/schema-object.test.ts index 32edfca2f..0753d5f84 100644 --- a/packages/openapi-typescript/test/schema-object.test.ts +++ b/packages/openapi-typescript/test/schema-object.test.ts @@ -374,6 +374,48 @@ describe("Schema Object", () => { expect(generated).toBe("0 | 1"); }); + test("empty object + oneOf is ignored", () => { + const schema: SchemaObject = { + type: "object", + oneOf: [ + { + title: "DetailsId", + type: "object", + required: ["id"], + properties: { + id: { type: "string", description: "The ID of an existing resource that exists before the pipeline is run." }, + }, + }, + { + title: "DetailsFrom", + type: "object", + required: ["from"], + properties: { + from: { + type: "object", + description: "The stage and step to report on.", + required: ["step"], + 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." } }, + }, + }, + }, + ], + }; + const generated = transformSchemaObject(schema, options); + expect(generated).toBe(`OneOf<[{ + /** @description The ID of an existing resource that exists before the pipeline is run. */ + id: string; +}, { + /** @description The stage and step to report on. */ + from: { + /** @description An identifier for the stage the step being reported on resides in. */ + stage?: string; + /** @description An identifier for the step to be reported on. */ + step: string; + }; +}]>`); + }); + test("nullable: true", () => { const schema: SchemaObject = { nullable: true, oneOf: [{ type: "integer" }, { type: "string" }] }; const generated = transformSchemaObject(schema, options); @@ -719,8 +761,8 @@ describe("ReferenceObject", () => { properties: { string: { type: "string" } }, "x-extension": true, }, - options - ) + options, + ), ).toBe("{\n string?: string;\n}"); }); });