From 2e2cbc25f37fb1f5eb7c0c27fd1a1898bc9507dd Mon Sep 17 00:00:00 2001 From: Richard Sandoval Date: Tue, 14 Apr 2026 16:27:44 +0200 Subject: [PATCH 1/2] feat(openapi-v3): Enhance schema handling with const literals support - Introduced `SchemaObjectWithConst` type to support const values in schemas. - Added tests to verify generation of literal types for string, number, boolean, and null const values. - Updated `getType` and `getConstType` functions to handle const values correctly. - Ensured const literals are preserved in adjacent schemas during type generation. --- .../core/schemaToTypeAliasDeclaration.test.ts | 176 ++++++++++++++++++ .../src/core/schemaToTypeAliasDeclaration.ts | 38 ++++ .../generators/generateSchemaTypes.test.ts | 50 +++++ 3 files changed, 264 insertions(+) diff --git a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts index 5429320..ff34cc8 100644 --- a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts +++ b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts @@ -11,6 +11,10 @@ import { } from "./schemaToTypeAliasDeclaration"; describe("schemaToTypeAliasDeclaration", () => { + type SchemaObjectWithConst = SchemaObject & { + const?: string | number | boolean | null; + }; + it("should generate null", () => { const schema: SchemaObject = { type: "null", @@ -52,6 +56,52 @@ describe("schemaToTypeAliasDeclaration", () => { expect(printSchema(schema)).toBe("export type Test = number | null;"); }); + it("should generate string const as a literal", () => { + const schema: SchemaObjectWithConst = { + type: "string", + const: "foo", + }; + + expect(printSchema(schema)).toBe(`export type Test = "foo";`); + }); + + it("should generate number const as a literal", () => { + const schema: SchemaObjectWithConst = { + type: "integer", + const: 42, + }; + + expect(printSchema(schema)).toBe(`export type Test = 42;`); + }); + + it("should generate boolean const as a literal", () => { + const schema: SchemaObjectWithConst = { + type: "boolean", + const: true, + }; + + expect(printSchema(schema)).toBe(`export type Test = true;`); + }); + + it("should generate null const as a literal", () => { + const schema: SchemaObjectWithConst = { + type: "null", + const: null, + }; + + expect(printSchema(schema)).toBe("export type Test = null;"); + }); + + it("should preserve nullable semantics for non-null const", () => { + const schema: SchemaObjectWithConst = { + type: "string", + const: "foo", + nullable: true, + }; + + expect(printSchema(schema)).toBe(`export type Test = "foo" | null;`); + }); + it("should generate an array of numbers", () => { const schema: SchemaObject = { type: "array", @@ -682,6 +732,106 @@ describe("schemaToTypeAliasDeclaration", () => { ); }); + it("should generate a oneOf with const branches", () => { + const schema: SchemaObjectWithConst = { + oneOf: [ + { type: "string", const: "foo" } as SchemaObjectWithConst, + { type: "number", const: 42 } as SchemaObjectWithConst, + ], + }; + + expect(printSchema(schema)).toMatchInlineSnapshot( + `"export type Test = "foo" | 42;"` + ); + }); + + it("should preserve const discriminators inside object oneOf branches", () => { + const schema: SchemaObject = { + oneOf: [ + { + type: "object", + properties: { + status: { + type: "string", + const: "READY", + } as SchemaObjectWithConst, + value: { + type: "string", + }, + }, + required: ["status", "value"], + }, + { + type: "object", + properties: { + status: { + type: "string", + const: "COUNT", + } as SchemaObjectWithConst, + value: { + $ref: "#/components/schemas/CountToken", + }, + }, + required: ["status", "value"], + }, + { + type: "object", + properties: { + status: { + type: "string", + const: "RATIO", + } as SchemaObjectWithConst, + value: { + type: "number", + format: "double", + }, + }, + required: ["status", "value"], + }, + { + type: "object", + properties: { + status: { + type: "string", + const: "ENABLED", + } as SchemaObjectWithConst, + value: { + type: "boolean", + }, + }, + required: ["status", "value"], + }, + ], + }; + + expect( + printSchema(schema, "Test", "schemas", { + schemas: { + CountToken: { + type: "string", + }, + }, + }) + ).toMatchInlineSnapshot(` + "export type Test = { + status: "READY"; + value: string; + } | { + status: "COUNT"; + value: CountToken; + } | { + status: "RATIO"; + /** + * @format double + */ + value: number; + } | { + status: "ENABLED"; + value: boolean; + };" + `); + }); + describe("discrimination", () => { const schema: SchemaObject = { oneOf: [ @@ -904,6 +1054,19 @@ describe("schemaToTypeAliasDeclaration", () => { `); }); + it("should preserve const when merged with compatible keywords", () => { + const schema: SchemaObjectWithConst = { + allOf: [ + { type: "string" }, + { const: "foo" } as SchemaObjectWithConst, + ], + }; + + expect(printSchema(schema)).toMatchInlineSnapshot( + `"export type Test = "foo";"` + ); + }); + it("should combine ref and inline type", () => { const schema: SchemaObject = { allOf: [ @@ -1063,6 +1226,19 @@ describe("schemaToTypeAliasDeclaration", () => { ); }); + it("should generate a union with const branches", () => { + const schema: SchemaObjectWithConst = { + anyOf: [ + { type: "boolean", const: false } as SchemaObjectWithConst, + { type: "string", const: "foo" } as SchemaObjectWithConst, + ], + }; + + expect(printSchema(schema)).toMatchInlineSnapshot( + `"export type Test = false | "foo";"` + ); + }); + it("should combine required & properties", () => { // from github api - operationId: gists/update const schema: SchemaObject = { diff --git a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts index 36e3327..18ab7e3 100644 --- a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts +++ b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts @@ -42,6 +42,11 @@ export type Context = { currentComponent: OpenAPIComponentType | null; }; +type SupportedConstValue = string | number | boolean | null; +type SchemaObjectWithConst = SchemaObject & { + const?: SupportedConstValue; +}; + let useEnumsConfigBase: boolean | undefined; /** @@ -148,6 +153,15 @@ export const getType = ( return f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword); } + const literalConstType = getConstType(schema); + const constValue = (schema as SchemaObjectWithConst).const; + if (literalConstType) { + return withNullable( + literalConstType, + constValue !== null ? schema.nullable : false + ); + } + if (schema.oneOf) { return f.createUnionTypeNode([ ...schema.oneOf.map((i) => @@ -349,6 +363,30 @@ export const getType = ( } }; +const getConstType = ( + schema: SchemaObject +): ts.LiteralTypeNode | undefined => { + const constValue = (schema as SchemaObjectWithConst).const; + + if (typeof constValue === "string") { + return f.createLiteralTypeNode(f.createStringLiteral(constValue)); + } + + if (typeof constValue === "number") { + return f.createLiteralTypeNode(f.createNumericLiteral(constValue)); + } + + if (typeof constValue === "boolean") { + return f.createLiteralTypeNode( + constValue ? f.createTrue() : f.createFalse() + ); + } + + if (constValue === null) { + return f.createLiteralTypeNode(f.createNull()); + } +}; + /** * Add nullable option if needed. * diff --git a/plugins/typescript/src/generators/generateSchemaTypes.test.ts b/plugins/typescript/src/generators/generateSchemaTypes.test.ts index 23c1012..36d89c9 100644 --- a/plugins/typescript/src/generators/generateSchemaTypes.test.ts +++ b/plugins/typescript/src/generators/generateSchemaTypes.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from "vitest"; +import { OpenAPIObject } from "openapi3-ts/oas30"; import { petstore } from "../fixtures/petstore"; import { generateSchemaTypes } from "./generateSchemaTypes"; import { createWriteFileMock } from "../testUtils"; @@ -324,6 +325,55 @@ describe("generateSchemaTypes", () => { `); }); + it("should preserve const literals without changing adjacent schemas", async () => { + const writeFile = createWriteFileMock(); + const readFile = vi.fn(() => Promise.resolve("")); + const openAPIDocument = { + openapi: "3.0.3", + info: { + title: "Const API", + version: "1.0.0", + }, + paths: {}, + components: { + schemas: { + Status: { + type: "string", + const: "ready", + }, + Counter: { + type: "integer", + }, + }, + }, + } as OpenAPIObject; + + await generateSchemaTypes( + { + openAPIDocument, + writeFile, + readFile, + existsFile: () => true, + }, + { + filenameCase: "camel", + } + ); + + expect(writeFile.mock.calls[0][0]).toBe("constApiSchemas.ts"); + expect(writeFile.mock.calls[0][1]).toMatchInlineSnapshot(` + "/** + * Generated by @openapi-codegen + * + * @version 1.0.0 + */ + export type Status = "ready"; + + export type Counter = number; + " + `); + }); + it("should generate the responses file", async () => { const writeFile = createWriteFileMock(); const readFile = vi.fn(() => Promise.resolve("")); From 35cbcc4c2627b65dde7812ae2c4ed56c26c7c4a7 Mon Sep 17 00:00:00 2001 From: Richard Sandoval Date: Thu, 23 Apr 2026 11:06:38 +0200 Subject: [PATCH 2/2] chore: run prettier and eslint fix --- .../typescript/src/core/schemaToTypeAliasDeclaration.test.ts | 5 +---- plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts index ff34cc8..83da449 100644 --- a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts +++ b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.test.ts @@ -1056,10 +1056,7 @@ describe("schemaToTypeAliasDeclaration", () => { it("should preserve const when merged with compatible keywords", () => { const schema: SchemaObjectWithConst = { - allOf: [ - { type: "string" }, - { const: "foo" } as SchemaObjectWithConst, - ], + allOf: [{ type: "string" }, { const: "foo" } as SchemaObjectWithConst], }; expect(printSchema(schema)).toMatchInlineSnapshot( diff --git a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts index 18ab7e3..908b448 100644 --- a/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts +++ b/plugins/typescript/src/core/schemaToTypeAliasDeclaration.ts @@ -363,9 +363,7 @@ export const getType = ( } }; -const getConstType = ( - schema: SchemaObject -): ts.LiteralTypeNode | undefined => { +const getConstType = (schema: SchemaObject): ts.LiteralTypeNode | undefined => { const constValue = (schema as SchemaObjectWithConst).const; if (typeof constValue === "string") {