From be0fcaf11ff8a6b53881de225601581a90f7d0f0 Mon Sep 17 00:00:00 2001 From: "cade.sarkin" Date: Mon, 24 Aug 2026 22:10:51 +0000 Subject: [PATCH 1/2] fix(cli): convert invalid Swagger 2.0 specs in lenient mode Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/__test__/convertOpenAPIV2ToV3.test.ts | 77 +++++++++++++++++++ .../src/loaders/OpenAPILoader.ts | 2 +- .../src/utils/convertOpenAPIV2ToV3.ts | 25 +++++- 3 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 packages/cli/workspace/lazy-fern-workspace/src/__test__/convertOpenAPIV2ToV3.test.ts diff --git a/packages/cli/workspace/lazy-fern-workspace/src/__test__/convertOpenAPIV2ToV3.test.ts b/packages/cli/workspace/lazy-fern-workspace/src/__test__/convertOpenAPIV2ToV3.test.ts new file mode 100644 index 000000000000..c1c543267b55 --- /dev/null +++ b/packages/cli/workspace/lazy-fern-workspace/src/__test__/convertOpenAPIV2ToV3.test.ts @@ -0,0 +1,77 @@ +import { CliError } from "@fern-api/task-context"; +import { OpenAPIV2 } from "openapi-types"; +import { vi } from "vitest"; + +import { convertOpenAPIV2ToV3 } from "../utils/convertOpenAPIV2ToV3.js"; +import { createMockTaskContext } from "./helpers/createMockTaskContext.js"; + +function createSwaggerSpec(): OpenAPIV2.Document { + return { + swagger: "2.0", + info: { + title: "Pet Store", + version: "1.0.0" + }, + host: "example.com", + schemes: ["https"], + paths: { + "/pets": { + get: { + operationId: "listPets", + responses: { + "200": { + description: "OK" + } + } + } + } + }, + definitions: { + Pet: { + type: "object", + properties: { + name: { + type: "string" + } + } + } + } + }; +} + +describe("convertOpenAPIV2ToV3", () => { + it("converts a valid Swagger 2.0 document", async () => { + const result = await convertOpenAPIV2ToV3(createSwaggerSpec()); + + expect(result.openapi).toMatch(/^3\.0\./); + expect(result.info.title).toBe("Pet Store"); + }); + + it("converts a Swagger 2.0 document with a nullable type array in patch mode", async () => { + const spec = createSwaggerSpec(); + const property = spec.definitions?.Pet?.properties?.name; + if (property == null || Array.isArray(property) || "$ref" in property) { + throw new Error("Expected Pet.name to be a schema object"); + } + Object.assign(property, { type: ["null", "string"] }); + + const context = createMockTaskContext(); + const warn = vi.spyOn(context.logger, "warn"); + const result = await convertOpenAPIV2ToV3(spec, { context }); + + expect(result.components?.schemas?.Pet).toMatchObject({ + type: "object", + properties: { + name: { + type: "string", + nullable: true + } + } + }); + expect(warn).toHaveBeenCalledWith(expect.stringContaining("lenient (patch) mode")); + }); + + it("throws a CliError when the document cannot be converted", async () => { + await expect(convertOpenAPIV2ToV3({} as OpenAPIV2.Document)).rejects.toBeInstanceOf(CliError); + }); +}); diff --git a/packages/cli/workspace/lazy-fern-workspace/src/loaders/OpenAPILoader.ts b/packages/cli/workspace/lazy-fern-workspace/src/loaders/OpenAPILoader.ts index 71cac01469b9..1e6165def6ba 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/loaders/OpenAPILoader.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/loaders/OpenAPILoader.ts @@ -67,7 +67,7 @@ export class OpenAPILoader { if (!openAPI.schemes || openAPI.schemes.length === 0) { openAPI.schemes = ["https"]; } - const convertedOpenAPI = await convertOpenAPIV2ToV3(openAPI); + const convertedOpenAPI = await convertOpenAPIV2ToV3(openAPI, { context }); return { type: "openapi", value: convertedOpenAPI, diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/convertOpenAPIV2ToV3.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/convertOpenAPIV2ToV3.ts index edc997524d7f..4efaccff27da 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/convertOpenAPIV2ToV3.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/convertOpenAPIV2ToV3.ts @@ -1,15 +1,32 @@ -import { CliError } from "@fern-api/task-context"; +import { CliError, TaskContext } from "@fern-api/task-context"; import { OpenAPIV2, OpenAPIV3 } from "openapi-types"; import { convertObj } from "swagger2openapi"; -export async function convertOpenAPIV2ToV3(openAPI: OpenAPIV2.Document): Promise { +export async function convertOpenAPIV2ToV3( + openAPI: OpenAPIV2.Document, + options?: { context?: TaskContext } +): Promise { + let strictError: unknown; + try { const conversionResult = await convertObj(openAPI, {}); return conversionResult.openapi; - } catch (e) { + } catch (error) { + strictError = error; + } + + try { + const conversionResult = await convertObj(openAPI, { patch: true }); + options?.context?.logger.warn( + `OpenAPI v2 (Swagger) document is not strictly valid and was converted in lenient (patch) mode: ${ + strictError instanceof Error ? strictError.message : String(strictError) + }` + ); + return conversionResult.openapi; + } catch { throw new CliError({ message: `Failed to convert OpenAPI v2 (Swagger) spec to OpenAPI v3: ${ - e instanceof Error ? e.message : String(e) + strictError instanceof Error ? strictError.message : String(strictError) }`, code: CliError.Code.ParseError }); From 3b6027ba8bb0a02a10dd1accb32f8b8b136ca881 Mon Sep 17 00:00:00 2001 From: "cade.sarkin" Date: Mon, 24 Aug 2026 22:14:16 +0000 Subject: [PATCH 2/2] chore(cli): add changelog entry for lenient Swagger 2.0 conversion Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../changes/unreleased/fix-lenient-swagger2-conversion.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/cli/cli/changes/unreleased/fix-lenient-swagger2-conversion.yml diff --git a/packages/cli/cli/changes/unreleased/fix-lenient-swagger2-conversion.yml b/packages/cli/cli/changes/unreleased/fix-lenient-swagger2-conversion.yml new file mode 100644 index 000000000000..6d475beb0f95 --- /dev/null +++ b/packages/cli/cli/changes/unreleased/fix-lenient-swagger2-conversion.yml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json + +- summary: | + Retry Swagger 2.0 to OpenAPI 3 conversion in lenient mode when strict conversion fails, instead + of dropping the spec. Specs using constructs such as `type: ['null', string]` now load, with a + warning naming the strict-mode error. + type: fix