Skip to content

Commit 2720165

Browse files
fix incorrect types generated when refs reference types with discriminators
1 parent d93755b commit 2720165

File tree

3 files changed

+112
-49
lines changed

3 files changed

+112
-49
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,8 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
203203
// Mapping value can either be a fully-qualified ref (#/components/schemas/XYZ) or a schema name (XYZ)
204204
const matchedValue = Object.entries(discriminator.mapping).find(([, v]) => (!v.startsWith("#") && v === value) || (v.startsWith("#") && parseRef(v).path.pop() === value));
205205
if (matchedValue) value = matchedValue[0]; // why was this designed backwards!?
206+
coreType.unshift(indent(`${escObjKey(discriminator.propertyName)}: ${escStr(value)};`, indentLv + 1));
206207
}
207-
coreType.unshift(indent(`${escObjKey(discriminator.propertyName)}: ${escStr(value)};`, indentLv + 1));
208208
break;
209209
}
210210
}
@@ -217,7 +217,7 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
217217
const output: string[] = [];
218218
for (const item of items) {
219219
const itemType = transformSchemaObject(item, { path, ctx: { ...ctx, indentLv } });
220-
if ("$ref" in item && ctx.discriminators[item.$ref]) {
220+
if ("$ref" in item && ctx.discriminators[item.$ref]?.mapping) {
221221
output.push(tsOmit(itemType, [ctx.discriminators[item.$ref].propertyName]));
222222
continue;
223223
}

packages/openapi-typescript/test/index.test.ts

+90-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from "node:fs";
2-
import type { OpenAPI3 } from "../src/types.js";
32
import openapiTS from "../dist/index.js";
3+
import type { OpenAPI3 } from "../src/types.js";
44

55
const BOILERPLATE = `/**
66
* This file was auto-generated by openapi-typescript.
@@ -457,7 +457,7 @@ export interface external {
457457
458458
export type operations = Record<string, never>;
459459
`);
460-
})
460+
});
461461
});
462462

463463
describe("3.1", () => {
@@ -546,7 +546,7 @@ export type operations = Record<string, never>;
546546
`);
547547
});
548548

549-
test("discriminator (oneOf)", async () => {
549+
test("discriminator with explicit mapping (oneOf)", async () => {
550550
const schema: OpenAPI3 = {
551551
openapi: "3.1",
552552
info: { title: "test", version: "1.0" },
@@ -625,6 +625,93 @@ export interface components {
625625
626626
export type external = Record<string, never>;
627627
628+
export type operations = Record<string, never>;
629+
`);
630+
});
631+
632+
test("discriminator with implicit mapping (oneOf)", async () => {
633+
const schema: OpenAPI3 = {
634+
openapi: "3.1",
635+
info: { title: "test", version: "1.0" },
636+
components: {
637+
schemas: {
638+
Pet: {
639+
oneOf: [{ $ref: "#/components/schemas/Cat" }, { $ref: "#/components/schemas/Dog" }, { $ref: "#/components/schemas/Lizard" }],
640+
discriminator: {
641+
propertyName: "petType",
642+
},
643+
} as any,
644+
Cat: {
645+
type: "object",
646+
properties: {
647+
name: { type: "string" },
648+
petType: { type: "string", enum: ["cat"] },
649+
},
650+
required: ["petType"],
651+
},
652+
Dog: {
653+
type: "object",
654+
properties: {
655+
bark: { type: "string" },
656+
petType: { type: "string", enum: ["dog"] },
657+
},
658+
required: ["petType"],
659+
},
660+
Lizard: {
661+
type: "object",
662+
properties: {
663+
lovesRocks: { type: "boolean" },
664+
petType: { type: "string", enum: ["lizard"] },
665+
},
666+
required: ["petType"],
667+
},
668+
Person: {
669+
type: "object",
670+
required: ["pet"],
671+
properties: {
672+
pet: { oneOf: [{ $ref: "#/components/schemas/Pet" }] },
673+
},
674+
},
675+
},
676+
},
677+
};
678+
const generated = await openapiTS(schema);
679+
expect(generated).toBe(`${BOILERPLATE}
680+
export type paths = Record<string, never>;
681+
682+
export type webhooks = Record<string, never>;
683+
684+
export interface components {
685+
schemas: {
686+
Pet: components["schemas"]["Cat"] | components["schemas"]["Dog"] | components["schemas"]["Lizard"];
687+
Cat: {
688+
name?: string;
689+
/** @enum {string} */
690+
petType: "cat";
691+
};
692+
Dog: {
693+
bark?: string;
694+
/** @enum {string} */
695+
petType: "dog";
696+
};
697+
Lizard: {
698+
lovesRocks?: boolean;
699+
/** @enum {string} */
700+
petType: "lizard";
701+
};
702+
Person: {
703+
pet: components["schemas"]["Pet"];
704+
};
705+
};
706+
responses: never;
707+
parameters: never;
708+
requestBodies: never;
709+
headers: never;
710+
pathItems: never;
711+
}
712+
713+
export type external = Record<string, never>;
714+
628715
export type operations = Record<string, never>;
629716
`);
630717
});

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

+20-44
Original file line numberDiff line numberDiff line change
@@ -374,48 +374,6 @@ 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-
419377
test("nullable: true", () => {
420378
const schema: SchemaObject = { nullable: true, oneOf: [{ type: "integer" }, { type: "string" }] };
421379
const generated = transformSchemaObject(schema, options);
@@ -538,6 +496,24 @@ describe("Schema Object", () => {
538496
}`);
539497
});
540498

499+
test("discriminator with oneOf and null", () => {
500+
const schema: SchemaObject = {
501+
oneOf: [{ $ref: 'components["schemas"]["parent"]' }, { type: "null" }],
502+
};
503+
const generated = transformSchemaObject(schema, {
504+
path: options.path,
505+
ctx: {
506+
...options.ctx,
507+
discriminators: {
508+
'components["schemas"]["parent"]': {
509+
propertyName: "operation",
510+
},
511+
},
512+
},
513+
});
514+
expect(generated).toBe(`components["schemas"]["parent"] | null`);
515+
});
516+
541517
test("discriminator escape", () => {
542518
const schema: SchemaObject = {
543519
type: "object",
@@ -761,8 +737,8 @@ describe("ReferenceObject", () => {
761737
properties: { string: { type: "string" } },
762738
"x-extension": true,
763739
},
764-
options,
765-
),
740+
options
741+
)
766742
).toBe("{\n string?: string;\n}");
767743
});
768744
});

0 commit comments

Comments
 (0)