From 1e7069d40034fb5546003d80ae0e2197023de2c2 Mon Sep 17 00:00:00 2001 From: Lucas Romero Date: Sun, 23 Nov 2025 13:39:00 +0100 Subject: [PATCH 1/9] Add default test case --- .../transform/parameters-with-default.test.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 packages/openapi-typescript/test/transform/parameters-with-default.test.ts diff --git a/packages/openapi-typescript/test/transform/parameters-with-default.test.ts b/packages/openapi-typescript/test/transform/parameters-with-default.test.ts new file mode 100644 index 000000000..62681f195 --- /dev/null +++ b/packages/openapi-typescript/test/transform/parameters-with-default.test.ts @@ -0,0 +1,64 @@ +import { fileURLToPath } from "node:url"; +import { astToString } from "../../src/lib/ts.js"; +import transformPathsObject from "../../src/transform/paths-object.js"; +import type { GlobalContext } from "../../src/types.js"; +import { DEFAULT_CTX, type TestCase } from "../test-helpers.js"; + +const DEFAULT_OPTIONS = DEFAULT_CTX; + +describe("parametersWithDefaults", () => { + const tests: TestCase[] = [ + [ + "options > default", + { + given: { + "/api/{version}/user/{user_id}": { + parameters: [ + { in: "query", name: "no_default", required: false, schema: { type: "number" } }, + { in: "query", name: "with_default", required: false, schema: { type: "number", default: 1337 } }, + ], + }, + }, + want: `{ + "/api/{version}/user/{user_id}": { + parameters: { + query?: { + no_default?: number; + with_default?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +}`, + options: { + ...DEFAULT_OPTIONS, + }, + }, + ], + ]; + + for (const [testName, { given, want, options = DEFAULT_OPTIONS, ci }] of tests) { + test.skipIf(ci?.skipIf)( + testName, + async () => { + const result = astToString(transformPathsObject(given, options)); + if (want instanceof URL) { + await expect(result).toMatchFileSnapshot(fileURLToPath(want)); + } else { + expect(result).toBe(`${want}\n`); + } + }, + ci?.timeout, + ); + } +}); From 40c2e9edf6e6fda85dbc5919910de0461e7d4d3b Mon Sep 17 00:00:00 2001 From: Lucas Romero Date: Sun, 23 Nov 2025 13:47:33 +0100 Subject: [PATCH 2/9] Add new option with expectation as new test --- packages/openapi-typescript/src/index.ts | 1 + packages/openapi-typescript/src/types.ts | 5 +++ .../fixtures/parameters-test-default.yaml | 19 ++++++++++ .../openapi-typescript/test/test-helpers.ts | 1 + .../transform/parameters-with-default.test.ts | 38 +++++++++++++++++++ 5 files changed, 64 insertions(+) create mode 100644 packages/openapi-typescript/test/fixtures/parameters-test-default.yaml diff --git a/packages/openapi-typescript/src/index.ts b/packages/openapi-typescript/src/index.ts index dabd4e81b..36f4efba2 100644 --- a/packages/openapi-typescript/src/index.ts +++ b/packages/openapi-typescript/src/index.ts @@ -95,6 +95,7 @@ export default async function openapiTS( resolve($ref) { return resolveRef(schema, $ref, { silent: options.silent ?? false }); }, + makeParametersWithDefaultNotUndefined: options.makeParametersWithDefaultNotUndefined ?? false, }; const transformT = performance.now(); diff --git a/packages/openapi-typescript/src/types.ts b/packages/openapi-typescript/src/types.ts index 0020bc03e..a9275a330 100644 --- a/packages/openapi-typescript/src/types.ts +++ b/packages/openapi-typescript/src/types.ts @@ -679,6 +679,10 @@ export interface OpenAPITSOptions { makePathsEnum?: boolean; /** Generate path params based on path even if they are not defiend in the open api schema */ generatePathParams?: boolean; + /** Generate non-optional parameter types if the parameter has a default. This is useful + * for server implementations that automatically set the default from the spec for the user. + */ + makeParametersWithDefaultNotUndefined?: boolean; } /** Context passed to all submodules */ @@ -714,6 +718,7 @@ export interface GlobalContext { inject?: string; makePathsEnum: boolean; generatePathParams: boolean; + makeParametersWithDefaultNotUndefined: boolean; } export type $defs = Record; diff --git a/packages/openapi-typescript/test/fixtures/parameters-test-default.yaml b/packages/openapi-typescript/test/fixtures/parameters-test-default.yaml new file mode 100644 index 000000000..938818e77 --- /dev/null +++ b/packages/openapi-typescript/test/fixtures/parameters-test-default.yaml @@ -0,0 +1,19 @@ +openapi: "3.0" +info: + title: test + version: "1.0" +paths: + /endpoint: + get: + description: OK + parameters: + - in: query + name: optional_nodefault + schema: + type: string + required: false + - in: query + name: optional_default + schema: + type: string + required: false diff --git a/packages/openapi-typescript/test/test-helpers.ts b/packages/openapi-typescript/test/test-helpers.ts index 13d46568a..f0c2a13d0 100644 --- a/packages/openapi-typescript/test/test-helpers.ts +++ b/packages/openapi-typescript/test/test-helpers.ts @@ -34,6 +34,7 @@ export const DEFAULT_CTX: GlobalContext = { transformProperty: undefined, makePathsEnum: false, generatePathParams: false, + makeParametersWithDefaultNotUndefined: false, }; /** Generic test case */ diff --git a/packages/openapi-typescript/test/transform/parameters-with-default.test.ts b/packages/openapi-typescript/test/transform/parameters-with-default.test.ts index 62681f195..5c1dafa15 100644 --- a/packages/openapi-typescript/test/transform/parameters-with-default.test.ts +++ b/packages/openapi-typescript/test/transform/parameters-with-default.test.ts @@ -45,6 +45,44 @@ describe("parametersWithDefaults", () => { }, }, ], + [ + "options > makeParametersWithDefaultNotUndefined: true", + { + given: { + "/api/{version}/user/{user_id}": { + parameters: [ + { in: "query", name: "no_default", required: false, schema: { type: "number" } }, + { in: "query", name: "with_default", required: false, schema: { type: "number", default: 1337 } }, + ], + }, + }, + want: `{ + "/api/{version}/user/{user_id}": { + parameters: { + query: { + no_default?: number; + with_default: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +}`, + options: { + ...DEFAULT_OPTIONS, + makeParametersWithDefaultNotUndefined: true, + }, + }, + ], ]; for (const [testName, { given, want, options = DEFAULT_OPTIONS, ci }] of tests) { From 3f0607e4c23030b41b1b9b9151841dab5a1cc64d Mon Sep 17 00:00:00 2001 From: Lucas Romero Date: Sun, 23 Nov 2025 14:02:39 +0100 Subject: [PATCH 3/9] Make parameters with default value non-optional if respective option is set --- .../openapi-typescript/src/transform/parameters-array.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/openapi-typescript/src/transform/parameters-array.ts b/packages/openapi-typescript/src/transform/parameters-array.ts index 1ecdff4d7..b7c27ab39 100644 --- a/packages/openapi-typescript/src/transform/parameters-array.ts +++ b/packages/openapi-typescript/src/transform/parameters-array.ts @@ -87,7 +87,10 @@ export function transformParametersArray( continue; } let optional: ts.QuestionToken | undefined = undefined; - if (paramIn !== "path" && !(resolved as ParameterObject).required) { + const isNonOptional = + (resolved as ParameterObject).required || + (options.ctx.makeParametersWithDefaultNotUndefined && resolved.schema?.default !== undefined); + if (paramIn !== "path" && !isNonOptional) { optional = QUESTION_TOKEN; } const subType = From 4e370c6fe310c572a919819e35136a41438fa0e1 Mon Sep 17 00:00:00 2001 From: Lucas Romero Date: Sun, 23 Nov 2025 14:28:20 +0100 Subject: [PATCH 4/9] Fix urls --- packages/openapi-typescript/test/node-api.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/openapi-typescript/test/node-api.test.ts b/packages/openapi-typescript/test/node-api.test.ts index 51390097e..a7ea7cf75 100644 --- a/packages/openapi-typescript/test/node-api.test.ts +++ b/packages/openapi-typescript/test/node-api.test.ts @@ -58,7 +58,7 @@ export type operations = Record;`, [ "input > string > URL", { - given: "https://raw.githubusercontent.com/Redocly/redocly-cli/main/__tests__/lint/oas3.1/openapi.yaml", + given: "https://raw.githubusercontent.com/Redocly/redocly-cli/main/tests/e2e/lint/oas3.1/openapi.yaml", want: new URL("simple-example.ts", EXAMPLES_DIR), // options: DEFAULT_OPTIONS, }, @@ -66,7 +66,7 @@ export type operations = Record;`, [ "input > URL > remote", { - given: new URL("https://raw.githubusercontent.com/Redocly/redocly-cli/main/__tests__/lint/oas3.1/openapi.yaml"), + given: new URL("https://raw.githubusercontent.com/Redocly/redocly-cli/main/tests/e2e/lint/oas3.1/openapi.yaml"), want: new URL("simple-example.ts", EXAMPLES_DIR), // options: DEFAULT_OPTIONS, }, From 84294f87a0f16b1fd5b19f6f3f75ed858e9b0092 Mon Sep 17 00:00:00 2001 From: Lucas Romero Date: Sun, 23 Nov 2025 14:40:47 +0100 Subject: [PATCH 5/9] Fix typo that was fixed upstream --- packages/openapi-typescript/examples/simple-example.ts | 2 +- packages/openapi-typescript/examples/simple-example.yaml | 2 +- .../test/fixtures/redocly-flag/openapi/a.yaml | 2 +- .../test/fixtures/redocly-flag/openapi/b.yaml | 2 +- .../test/fixtures/redocly-flag/openapi/c.yaml | 2 +- .../openapi-typescript/test/fixtures/redocly/openapi/a.yaml | 2 +- .../openapi-typescript/test/fixtures/redocly/openapi/b.yaml | 2 +- .../openapi-typescript/test/fixtures/redocly/openapi/c.yaml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/openapi-typescript/examples/simple-example.ts b/packages/openapi-typescript/examples/simple-example.ts index 88f9c2041..e98bfea5f 100644 --- a/packages/openapi-typescript/examples/simple-example.ts +++ b/packages/openapi-typescript/examples/simple-example.ts @@ -112,7 +112,7 @@ export interface paths { path?: never; cookie?: never; }; - /** Checks if unevaluetedProperties work */ + /** Checks if unevaluatedProperties work */ get: operations["getUnevaluatedProperties"]; put?: never; post?: never; diff --git a/packages/openapi-typescript/examples/simple-example.yaml b/packages/openapi-typescript/examples/simple-example.yaml index f6323a406..b5d309a88 100644 --- a/packages/openapi-typescript/examples/simple-example.yaml +++ b/packages/openapi-typescript/examples/simple-example.yaml @@ -109,7 +109,7 @@ paths: /unevaluated-properties: get: operationId: getUnevaluatedProperties - summary: Checks if unevaluetedProperties work + summary: Checks if unevaluatedProperties work responses: 200: description: 'OK' diff --git a/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/a.yaml b/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/a.yaml index f6323a406..b5d309a88 100644 --- a/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/a.yaml +++ b/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/a.yaml @@ -109,7 +109,7 @@ paths: /unevaluated-properties: get: operationId: getUnevaluatedProperties - summary: Checks if unevaluetedProperties work + summary: Checks if unevaluatedProperties work responses: 200: description: 'OK' diff --git a/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/b.yaml b/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/b.yaml index f6323a406..b5d309a88 100644 --- a/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/b.yaml +++ b/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/b.yaml @@ -109,7 +109,7 @@ paths: /unevaluated-properties: get: operationId: getUnevaluatedProperties - summary: Checks if unevaluetedProperties work + summary: Checks if unevaluatedProperties work responses: 200: description: 'OK' diff --git a/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/c.yaml b/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/c.yaml index f6323a406..b5d309a88 100644 --- a/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/c.yaml +++ b/packages/openapi-typescript/test/fixtures/redocly-flag/openapi/c.yaml @@ -109,7 +109,7 @@ paths: /unevaluated-properties: get: operationId: getUnevaluatedProperties - summary: Checks if unevaluetedProperties work + summary: Checks if unevaluatedProperties work responses: 200: description: 'OK' diff --git a/packages/openapi-typescript/test/fixtures/redocly/openapi/a.yaml b/packages/openapi-typescript/test/fixtures/redocly/openapi/a.yaml index f6323a406..b5d309a88 100644 --- a/packages/openapi-typescript/test/fixtures/redocly/openapi/a.yaml +++ b/packages/openapi-typescript/test/fixtures/redocly/openapi/a.yaml @@ -109,7 +109,7 @@ paths: /unevaluated-properties: get: operationId: getUnevaluatedProperties - summary: Checks if unevaluetedProperties work + summary: Checks if unevaluatedProperties work responses: 200: description: 'OK' diff --git a/packages/openapi-typescript/test/fixtures/redocly/openapi/b.yaml b/packages/openapi-typescript/test/fixtures/redocly/openapi/b.yaml index f6323a406..b5d309a88 100644 --- a/packages/openapi-typescript/test/fixtures/redocly/openapi/b.yaml +++ b/packages/openapi-typescript/test/fixtures/redocly/openapi/b.yaml @@ -109,7 +109,7 @@ paths: /unevaluated-properties: get: operationId: getUnevaluatedProperties - summary: Checks if unevaluetedProperties work + summary: Checks if unevaluatedProperties work responses: 200: description: 'OK' diff --git a/packages/openapi-typescript/test/fixtures/redocly/openapi/c.yaml b/packages/openapi-typescript/test/fixtures/redocly/openapi/c.yaml index f6323a406..b5d309a88 100644 --- a/packages/openapi-typescript/test/fixtures/redocly/openapi/c.yaml +++ b/packages/openapi-typescript/test/fixtures/redocly/openapi/c.yaml @@ -109,7 +109,7 @@ paths: /unevaluated-properties: get: operationId: getUnevaluatedProperties - summary: Checks if unevaluetedProperties work + summary: Checks if unevaluatedProperties work responses: 200: description: 'OK' From 0f11d02dcb471223172bdb70876049b4bcff2261 Mon Sep 17 00:00:00 2001 From: Lucas Romero Date: Sun, 23 Nov 2025 15:40:06 +0100 Subject: [PATCH 6/9] Add CLI support for new option --- docs/cli.md | 9 +++++++++ packages/openapi-typescript/bin/cli.js | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/docs/cli.md b/docs/cli.md index fb41bd941..da64c86a1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -234,3 +234,12 @@ export enum ApiPaths { This option is useful for generating path params optimistically when the schema has flaky path parameter definitions. Checks the path for opening and closing brackets and extracts them as path parameters. Does not override already defined by schema path parameters. + +### makeParametersWithDefaultNotUndefined + +Enabling `--make-parameters-with-default-not-undefined` will result in generated types where operation parameters that +have default values defined are non-optional. + +This allows generating types for certain server implementation use cases where the default value is injected +automatically by the framework for the handling function to consume and the signature on the handler should +indicate that the value can not be undefined. diff --git a/packages/openapi-typescript/bin/cli.js b/packages/openapi-typescript/bin/cli.js index c3a589dc5..d2b78953b 100755 --- a/packages/openapi-typescript/bin/cli.js +++ b/packages/openapi-typescript/bin/cli.js @@ -34,6 +34,8 @@ Options --root-types-no-schema-prefix (optional) Do not add "Schema" prefix to types at the root level (should only be used with --root-types) --make-paths-enum Generate ApiPaths enum for all paths + --make-parameters-with-default-not-undefined + Generate non-optional parameter types if the parameter has a default `; const OUTPUT_FILE = "FILE"; @@ -86,6 +88,7 @@ const flags = parser(args, { "rootTypesNoSchemaPrefix", "makePathsEnum", "generatePathParams", + "makeParametersWithDefaultNotUndefined", ], string: ["output", "redocly"], alias: { @@ -149,6 +152,7 @@ async function generateSchema(schema, { redocly, silent = false }) { rootTypesNoSchemaPrefix: flags.rootTypesNoSchemaPrefix, makePathsEnum: flags.makePathsEnum, generatePathParams: flags.generatePathParams, + makeParametersWithDefaultNotUndefined: flags.makeParametersWithDefaultNotUndefined, redocly, silent, }), From 5fba88e71074debd8aa8b5d0252f75d93280567f Mon Sep 17 00:00:00 2001 From: Lucas Romero Date: Sun, 23 Nov 2025 16:49:54 +0100 Subject: [PATCH 7/9] Add changeset --- .changeset/dirty-apes-itch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dirty-apes-itch.md diff --git a/.changeset/dirty-apes-itch.md b/.changeset/dirty-apes-itch.md new file mode 100644 index 000000000..4a2bb16c6 --- /dev/null +++ b/.changeset/dirty-apes-itch.md @@ -0,0 +1,5 @@ +--- +"openapi-typescript": minor +--- + +Add option to handle parameters with default as non-optional From 4e6b066c6057cfea2ff64402a943bd52060a0c95 Mon Sep 17 00:00:00 2001 From: Lucas Romero Date: Sun, 23 Nov 2025 17:41:20 +0100 Subject: [PATCH 8/9] Add test cases where property schema is ref --- .../transform/parameters-with-default.test.ts | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/openapi-typescript/test/transform/parameters-with-default.test.ts b/packages/openapi-typescript/test/transform/parameters-with-default.test.ts index 5c1dafa15..2550f9874 100644 --- a/packages/openapi-typescript/test/transform/parameters-with-default.test.ts +++ b/packages/openapi-typescript/test/transform/parameters-with-default.test.ts @@ -16,6 +16,18 @@ describe("parametersWithDefaults", () => { parameters: [ { in: "query", name: "no_default", required: false, schema: { type: "number" } }, { in: "query", name: "with_default", required: false, schema: { type: "number", default: 1337 } }, + { + in: "query", + name: "ref_without_default", + required: false, + schema: { $ref: "#/components/schemas/NoDefault" }, + }, + { + in: "query", + name: "ref_with_default", + required: false, + schema: { $ref: "#/components/schemas/WithDefault" }, + }, ], }, }, @@ -25,6 +37,8 @@ describe("parametersWithDefaults", () => { query?: { no_default?: number; with_default?: number; + ref_without_default?: components["schemas"]["NoDefault"]; + ref_with_default?: components["schemas"]["WithDefault"]; }; header?: never; path?: never; @@ -53,6 +67,18 @@ describe("parametersWithDefaults", () => { parameters: [ { in: "query", name: "no_default", required: false, schema: { type: "number" } }, { in: "query", name: "with_default", required: false, schema: { type: "number", default: 1337 } }, + { + in: "query", + name: "ref_without_default", + required: false, + schema: { $ref: "#/components/schemas/NoDefault" }, + }, + { + in: "query", + name: "ref_with_default", + required: false, + schema: { $ref: "#/components/schemas/WithDefault" }, + }, ], }, }, @@ -62,6 +88,8 @@ describe("parametersWithDefaults", () => { query: { no_default?: number; with_default: number; + ref_without_default?: components["schemas"]["NoDefault"]; + ref_with_default: components["schemas"]["WithDefault"]; }; header?: never; path?: never; @@ -89,7 +117,29 @@ describe("parametersWithDefaults", () => { test.skipIf(ci?.skipIf)( testName, async () => { - const result = astToString(transformPathsObject(given, options)); + const result = astToString( + transformPathsObject(given, { + ...options, + resolve($ref) { + switch ($ref) { + case "#/components/schemas/NoDefault": { + return { + type: "number", + }; + } + case "#/components/schemas/WithDefault": { + return { + type: "number", + default: 1338, + }; + } + default: { + return undefined as any; + } + } + }, + }), + ); if (want instanceof URL) { await expect(result).toMatchFileSnapshot(fileURLToPath(want)); } else { From 5d314f481f56ebc3577820f5f79f3fce33575a55 Mon Sep 17 00:00:00 2001 From: Lucas Romero Date: Sun, 23 Nov 2025 17:42:11 +0100 Subject: [PATCH 9/9] Support looking up default value from $ref'ed schema --- .../openapi-typescript/src/transform/parameters-array.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/openapi-typescript/src/transform/parameters-array.ts b/packages/openapi-typescript/src/transform/parameters-array.ts index b7c27ab39..6cd3798fd 100644 --- a/packages/openapi-typescript/src/transform/parameters-array.ts +++ b/packages/openapi-typescript/src/transform/parameters-array.ts @@ -1,7 +1,7 @@ import ts from "typescript"; import { addJSDocComment, NEVER, oapiRef, QUESTION_TOKEN, tsModifiers, tsPropertyIndex } from "../lib/ts.js"; import { createRef } from "../lib/utils.js"; -import type { ParameterObject, ReferenceObject, TransformNodeOptions } from "../types.js"; +import type { ParameterObject, ReferenceObject, SchemaObject, TransformNodeOptions } from "../types.js"; import transformParameterObject from "./parameter-object.js"; // Regex to match path parameters in URL @@ -86,10 +86,14 @@ export function transformParametersArray( if (resolved?.in !== paramIn) { continue; } + const resolvedSchema = + resolved.schema && "$ref" in resolved.schema + ? options.ctx.resolve(resolved.schema.$ref as string) + : resolved.schema; let optional: ts.QuestionToken | undefined = undefined; const isNonOptional = (resolved as ParameterObject).required || - (options.ctx.makeParametersWithDefaultNotUndefined && resolved.schema?.default !== undefined); + (options.ctx.makeParametersWithDefaultNotUndefined && resolvedSchema?.default !== undefined); if (paramIn !== "path" && !isNonOptional) { optional = QUESTION_TOKEN; }