From 49a661f542bc574a45bfdde6c3a8ba9256cc66cb Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 17:04:25 -0400 Subject: [PATCH 1/2] fix(golang): always emit consolidated imports regardless of leadingComments (#2670) Passing leadingComments (even []) to the quicktype-core API skipped GolangRenderer's whole single-file header block, including the consolidated top-of-file import collection, so a lazily-emitted `import "time"` for date-time fields landed mid-file, producing invalid Go. leadingComments now only replaces the default header comment, matching the pattern used by other language renderers, while imports are always consolidated at the top of the file. Co-Authored-By: gpt-5.6-sol via pi --- .../src/language/Golang/GolangRenderer.ts | 60 +++++++----- test/fixtures.ts | 91 ++++++++++++++++++- 2 files changed, 126 insertions(+), 25 deletions(-) diff --git a/packages/quicktype-core/src/language/Golang/GolangRenderer.ts b/packages/quicktype-core/src/language/Golang/GolangRenderer.ts index 72d50068e8..37cbfaf5e4 100644 --- a/packages/quicktype-core/src/language/Golang/GolangRenderer.ts +++ b/packages/quicktype-core/src/language/Golang/GolangRenderer.ts @@ -12,7 +12,7 @@ import { assert, defined } from "../../support/Support.js"; import type { TargetLanguage } from "../../TargetLanguage.js"; import { type ClassProperty, - type ClassType, + ClassType, type EnumType, type Type, type TypeKind, @@ -194,28 +194,37 @@ export class GoRenderer extends ConvenienceRenderer { if ( this._options.multiFileOutput && this._options.justTypes === false && - this._options.justTypesAndPackage === false && - this.leadingComments === undefined + this._options.justTypesAndPackage === false ) { - this.emitLineOnce( - "// Code generated from JSON Schema using quicktype. DO NOT EDIT.", - ); - this.emitLineOnce( - "// To parse and unparse this JSON data, add this code to your project and do:", - ); - this.emitLineOnce("//"); - const ref = modifySource(camelCase, name); - this.emitLineOnce( - "// ", - ref, - ", err := ", - defined(this._topLevelUnmarshalNames.get(name)), - "(bytes)", - ); - this.emitLineOnce("// bytes, err = ", ref, ".Marshal()"); + if (this.leadingComments !== undefined) { + this.emitComments(this.leadingComments); + } else { + this.emitLineOnce( + "// Code generated from JSON Schema using quicktype. DO NOT EDIT.", + ); + this.emitLineOnce( + "// To parse and unparse this JSON data, add this code to your project and do:", + ); + this.emitLineOnce("//"); + const ref = modifySource(camelCase, name); + this.emitLineOnce( + "// ", + ref, + ", err := ", + defined(this._topLevelUnmarshalNames.get(name)), + "(bytes)", + ); + this.emitLineOnce("// bytes, err = ", ref, ".Marshal()"); + } } - this.emitPackageDefinitons(true); + const imports = + t instanceof ClassType + ? this.collectClassImports(t) + : t instanceof UnionType + ? this.collectUnionImports(t) + : undefined; + this.emitPackageDefinitons(true, imports); const unmarshalName = defined(this._topLevelUnmarshalNames.get(name)); if (this.namedTypeToNameForTopLevel(t) === undefined) { @@ -305,7 +314,7 @@ export class GoRenderer extends ConvenienceRenderer { private emitUnion(u: UnionType, unionName: Name): void { this.startFile(unionName); - this.emitPackageDefinitons(false); + this.emitPackageDefinitons(false, this.collectUnionImports(u)); const [hasNull, nonNulls] = removeNullFromUnion(u); const isNullableArg = hasNull !== null ? "true" : "false"; @@ -610,10 +619,13 @@ func marshalUnion(pi *int64, pf *float64, pb *bool, ps *string, haveArray bool, if ( this._options.multiFileOutput === false && this._options.justTypes === false && - this._options.justTypesAndPackage === false && - this.leadingComments === undefined + this._options.justTypesAndPackage === false ) { - this.emitSingleFileHeaderComments(); + if (this.leadingComments !== undefined) { + this.emitComments(this.leadingComments); + } else { + this.emitSingleFileHeaderComments(); + } this.emitPackageDefinitons(false, this.collectAllImports()); } diff --git a/test/fixtures.ts b/test/fixtures.ts index dece181ef6..8339005a54 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -25,7 +25,16 @@ import { callAndExpectFailure, } from "./utils"; import * as languages from "./languages"; -import type { LanguageName, Option, RendererOptions } from "quicktype-core"; +import { + FetchingJSONSchemaStore, + InputData, + JSONSchemaInput, + type LanguageName, + type Option, + type RendererOptions, + quicktype as quicktypeCore, + quicktypeMultiFile, +} from "quicktype-core"; import { mustNotHappen, defined, @@ -823,6 +832,85 @@ class JSONSchemaFixture extends LanguageFixture { } } +// `leadingComments` is a quicktype-core API option, so the CLI fixture path +// cannot exercise it. +class LeadingCommentsGoFixture extends JSONSchemaFixture { + constructor() { + super(languages.GoLanguage, "schema-golang-leading-comments"); + } + + getSamples(sources: string[]): { priority: Sample[]; others: Sample[] } { + return samplesFromSources( + sources, + ["test/inputs/schema/date-time.schema"], + [], + "schema", + ); + } + + private async inputData(filename: string): Promise { + const schemaInput = new JSONSchemaInput( + new FetchingJSONSchemaStore(), + ); + await schemaInput.addSource({ + name: this.language.topLevel, + schema: fs.readFileSync(filename, "utf8"), + }); + const inputData = new InputData(); + inputData.addInput(schemaInput); + return inputData; + } + + async runQuicktype( + filename: string, + additionalRendererOptions: RendererOptions, + ): Promise { + const rendererOptions = _.merge( + {}, + this.language.rendererOptions, + additionalRendererOptions, + ); + const result = await quicktypeCore({ + inputData: await this.inputData(filename), + lang: this.language.name, + leadingComments: [], + rendererOptions, + }); + fs.writeFileSync(this.language.output, result.lines.join("\n")); + + const multiFileResult = await quicktypeMultiFile({ + inputData: await this.inputData(filename), + lang: this.language.name, + leadingComments: [], + rendererOptions: { + ...rendererOptions, + "multi-file-output": true, + }, + }); + mkdirs("multi"); + for (const [outputFilename, output] of multiFileResult) { + fs.writeFileSync( + path.join("multi", outputFilename), + output.lines.join("\n"), + ); + } + } + + async test( + filename: string, + additionalRendererOptions: RendererOptions, + additionalFiles: string[], + ): Promise { + const numFiles = await super.test( + filename, + additionalRendererOptions, + additionalFiles, + ); + await execAsync("go test multi/*.go"); + return numFiles + testsInDir("multi", "go").length; + } +} + type TreeSitterTarget = { displayName: string; language: languages.Language; @@ -1602,6 +1690,7 @@ export const allFixtures: Fixture[] = [ "schema-java-lombok", ), new JSONSchemaFixture(languages.GoLanguage), + new LeadingCommentsGoFixture(), new JSONSchemaFixture(languages.CJSONLanguage), new JSONSchemaFixture(languages.CPlusPlusLanguage), new JSONSchemaFixture(languages.RustLanguage), From 3baf628f51f100a85fe3441f7321d8b8114cc10d Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Mon, 20 Jul 2026 20:04:59 -0400 Subject: [PATCH 2/2] style: collapse JSONSchemaInput constructor call to satisfy Biome Co-Authored-By: Claude --- test/fixtures.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/fixtures.ts b/test/fixtures.ts index 8339005a54..a38a52d77f 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -849,9 +849,7 @@ class LeadingCommentsGoFixture extends JSONSchemaFixture { } private async inputData(filename: string): Promise { - const schemaInput = new JSONSchemaInput( - new FetchingJSONSchemaStore(), - ); + const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore()); await schemaInput.addSource({ name: this.language.topLevel, schema: fs.readFileSync(filename, "utf8"),