Skip to content

Commit 9713c96

Browse files
authored
fix: handle type: "null" in anyOf / oneOf / allOf (#252)
previously `EmptyObject` was erroneously being added to the resulting type / schema, which would cause a build error in the case of a header parameter that used this pattern.
1 parent 9c09191 commit 9713c96

File tree

7 files changed

+78
-5
lines changed

7 files changed

+78
-5
lines changed

packages/openapi-code-generator/src/core/input.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,12 @@ function normalizeSchemaObject(
426426
}
427427

428428
const properties = normalizeProperties(schemaObject.properties)
429+
430+
const hasNull =
431+
hasATypeNull(schemaObject.allOf) ||
432+
hasATypeNull(schemaObject.oneOf) ||
433+
hasATypeNull(schemaObject.anyOf)
434+
429435
const allOf = normalizeAllOf(schemaObject.allOf)
430436
const oneOf = normalizeOneOf(schemaObject.oneOf)
431437
const anyOf = normalizeAnyOf(schemaObject.anyOf)
@@ -447,7 +453,7 @@ function normalizeSchemaObject(
447453
return {
448454
...base,
449455
// TODO: HACK
450-
nullable: base.nullable || schemaObject.type === "null",
456+
nullable: base.nullable || schemaObject.type === "null" || hasNull,
451457
type: "object",
452458
allOf,
453459
oneOf,
@@ -554,11 +560,15 @@ function normalizeSchemaObject(
554560
}
555561

556562
function normalizeAllOf(allOf: Schema["allOf"] = []): MaybeIRModel[] {
557-
return allOf.map(normalizeSchemaObject)
563+
return allOf
564+
?.filter((it) => isRef(it) || it.type !== "null")
565+
.map(normalizeSchemaObject)
558566
}
559567

560568
function normalizeOneOf(oneOf: Schema["oneOf"] = []): MaybeIRModel[] {
561-
return oneOf.map(normalizeSchemaObject)
569+
return oneOf
570+
.filter((it) => isRef(it) || it.type !== "null")
571+
.map(normalizeSchemaObject)
562572
}
563573

564574
function normalizeAnyOf(anyOf: Schema["anyOf"] = []): MaybeIRModel[] {
@@ -568,8 +578,16 @@ function normalizeSchemaObject(
568578
// anyOf: [ {required: ["bla"]}, {required: ["foo"]} ] in addition to top-level schema, which looks like
569579
// it's intended to indicate that at least one of the objects properties must be set (consider it a
570580
// Omit<Partial<Thing>, EmptyObject> )
571-
return isRef(it) || Boolean(it.type)
581+
return isRef(it) || (Boolean(it.type) && it.type !== "null")
572582
})
573583
.map(normalizeSchemaObject)
574584
}
585+
586+
function hasATypeNull(arr?: (Schema | Reference)[]) {
587+
return Boolean(
588+
arr?.find((it) => {
589+
return !isRef(it) && it.type === "null"
590+
}),
591+
)
592+
}
575593
}

packages/openapi-code-generator/src/core/openapi-types-normalized.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {xInternalPreproccess} from "./openapi-types"
21
import type {HttpMethod} from "./utils"
32

43
export interface IRRef {
@@ -85,13 +84,18 @@ export interface IRModelArray extends IRModelBase {
8584
// TODO: contains / maxContains / minContains
8685
}
8786

87+
export interface IRModelNull extends IRModelBase {
88+
type: "null"
89+
}
90+
8891
export type IRModel =
8992
| IRModelNumeric
9093
| IRModelString
9194
| IRModelBoolean
9295
| IRModelObject
9396
| IRModelArray
9497
| IRModelAny
98+
| IRModelNull
9599

96100
export type MaybeIRModel = IRModel | IRRef
97101

packages/openapi-code-generator/src/test/unit-test-inputs-3.0.3.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ components:
101101
anyOf:
102102
- type: number
103103
- type: string
104+
AnyOfNullableString:
105+
anyOf:
106+
- type: string
107+
- type: "null"
104108

105109
AllOf:
106110
allOf:

packages/openapi-code-generator/src/test/unit-test-inputs-3.1.0.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ components:
105105
type:
106106
- number
107107
- string
108+
AnyOfNullableString:
109+
anyOf:
110+
- type: string
111+
- type: "null"
108112

109113
AllOf:
110114
allOf:

packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ export abstract class AbstractSchemaBuilder<
320320
result = this.config.allowAny ? this.any() : this.unknown()
321321
break
322322
}
323+
case "null":
324+
throw new Error("unreachable - input should normalize this out")
323325
}
324326

325327
if (model["x-internal-preprocess"]) {

packages/openapi-code-generator/src/typescript/common/schema-builders/zod-schema-builder.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,31 @@ describe.each(testVersions)(
539539
)
540540
})
541541

542+
it("supports nullable string using allOf", async () => {
543+
const {code, execute} = await getActualFromModel({
544+
type: "object",
545+
anyOf: [
546+
{type: "string", nullable: false, readOnly: false},
547+
{type: "null", nullable: false, readOnly: false},
548+
],
549+
allOf: [],
550+
oneOf: [],
551+
properties: {},
552+
additionalProperties: undefined,
553+
required: [],
554+
nullable: false,
555+
readOnly: false,
556+
})
557+
558+
expect(code).toMatchInlineSnapshot('"const x = z.string().nullable()"')
559+
560+
await expect(execute("a string")).resolves.toBe("a string")
561+
await expect(execute(null)).resolves.toBe(null)
562+
await expect(execute(123)).rejects.toThrow(
563+
"Expected string, received number",
564+
)
565+
})
566+
542567
it("supports minLength", async () => {
543568
const {code, execute} = await getActualFromModel({
544569
...base,

packages/openapi-code-generator/src/typescript/common/type-builder.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,22 @@ describe.each(testVersions)(
129129
)
130130
})
131131

132+
it("can build a type for a nullable string using anyOf correctly", async () => {
133+
const {code, types} = await getActual(
134+
"components/schemas/AnyOfNullableString",
135+
)
136+
137+
expect(code).toMatchInlineSnapshot(`
138+
"import {t_AnyOfNullableString} from './unit-test.types'
139+
140+
const x: t_AnyOfNullableString"
141+
`)
142+
143+
expect(types).toMatchInlineSnapshot(
144+
'"export type t_AnyOfNullableString = string | null"',
145+
)
146+
})
147+
132148
it("can build a type for a allOf correctly", async () => {
133149
const {code, types} = await getActual("components/schemas/AllOf")
134150

0 commit comments

Comments
 (0)