From 7dbcdf86fe4fcba285d753b320effd4bc3eccf18 Mon Sep 17 00:00:00 2001 From: coyaSONG <66289470+coyaSONG@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:15:14 +0900 Subject: [PATCH 1/2] fix(swift): generate final classes --- .../src/language/Swift/SwiftRenderer.ts | 15 ++- test/unit/swift-final-classes.test.ts | 93 +++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 test/unit/swift-final-classes.test.ts diff --git a/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts index bd1ebe3d1e..0ba9401461 100644 --- a/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts +++ b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts @@ -500,6 +500,7 @@ export class SwiftRenderer extends ConvenienceRenderer { const isClass = this._options.useClasses || this.isCycleBreakerType(c); const structOrClass = isClass ? "class" : "struct"; + const finalPrefix = isClass ? "final " : ""; if (isClass && this._options.objcSupport) { // [Michael Fey (@MrRooni), 2019-4-24] Swift 5 or greater, must come before the access declaration for the class. @@ -508,6 +509,7 @@ export class SwiftRenderer extends ConvenienceRenderer { this.emitBlockWithAccess( [ + finalPrefix, structOrClass, " ", className, @@ -1163,12 +1165,12 @@ encoder.dateEncodingStrategy = .formatted(formatter)`); this.emitLine( this.objcMembersDeclaration, this.accessLevel, - "class JSONNull: NSObject, Codable {", + "final class JSONNull: NSObject, Codable {", ); } else { this.emitLine( this.accessLevel, - "class JSONNull: Codable, Hashable {", + "final class JSONNull: Codable, Hashable {", ); } @@ -1214,7 +1216,7 @@ encoder.dateEncodingStrategy = .formatted(formatter)`); if (this._needAny) { this.ensureBlankLine(); - this.emitMultiline(`class JSONCodingKey: CodingKey { + this.emitMultiline(`final class JSONCodingKey: CodingKey { let key: String required init?(intValue: Int) { @@ -1239,10 +1241,13 @@ encoder.dateEncodingStrategy = .formatted(formatter)`); this.emitLine( this.objcMembersDeclaration, this.accessLevel, - "class JSONAny: NSObject, Codable {", + "final class JSONAny: NSObject, Codable {", ); } else { - this.emitLine(this.accessLevel, "class JSONAny: Codable {"); + this.emitLine( + this.accessLevel, + "final class JSONAny: Codable {", + ); } this.ensureBlankLine(); diff --git a/test/unit/swift-final-classes.test.ts b/test/unit/swift-final-classes.test.ts new file mode 100644 index 0000000000..bcdfd7c5a2 --- /dev/null +++ b/test/unit/swift-final-classes.test.ts @@ -0,0 +1,93 @@ +import { + InputData, + JSONSchemaInput, + type RendererOptions, + quicktype, +} from "../../packages/quicktype-core/src/index.js"; +import { describe, expect, test } from "vitest"; + +async function renderSwift( + schema: object, + rendererOptions: RendererOptions = {}, +): Promise { + const schemaInput = new JSONSchemaInput(undefined); + await schemaInput.addSource({ + name: "TopLevel", + schema: JSON.stringify(schema), + }); + + const inputData = new InputData(); + inputData.addInput(schemaInput); + + const result = await quicktype({ + inputData, + lang: "swift", + rendererOptions, + }); + return result.lines.join("\n"); +} + +function classDeclarations(output: string): string[] { + return output + .split("\n") + .filter((line) => + /^\s*(?:(?:public|internal)\s+)?(?:final\s+)?class\s+/.test(line), + ); +} + +describe("Swift class generation", () => { + test("marks requested model classes as final", async () => { + const output = await renderSwift( + { + type: "object", + properties: { + child: { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }, + }, + required: ["child"], + }, + { "struct-or-class": "class" }, + ); + const declarations = classDeclarations(output); + + expect(declarations).toHaveLength(2); + expect(declarations).toEqual( + expect.arrayContaining([ + expect.stringContaining("final class TopLevel"), + expect.stringContaining("final class Child"), + ]), + ); + }); + + test("marks cycle breakers and Codable helper classes as final", async () => { + const output = await renderSwift({ + $ref: "#/definitions/Node", + definitions: { + Node: { + type: "object", + properties: { + next: { $ref: "#/definitions/Node" }, + value: {}, + }, + required: ["value"], + }, + }, + }); + const declarations = classDeclarations(output); + + expect(declarations).toEqual( + expect.arrayContaining([ + expect.stringContaining("final class TopLevel"), + expect.stringContaining("final class JSONNull"), + expect.stringContaining("final class JSONCodingKey"), + expect.stringContaining("final class JSONAny"), + ]), + ); + expect(declarations.every((line) => line.includes("final class"))).toBe( + true, + ); + }); +}); From 867c8a4629603b546ab344041aaf69b8413fea7d Mon Sep 17 00:00:00 2001 From: coyaSONG <66289470+coyaSONG@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:06:52 +0900 Subject: [PATCH 2/2] fix(swift): make final classes optional --- .../src/language/Swift/SwiftRenderer.ts | 17 +++-- .../src/language/Swift/language.ts | 5 ++ test/languages.ts | 4 ++ test/unit/swift-final-classes.test.ts | 64 ++++++++++++------- 4 files changed, 62 insertions(+), 28 deletions(-) diff --git a/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts index 0ba9401461..a47b189df1 100644 --- a/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts +++ b/packages/quicktype-core/src/language/Swift/SwiftRenderer.ts @@ -500,7 +500,8 @@ export class SwiftRenderer extends ConvenienceRenderer { const isClass = this._options.useClasses || this.isCycleBreakerType(c); const structOrClass = isClass ? "class" : "struct"; - const finalPrefix = isClass ? "final " : ""; + const finalPrefix = + isClass && this._options.finalClasses ? "final " : ""; if (isClass && this._options.objcSupport) { // [Michael Fey (@MrRooni), 2019-4-24] Swift 5 or greater, must come before the access declaration for the class. @@ -1165,12 +1166,14 @@ encoder.dateEncodingStrategy = .formatted(formatter)`); this.emitLine( this.objcMembersDeclaration, this.accessLevel, - "final class JSONNull: NSObject, Codable {", + this._options.finalClasses ? "final " : "", + "class JSONNull: NSObject, Codable {", ); } else { this.emitLine( this.accessLevel, - "final class JSONNull: Codable, Hashable {", + this._options.finalClasses ? "final " : "", + "class JSONNull: Codable, Hashable {", ); } @@ -1216,7 +1219,7 @@ encoder.dateEncodingStrategy = .formatted(formatter)`); if (this._needAny) { this.ensureBlankLine(); - this.emitMultiline(`final class JSONCodingKey: CodingKey { + this.emitMultiline(`${this._options.finalClasses ? "final " : ""}class JSONCodingKey: CodingKey { let key: String required init?(intValue: Int) { @@ -1241,12 +1244,14 @@ encoder.dateEncodingStrategy = .formatted(formatter)`); this.emitLine( this.objcMembersDeclaration, this.accessLevel, - "final class JSONAny: NSObject, Codable {", + this._options.finalClasses ? "final " : "", + "class JSONAny: NSObject, Codable {", ); } else { this.emitLine( this.accessLevel, - "final class JSONAny: Codable {", + this._options.finalClasses ? "final " : "", + "class JSONAny: Codable {", ); } diff --git a/packages/quicktype-core/src/language/Swift/language.ts b/packages/quicktype-core/src/language/Swift/language.ts index de936322fd..1e6a53b493 100644 --- a/packages/quicktype-core/src/language/Swift/language.ts +++ b/packages/quicktype-core/src/language/Swift/language.ts @@ -54,6 +54,11 @@ export const swiftOptions = { } as const, "struct", ), + finalClasses: new BooleanOption( + "final-classes", + "Mark classes as final", + false, + ), mutableProperties: new BooleanOption( "mutable-properties", "Use var instead of let for object properties", diff --git a/test/languages.ts b/test/languages.ts index a84d020187..e268fae95f 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -769,6 +769,10 @@ export const SwiftLanguage: Language = { quickTestRendererOptions: [ { "support-linux": "false" }, { "struct-or-class": "class" }, + [ + "simple-object.json", + { "struct-or-class": "class", "final-classes": "true" }, + ], { density: "dense" }, { density: "normal" }, { "access-level": "internal" }, diff --git a/test/unit/swift-final-classes.test.ts b/test/unit/swift-final-classes.test.ts index bcdfd7c5a2..6dd680356f 100644 --- a/test/unit/swift-final-classes.test.ts +++ b/test/unit/swift-final-classes.test.ts @@ -36,21 +36,51 @@ function classDeclarations(output: string): string[] { } describe("Swift class generation", () => { - test("marks requested model classes as final", async () => { - const output = await renderSwift( - { + const modelSchema = { + type: "object", + properties: { + child: { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }, + }, + required: ["child"], + }; + const recursiveSchema = { + $ref: "#/definitions/Node", + definitions: { + Node: { type: "object", properties: { - child: { - type: "object", - properties: { name: { type: "string" } }, - required: ["name"], - }, + next: { $ref: "#/definitions/Node" }, + value: {}, }, - required: ["child"], + required: ["value"], }, - { "struct-or-class": "class" }, + }, + }; + + test("keeps classes open by default", async () => { + const modelOutput = await renderSwift(modelSchema, { + "struct-or-class": "class", + }); + const recursiveOutput = await renderSwift(recursiveSchema); + const declarations = classDeclarations( + `${modelOutput}\n${recursiveOutput}`, ); + + expect(declarations.length).toBeGreaterThan(0); + expect( + declarations.every((line) => !line.includes("final class")), + ).toBe(true); + }); + + test("marks requested model classes as final when enabled", async () => { + const output = await renderSwift(modelSchema, { + "struct-or-class": "class", + "final-classes": "true", + }); const declarations = classDeclarations(output); expect(declarations).toHaveLength(2); @@ -63,18 +93,8 @@ describe("Swift class generation", () => { }); test("marks cycle breakers and Codable helper classes as final", async () => { - const output = await renderSwift({ - $ref: "#/definitions/Node", - definitions: { - Node: { - type: "object", - properties: { - next: { $ref: "#/definitions/Node" }, - value: {}, - }, - required: ["value"], - }, - }, + const output = await renderSwift(recursiveSchema, { + "final-classes": "true", }); const declarations = classDeclarations(output);