codegen: choose generated names once and enforce generated contracts - #3969
Open
raphael wants to merge 43 commits into
Open
codegen: choose generated names once and enforce generated contracts#3969raphael wants to merge 43 commits into
raphael wants to merge 43 commits into
Conversation
raphael
marked this pull request as ready for review
August 24, 2026 04:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
Goa could choose a generated Go name in one local naming scope and later look it up in another. When two services or transports wanted the same name, those scopes did not always see conflicts in the same order. Goa could therefore declare
ValidateLifecycleand emit a call toValidateLifecycle2. AURA exposed the result as generated source that did not compile.This change makes one generation run choose every generated package name, import name, type name, function name, and constant name before writing source. Service, HTTP, gRPC, JSON-RPC, OpenAPI, example, and plugin generation all read those same final choices. Templates write the selected code; they do not choose names or rediscover static type shapes while the generated program runs.
The result is repeatable generated source whose declarations and references agree across services, transports, shared packages, and plugins.
Before and after
Before:
NameScopeto choose a name again.After:
Union names use the union's design identity and owning Go package. They do not use decorated strings, file-path registry keys, or changed
Hashbehavior.AGENTS.mdandcodegen/ARCHITECTURE.mdrecord this lifecycle so later generator work follows the same ownership rules.Other incorrect generated contracts fixed here
The shared generation model exposed several places where generated declarations, validation, examples, or transport code disagreed. These fixes belong in Goa because application code cannot safely repair generated wire contracts.
OneOfnow requires exactly one non-nil branch. Selecting an empty-message branch is valid. Selecting no branch, or selecting a typed wrapper whose value is nil, returns a precise validation error.ArrayOfRequiredworks for primitive aliases. Goa no longer emitselement == nilwhen the Go element is a value such as a string alias.The final example review found four additional transport defects:
tenantIDdid not populate the generated protobuf field taggedtenant_id, while unknown fields were silently ignored. Generated gRPC commands now use protobuf JSON. Help shows the exact.protofield names, standard lower-camel protobuf aliases remain accepted, and unknown fields return an error.MapParams()handling was internally inconsistent in v3.30.0: generated clients sentkey=value, while generated servers searched forquery[key]. Servers now decode the flat query entries that generated clients and the DSL contract use.Closenow performs the WebSocket handshake and sends the normal close frame. A separate one-time close record preserves the behavior introduced in 2018: later and concurrentClosecalls return the first result without writing again.The gRPC JSON, whole-query map, OpenAPI, and empty-stream problems all exist in v3.30.0; they were not invented by the new planning architecture. The diff corrects them because the complete example regeneration made their caller-visible failures observable.
JSON-RPC contract
This PR supports two clear JSON-RPC transports:
Goa now rejects JSON-RPC methods that request WebSocket transport, stream client payloads, stream in both directions, combine
ResultwithStreamingResult, or declare server streaming without server-sent events. The DSL reports these errors before code generation. Ordinary Goa HTTP WebSockets and gRPC streaming are unchanged.Every JSON-RPC response contains either
resultorerror; a successful result is present even when its value is null. A malformed object without an ID is an invalid request and receives error-32600with a null ID. Only a valid request object without an ID is a notification and receives no response.For a viewed Goa result, the standard JSON-RPC
resultmember contains this JSON value:{ "view": "detailed", "body": {} }The
viewandbodyfields are emitted only for results that use Goa views. Results without views keep their normal JSON value.The removed JSON-RPC WebSocket path was not used by goa-ai's MCP implementation. goa-ai mounts MCP over JSON-RPC HTTP and server-sent events. The dedicated goa-ai generation-migration branch passes its generation, runtime, and race-enabled integration tests against this Goa branch. Current goa-ai main still calls the removed internal
NormalizeRootandservice.NewServicesDataconstructors, so it must merge a dedicated migration before updating its Goa dependency.Plugin compatibility
The new per-run plugin factory API lets a plugin declare names before Goa fixes them, then generate files using those exact names.
The released extension API remains available:
codegen.RegisterPlugin,RegisterPluginFirst, andRegisterPluginLastkeep their four-argument signatures and ordering.generator.Generatorsremains replaceable.generator.Service,Transport,OpenAPI, andExampleremain exported with the releasedGenfuncsignature.Generatorsjoin the shared naming run. Additional generator functions run with the prepared roots after names are fixed.MountHandlerandHandlerInit, are preserved.Plugins that use the released registration API and add or modify generated files should continue to compile. Plugins that depended on mutable internal generator data, changed the design after preparation, or called removed internal constructors must move that work into the new planning API.
External plugin repositories reviewed for follow-up:
keboola/keboola-as-code,tchssk/goaplugins,xeger/goa-vcr, andkitagry/goaplugin.pgEdge/control-planeandNagayamaRyoga/goalint.Breaking changes and required action
Regeneration is required. Generated source from the old and new generator must not be mixed in one module. Current goa-ai main must merge its generator migration before updating to this Goa revision. There is no stored-data migration.
info *LoggingInfobecomesinfo LoggingInfo. The public name is an interface because unary calls, stream sends, and stream receives use separate specific implementations.Service(),Method(),CallType(), and typed payload or result methods remain available.jsonrpc.WebSocketConfigis removed.viewandbodyinsideresult. Successful null results include"result": null.Request2may becomeRequest.key=valueentries rather than the accidentalquery[key]=valueform..protofield names. Primitive, array, and map wrapper messages are shown as JSON objects such as{"field": ...}.The generator and regenerated application code should be deployed together. Rolling back means reverting the Goa revision and regenerating with the prior generator; no database or message conversion is involved.
Verification performed
make lintin Goa: zero issues.go test ./... -count=1in Goa: all packages pass.make integration-testin Goa: the separate JSON-RPC protocol suite passes.go test ./... -count=1andmake iteston goa-ai commit758a500with this Goa revision: all package, MCP, and race-enabled integration tests pass. Its lint target reports 33 findings, so this PR does not claim a clean goa-ai lint result.26b57ee) was also tested. It does not compile against this Goa branch because it still callsNormalizeRootandservice.NewServicesData. That companion migration remains required before dependency rollout.goa.design/exampleswere deleted and regenerated twice with this exact Goa binary. Both runs produced the same digest across 493 generated files.go test -count=1 ./...andgo build ./...against this Goa branch. The nineteenth module is the tracing example, which has no design directory to regenerate.origin/main: 238 are unchanged, 252 are modified, and 3 are newly generated. No generated file was omitted from the comparison.apidiffwas run for every example module and the affectedhttpstatuspackages; the breaking changes are listed above.No production deployment or stored-data migration was performed.
Reviewer focus
The most useful review order is:
codegen/generation.go,codegen/generated_types.go, and import naming: one owner chooses final names.codegen/serviceplanning and linking: service types, views, validation, and interceptor call shapes.codegen/generatorplugin compatibility tests: released entry points still work while new plugins can declare names before they become final.