From a5e5f24d06ae4422fcce1a60cd0645c965bceb6d Mon Sep 17 00:00:00 2001 From: Effect Bot Date: Fri, 13 Nov 2026 07:52:06 +0000 Subject: [PATCH 1/5] test(openapi-generator): reproduce missing type-only Schema import --- .../test/OpenApiGenerator.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts b/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts index 4410d32be71..c0f02912851 100644 --- a/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts +++ b/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts @@ -3858,6 +3858,41 @@ export const withoutLocale = client.getGreeting({ params: {} }) } ])) + itGeneratedClientsCompile("emits compilable type-only clients with implicit additional properties", { + openapi: "3.1.0", + info: { title: "Open object API", version: "1.0.0" }, + paths: { + "/users": { + get: { + operationId: "getUser", + parameters: [], + responses: { + "200": { + description: "OK", + content: { + "application/json": { schema: { $ref: "#/components/schemas/User" } } + } + } + }, + tags: ["Users"], + security: [] + } + } + }, + components: { + schemas: { + User: { + type: "object", + properties: { Name: { type: "string" } }, + required: ["Name"] + } + }, + securitySchemes: {} + }, + security: [], + tags: [] + }, { formats: ["httpclient-type-only"] }) + itGeneratedClientsCompile("emits compilable open objects with optional properties", { openapi: "3.1.0", info: { From 5a17e0ff7c2744c7dfbba4ae2d1493629b2c4269 Mon Sep 17 00:00:00 2001 From: Effect Bot Date: Fri, 13 Nov 2026 07:53:42 +0000 Subject: [PATCH 2/5] fix(openapi-generator): import Schema in type-only clients --- .changeset/fix-openapi-type-only-schema-import.md | 5 +++++ packages/tools/openapi-generator/src/OpenApiTransformer.ts | 5 +++-- .../tools/openapi-generator/test/OpenApiGenerator.test.ts | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-openapi-type-only-schema-import.md diff --git a/.changeset/fix-openapi-type-only-schema-import.md b/.changeset/fix-openapi-type-only-schema-import.md new file mode 100644 index 00000000000..db68e671eaf --- /dev/null +++ b/.changeset/fix-openapi-type-only-schema-import.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Fix missing Schema imports in generated type-only HTTP clients with open object schemas. diff --git a/packages/tools/openapi-generator/src/OpenApiTransformer.ts b/packages/tools/openapi-generator/src/OpenApiTransformer.ts index 290b3833c15..5fde1d49cce 100644 --- a/packages/tools/openapi-generator/src/OpenApiTransformer.ts +++ b/packages/tools/openapi-generator/src/OpenApiTransformer.ts @@ -859,12 +859,13 @@ export const make = ( } return OpenApiTransformer.of({ - imports: (_importName, parsed) => { + imports: (importName, parsed) => { const operations = parsed.operations const requirements = computeImportRequirements(operations) const imports = [ `import * as Data from "effect/Data"`, - `import * as Effect from "effect/Effect"` + `import * as Effect from "effect/Effect"`, + `import type * as ${importName} from "effect/Schema"` ] if (requiresStreaming(requirements)) { imports.push( diff --git a/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts b/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts index c0f02912851..63ed7c2785b 100644 --- a/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts +++ b/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts @@ -1401,6 +1401,7 @@ export const TestClientError = ( }, `import * as Data from "effect/Data" import * as Effect from "effect/Effect" +import type * as Schema from "effect/Schema" import type * as HttpClient from "effect/http/HttpClient" import * as HttpClientError from "effect/http/HttpClientError" import * as HttpClientRequest from "effect/http/HttpClientRequest" From 65896e32594b19e19cb234252c22b38eac459619 Mon Sep 17 00:00:00 2001 From: Effect Bot Date: Fri, 13 Nov 2026 08:02:01 +0000 Subject: [PATCH 3/5] fix(openapi-generator): emit type-only Schema import only when referenced --- .../tools/openapi-generator/src/JsonSchemaGenerator.ts | 7 +++++-- .../tools/openapi-generator/src/OpenApiTransformer.ts | 5 ++--- .../openapi-generator/test/OpenApiGenerator.test.ts | 9 ++++++--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts b/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts index 2f2bbf05819..7d56dbe2913 100644 --- a/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts +++ b/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts @@ -128,11 +128,14 @@ function makeWithRepresentation() { renderSchemaTypeAndRuntime(name, generated.codeDocument.codes[i], typeOnly, options) ) - return renderImportArtifacts(generated.codeDocument, !typeOnly) + - render("recursive declarations", recursiveDeclarations) + + const declarations = render("recursive declarations", recursiveDeclarations) + render("non-recursive definitions", nonRecursives) + render("recursive definitions", recursives) + render("schemas", codes) + const schemaImport = typeOnly && declarations.includes("Schema.") + ? "import type * as Schema from \"effect/Schema\"\n" + : "" + return renderImportArtifacts(generated.codeDocument, !typeOnly) + schemaImport + declarations } function generateHttpApi( diff --git a/packages/tools/openapi-generator/src/OpenApiTransformer.ts b/packages/tools/openapi-generator/src/OpenApiTransformer.ts index 5fde1d49cce..290b3833c15 100644 --- a/packages/tools/openapi-generator/src/OpenApiTransformer.ts +++ b/packages/tools/openapi-generator/src/OpenApiTransformer.ts @@ -859,13 +859,12 @@ export const make = ( } return OpenApiTransformer.of({ - imports: (importName, parsed) => { + imports: (_importName, parsed) => { const operations = parsed.operations const requirements = computeImportRequirements(operations) const imports = [ `import * as Data from "effect/Data"`, - `import * as Effect from "effect/Effect"`, - `import type * as ${importName} from "effect/Schema"` + `import * as Effect from "effect/Effect"` ] if (requiresStreaming(requirements)) { imports.push( diff --git a/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts b/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts index 63ed7c2785b..df8b831d084 100644 --- a/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts +++ b/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts @@ -39,6 +39,8 @@ const assertTypeOnly = Effect.fnUntraced(function*(spec: OpenAPISpec, expected: // console.log(result) assert.strictEqual(result, expected) + const failures = yield* typeCheckCases([{ sources: [result], exactOptionalPropertyTypes: true }], true) + assert.deepStrictEqual(failures, [[]]) }, Effect.provide(OpenApiGenerator.layerTransformerTs)) const assertRuntimeIncludes = Effect.fnUntraced(function*( @@ -148,7 +150,8 @@ const parseDiagnostics = (output: string) => { // which cases are clean; each of its cases is then checked on its own. const typeCheckCases = Effect.fnUntraced( function*( - cases: ReadonlyArray<{ readonly sources: ReadonlyArray; readonly exactOptionalPropertyTypes: boolean }> + cases: ReadonlyArray<{ readonly sources: ReadonlyArray; readonly exactOptionalPropertyTypes: boolean }>, + noUnusedLocals = false ) { const directory = mkdtempSync(join(dirname(fileURLToPath(import.meta.url)), ".generated-clients-")) return yield* Effect.gen(function*() { @@ -171,6 +174,7 @@ const typeCheckCases = Effect.fnUntraced( lib: ["ESNext", "DOM"], module: "NodeNext", noEmit: true, + noUnusedLocals, skipLibCheck: true, strict: true, target: "ES2022", @@ -1401,7 +1405,6 @@ export const TestClientError = ( }, `import * as Data from "effect/Data" import * as Effect from "effect/Effect" -import type * as Schema from "effect/Schema" import type * as HttpClient from "effect/http/HttpClient" import * as HttpClientError from "effect/http/HttpClientError" import * as HttpClientRequest from "effect/http/HttpClientRequest" @@ -1575,7 +1578,7 @@ export const TestClientError = ( response, request: response.request, }) as any` - )) + ), compilationTimeout) it.effect("preserves response variants and routes binary and bodiless statuses", () => assertTypeOnlyIncludes(responseMatchingSpec, [ From fdbf04c09db3ed14e9b16e03f5352a7eab8fa2bf Mon Sep 17 00:00:00 2001 From: Effect Bot Date: Fri, 13 Nov 2026 08:06:30 +0000 Subject: [PATCH 4/5] fix(openapi-generator): ignore literals when detecting Schema references --- .../openapi-generator/src/JsonSchemaGenerator.ts | 15 ++++++++++++++- .../test/OpenApiGenerator.test.ts | 11 ++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts b/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts index 7d56dbe2913..62d78b4ecf4 100644 --- a/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts +++ b/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts @@ -132,7 +132,7 @@ function makeWithRepresentation() { render("non-recursive definitions", nonRecursives) + render("recursive definitions", recursives) + render("schemas", codes) - const schemaImport = typeOnly && declarations.includes("Schema.") + const schemaImport = typeOnly && referencesSchema(declarations) ? "import type * as Schema from \"effect/Schema\"\n" : "" return renderImportArtifacts(generated.codeDocument, !typeOnly) + schemaImport + declarations @@ -346,6 +346,19 @@ function render(title: string, as: ReadonlyArray) { return "// " + title + "\n" + as.join("\n") + "\n" } +// Ignore quoted property names and literal types when checking generated dependencies. +const typeTokenPattern = + /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\x60(?:\\.|[^\x60\\])*\x60|\/\/[^\n]*|\/\*[\s\S]*?\*\/|[A-Za-z_$][\w$]*|[^\s]/g + +function referencesSchema(source: string): boolean { + let previous = "" + for (const [token] of source.matchAll(typeTokenPattern)) { + if (previous === "Schema" && token === ".") return true + previous = token + } + return false +} + function renderImportArtifacts(codeDocument: SchemaRepresentation.CodeDocument, enabled: boolean): string { if (!enabled) return "" const imports = codeDocument.artifacts.flatMap((artifact) => diff --git a/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts b/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts index df8b831d084..26d85e343e7 100644 --- a/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts +++ b/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts @@ -38,9 +38,9 @@ const assertTypeOnly = Effect.fnUntraced(function*(spec: OpenAPISpec, expected: }) // console.log(result) - assert.strictEqual(result, expected) const failures = yield* typeCheckCases([{ sources: [result], exactOptionalPropertyTypes: true }], true) assert.deepStrictEqual(failures, [[]]) + assert.strictEqual(result, expected) }, Effect.provide(OpenApiGenerator.layerTransformerTs)) const assertRuntimeIncludes = Effect.fnUntraced(function*( @@ -1380,11 +1380,12 @@ export const TestClientError = ( id: { type: "string" }, - name: { - type: "string" + "Schema.property": { + type: "string", + enum: ["Schema.Json"] } }, - required: ["id", "name"], + required: ["id", "Schema.property"], additionalProperties: false } } @@ -1410,7 +1411,7 @@ import * as HttpClientError from "effect/http/HttpClientError" import * as HttpClientRequest from "effect/http/HttpClientRequest" import * as HttpClientResponse from "effect/http/HttpClientResponse" // schemas -export type GetUser200 = { readonly "id": string, readonly "name": string } +export type GetUser200 = { readonly "id": string, readonly "Schema.property": "Schema.Json" } export interface OperationConfig { /** From 674f7938bf810f6354553eb9c6cfc66aef2868bc Mon Sep 17 00:00:00 2001 From: Effect Bot Date: Fri, 13 Nov 2026 08:13:23 +0000 Subject: [PATCH 5/5] refactor(openapi-generator): simplify Schema reference detection --- .../openapi-generator/src/JsonSchemaGenerator.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts b/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts index 62d78b4ecf4..febf93181e3 100644 --- a/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts +++ b/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts @@ -346,17 +346,9 @@ function render(title: string, as: ReadonlyArray) { return "// " + title + "\n" + as.join("\n") + "\n" } -// Ignore quoted property names and literal types when checking generated dependencies. -const typeTokenPattern = - /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\x60(?:\\.|[^\x60\\])*\x60|\/\/[^\n]*|\/\*[\s\S]*?\*\/|[A-Za-z_$][\w$]*|[^\s]/g - -function referencesSchema(source: string): boolean { - let previous = "" - for (const [token] of source.matchAll(typeTokenPattern)) { - if (previous === "Schema" && token === ".") return true - previous = token - } - return false +// Quoted property names and literal types can contain "Schema." without referencing the namespace. +function referencesSchema(declarations: string): boolean { + return /\bSchema\./.test(declarations.replace(/"(?:\\.|[^"\\])*"/g, "")) } function renderImportArtifacts(codeDocument: SchemaRepresentation.CodeDocument, enabled: boolean): string {