From 8a7d372ecd74bba4a418e86c9dcd908c988b389e Mon Sep 17 00:00:00 2001 From: Adam Wolfe Gordon Date: Thu, 18 Jun 2026 09:48:49 -0600 Subject: [PATCH] schemas: Prime oapi-codegen's global state to make capitalization consistent The oapi-codegen package we use to generate Go code keeps a bunch of internal global state that affects how code is generated. In particular, the name normalizer it uses to form types names is a global variable initialized when we call `codegen.Generate`. We call some of the codegen package's utility functions to form custom names for types before generating code. These utility functions also use the global state set up by `codegen.Generate`, which isn't initialized when we call them, resulting in the utility functions producing different names than we expect. This makes our code generation non-deterministic: we get different name capitalization depending what order schemas get generated in. Make a no-op call to `codegen.Generate` with our desired options before calling the mutators to ensure that the global state is initialized for all our calls. Note that once https://github.com/oapi-codegen/oapi-codegen/pull/2393 is merged in the upstream code generator, we can use the new `SetGlobalStateOptions` function rather than the dummy `Generate` call. Fixes #113 Signed-off-by: Adam Wolfe Gordon --- internal/schemas/generator/go.go | 53 ++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/internal/schemas/generator/go.go b/internal/schemas/generator/go.go index b83f58cc..129f76ea 100644 --- a/internal/schemas/generator/go.go +++ b/internal/schemas/generator/go.go @@ -377,6 +377,39 @@ func goCollectOpenAPIs(fromFS afero.Fs) ([]goOpenAPI, error) { //nolint:gocognit var generateGoMutex sync.Mutex //nolint:gochecknoglobals // Must be global. func generateGo(s *spec3.OpenAPI, version string, mutators ...func(*spec3.OpenAPI)) (string, error) { + // codegen.Generate sets some global state that's used by the utility + // functions we call from our mutators. That has two implications for us: + // + // 1. We must hold the `generateGoMutex` while calling the mutators, not + // just when we call `codegen.Generate`. + // 2. We must call `codegen.Generate` with the options we're going to use + // *before* we call the mutators, so that the global state is correct + // inside the codegen package. We call it with a minimal input and ignore + // the output - this call is just to set up the global state. Once + // https://github.com/oapi-codegen/oapi-codegen/pull/2393 is merged we + // can use `codegen.SetGlobalStateOptions` instead of calling `Generate`. + generateGoMutex.Lock() + defer generateGoMutex.Unlock() + + cfg := codegen.Configuration{ + PackageName: version, + Generate: codegen.GenerateOptions{ + Models: true, + }, + OutputOptions: codegen.OutputOptions{ + SkipPrune: true, + NameNormalizer: string(codegen.NameNormalizerFunctionToCamelCaseWithInitialisms), + SkipFmt: true, + UserTemplates: map[string]string{ + "imports.tmpl": goImportsTemplate, + }, + }, + } + _, err := codegen.Generate(&openapi3.T{}, cfg) + if err != nil { + return "", errors.Wrap(err, "failed to setup codegen global state") + } + for _, mut := range mutators { mut(s) } @@ -394,22 +427,7 @@ func generateGo(s *spec3.OpenAPI, version string, mutators ...func(*spec3.OpenAP } // Generate code! - generateGoMutex.Lock() - defer generateGoMutex.Unlock() - goCode, err := codegen.Generate(oapiInput, codegen.Configuration{ - PackageName: version, - Generate: codegen.GenerateOptions{ - Models: true, - }, - OutputOptions: codegen.OutputOptions{ - SkipPrune: true, - NameNormalizer: string(codegen.NameNormalizerFunctionToCamelCaseWithInitialisms), - SkipFmt: true, - UserTemplates: map[string]string{ - "imports.tmpl": goImportsTemplate, - }, - }, - }) + goCode, err := codegen.Generate(oapiInput, cfg) if err != nil { return "", errors.Wrap(err, "failed to generate go code from OpenAPI schema") } @@ -542,9 +560,6 @@ func goRenamePropertyTypes(baseName string, props map[string]spec.Schema) { } func goFixName(name string) string { - generateGoMutex.Lock() - defer generateGoMutex.Unlock() - lastDot := strings.LastIndex(name, ".") if lastDot == -1 { return codegen.ToCamelCaseWithInitialisms(name)