Generate one exact contract across every Goa output - #3971
Draft
raphael wants to merge 118 commits into
Draft
Conversation
* Fix schemas and security for named strings * Complete named credential generation contracts * Test optional Basic username with required password
This was referenced Sep 3, 2026
Co-authored-by: Raphael (manual office deploy after cloud-state fix) <simon.raphael@gmail.com>
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.
Community preview
v3.31.0-preview.3is the published, opt-in preview at exact commit5a434148df83c30b9a6c889e399b6548fbb0c83f. The public Go proxy resolves that tag to the same commit. It is not selected bygo get ...@latestwhile stable Goav3.30.0exists.Testers request it explicitly:
Before regenerating, read
UPGRADING.md. It explains the generated-source changes, application and plugin breaks, coordinated client/server cases, migration steps, rollback, and what to include in a useful report.The final stable version is deliberately undecided. This preview lets services and plugins test the corrected contracts before we decide whether the final release can remain in v3 or requires a new major version. Please use this PR for preview-wide feedback, open a separate issue for a small reproducible bug, and use GitHub Discussions for migration or design questions.
What this fixes
Goa now chooses every generated package name, import, file, validation function, conversion function, and transport helper once for the complete generation run. Service, HTTP, gRPC, JSON-RPC, CLI, OpenAPI, example, and plugin output all use those same recorded choices.
The failure that started this work declared a validation function under one name and called another. Separate generator passes had created separate name scopes for the same Go package. Each pass saw a different set of conflicts, so each answer looked valid by itself while the combined source did not compile. The same split ownership affected union types, imports, transport conversions, examples, plugins, and repeated generation in one process.
The earlier previews also exposed a related HTTP client error for results whose view is chosen at runtime. A view such as
tinymay intentionally omit a field required by the complete result. The client decoded that valid response with the complete body type, so validation could reject it. A WebSocket client could also panic when the selected view omitted a required nested object. Preview.3 generates and selects the exact body, validator, and conversion for each view before decoding the response.UPGRADING.mdis the public testing and migration guide.codegen/ARCHITECTURE.mdis the internal generator contract. It includes the complete exported-API migration table, generated-source changes, mixed-version behavior, and rollback effects. This description highlights the contracts reviewers should understand first.One generation run owns every answer
A generation run now has one saved plan: a typed record of the prepared design and every decision needed to write source.
Rendering cannot discover another declaration, allocate another name, rebuild transport analysis, or change the design. Work known from the design stays inside generation instead of becoming runtime branches or code that parses generator-made names.
Ownership is explicit:
codegen.Generationowns the prepared roots, generated packages, names, imports, and output paths for one run.service.Planowns service types, endpoints, clients, errors, interceptors, views, unions, validators, and service conversions..protodeclarations, protobuf Go names, field presence, metadata, validation, and conversion.Repeated, concurrent, reversed-order, multi-root, multi-transport, and plugin-assisted runs now produce the same complete answer.
Generated contracts that become exact
OneOf unions
Copies of one authored
OneOfshare a declaration only when their emitted definitions match. Separately authored unions stay separate even when their branches happen to be equal. Public union names are exact; a true collision stops generation and asks the design author to give the declarations distinctTypeNamevalues.Compiler-created branch types use names such as
ValueBranchText. HTTP union names describe the body that owns them, for exampleValueRequestBody,ValueStreamingBody,ValueResponseBody, andValueDetailedResponseBody. Relocated unions are written by their owning package inunions.go.Generated branch fields are private. Callers use
New...,Set...,As...,Kind, andValidate. Selecting a branch replaces the previous selection, and a failed JSON decode leaves the prior valid value unchanged.Required unions reject no selection, typed nil wrappers, and selected nil message, bytes, or
Anyvalues. A selected nonnil empty message remains valid. JSON and protobuf union data do not change.Protobuf and gRPC
Required singular booleans, numbers, strings, enums, bytes, and their aliases use proto3 presence. Generated Go scalar fields become pointers; bytes and bytes aliases remain
[]byte;Anyand other messages remain pointers. Goa service fields keep their existing value layout.Generated clients and servers validate protobuf messages before converting them. Omitted required fields now return precise validation errors instead of silently becoming service zero values. Explicit
false,0, empty string, empty bytes, and protobuf null remain valid when supplied.Defaults now follow presence. An absent protobuf input receives its authored default. An explicit zero, empty bytes, or protobuf null remains explicit. Service-to-protobuf conversion never adds defaults; it sends exactly what service code returned.
Each selected gRPC result view has its own conversion and validation. Fields omitted by that view are not required. Dynamic server streams send the selected view before the first message, and clients use it before decoding. Default validators keep names such as
ValidateShowResponse; another view uses a stable name such asValidateShowResponseTiny. Equal validators share one declaration instead of receiving discovery-order suffixes.Repeated and map wrappers no longer claim protobuf can distinguish omitted from empty. Nil map-value wrappers decode as empty collections instead of panicking, while authored length and item rules still run. Validators that provably do nothing are not emitted.
ArrayOfRequiredalso works for primitive aliases without generating an impossible nil check for value elements.gRPC metadata conversion uses the designed type. Bytes use their actual string contents rather than Go slice display text, floating-point values use the designed width, and response encoders use the real result variable.
Generation checks
protoc-gen-go v1.36.12andprotoc-gen-go-grpc v1.6.2before files are written. These are the exact tools covered by the generated-module tests.HTTP
Incoming JSON arrays declared with
ArrayOfRequireduse pointer elements for primitives and primitive aliases so[null]is rejected. Valid JSON converts to the same service value slices, and outgoing bodies remain value slices.Multipart decoders now fill the generated request body, validate it, and only then build the service payload. Nested validation errors keep their complete field and array-index paths. Exclusive maximum validation rejects the maximum itself and values above it.
A map assigned to the complete query string now reads raw keys such as
?a=1&b=2, matching generated clients. Float query values use Go's shortest round-trip text. Generated clients close bodies they fully consume and return read and close errors; a deliberately returned raw body remains open for the caller.Server-sent events write primitive values as raw event text, distinguish an omitted optional value from a present empty string, return write and flush failures, and decode retry values into the designed integer type. A variable result view is fixed before the first event; unknown or changing views fail precisely.
For an ordinary HTTP result whose view is selected at runtime, the client now reads the selected view before decoding the body and uses that view's exact generated body, validator, and conversion. Missing view metadata selects
default. An unknown view or a body that is invalid for the selected view returns a precise error. HTTP method signatures and wire formats do not change.The same rule applies to server-streaming WebSocket results. The client records the selected view once and decodes every message through its exact generated contract. A view that omits a required field from the complete result no longer fails validation or causes a nil dereference.
An empty successful WebSocket stream now performs the upgrade, sends a normal close frame, and closes once instead of returning before the handshake.
JSON-RPC
JSON-RPC now supports two honest method shapes: one request and response over HTTP, or one request followed by server results over explicit server-sent events. Design validation rejects client streams, bidirectional streams, WebSocket streaming, server streams without
ServerSentEvents(), and methods that define bothResultandStreamingResult.Generated server-sent-event implementations use the transport-independent service stream methods
Send,SendWithContext, andClose. Clients useRecvorRecvWithContext. Each result is a JSON-RPC notification. A request with an ID ends with one terminal response:result: nullfor success or the returned JSON-RPC error. A notification receives no terminal response.Request handling now follows JSON-RPC 2.0 for omitted, null, empty-string, string, and numeric IDs; invalid objects; leading whitespace; empty and mixed batches; notifications; explicit
result: null; invalid parameters; internal failures; acceptable JSON and server-sent-event media types; and rejection of streams inside batches. Clients reject unknown event names and notifications for another method instead of silently skipping them. Body reads, closes, and batch writes return their failures.A caller-selected view uses this method result inside JSON-RPC's standard top-level
resultmember:{ "view": "detailed", "body": { "...": "..." } }This envelope is generated only when the caller chooses among views. Fixed-view and unviewed methods retain their body shape. The envelope is valid generic JSON-RPC; a protocol layered on JSON-RPC, including MCP, must still use that protocol's required result schema.
API errors, interceptors, CLI, examples, and OpenAPI
An API-level error is a reusable definition, not an error returned by every endpoint. A service or method selects it with name-only
Error("busy"), which preserves its type, validation, defaults, description, andTemporary,Timeout, andFaultsettings. Supplying another argument defines a separate local error.Generated interceptor information changes from
*LoggingInfo, a pointer to a public struct with private fields, to the read-onlyLoggingInfointerface with the same public accessors. Goa emits a private implementation specialized for the exact method and call kind, so payload, result, send, and receive accessors do not inspect method names or switch on runtime types.Generated commands execute the endpoint, receive streams, print values, and return endpoint, stream, output, and close errors. gRPC complete-message flags decode protobuf JSON. Example values belong to the declaration that authored them, so an earlier example cannot consume shared random state and alter a later one.
OpenAPI now emits consistent base64 byte examples, empty security scope arrays rather than JSON null, independent server-variable values, designed descriptions, correctly filtered security definitions and examples, and schemas that match selected views and server-sent-event data.
Plugin compatibility
The released four-argument registration functions,
Genfunc, replaceableGenerators, and the exportedService,Transport,OpenAPI, andExamplefunctions remain available. Released callback ordering and repeated callback names remain supported. Built-in functions join the shared plan; externalGenfuncvalues run after names are final.Common generated-name fields used by existing templates remain, including
MountHandler,HandlerInit, constructors, codecs, validators, multipart helpers, server-sent-event names, WebSocket names, and gRPC names. Plugins that edit ordinary values or files should largely continue to work.A plugin that adds a package declaration or chooses a generated name must use
PluginFactoryand declare it duringPlugin.Plan. A preparation plugin that adds services must attach them to the owning root and callEvaluateAttachedServices. Public helpers that manually ran plugin callbacks or rebuilt private service or transport analysis are removed because they would create a second set of decisions.Several exported planning and template-data structures changed. Some gained declaration records or private state, some no longer compare with
==, and positional literals must become named literals.codegen/ARCHITECTURE.mdlists every changed exported API and its replacement.Upgrade, mixed versions, and rollback
Updating the Goa module does not change an already compiled program. Changes take effect when code is regenerated. There is no persisted-data migration.
Regenerate the entire
gentree together; declarations and callers from different generations are not compatible.goa examplepreserves handwritten starter files, so update those separately.Coordinate client and server deployment and rollback for:
Required protobuf presence keeps the binary schema compatible, but an old client cannot prove that a required zero or empty primitive was present. A new server can reject that omission. Regenerate both sides where required zero or empty values matter.
Other intentional source changes affect direct union field access, interceptor info pointers, multipart decoder signatures, incoming
ArrayOfRequiredtransport literals, protobuf scalar fields, direct calls to removed empty validators or combined conversions, generated command starters, gRPC protobuf-JSON CLI scripts, and JSON-RPC WebSocket APIs.JSON-RPC WebSocket has no compatibility mode. Migrate those methods to unary JSON-RPC over HTTP, explicit JSON-RPC server-sent-event streaming, ordinary HTTP WebSocket, or gRPC before upgrading.
The preview.3 selected-view repair does not change service client method signatures or HTTP wire formats. Code that directly calls generated transport constructors for a dynamic viewed result may need the new view-specific constructor name, such as
NewShowResultTinyOK.To roll back, restore the prior Goa module version, regenerate the complete
gentree with that version, and redeploy matched client and server artifacts. Roll back both sides together for the coordinated cases above. Do not mix files generated by two Goa versions.The detailed table in
codegen/ARCHITECTURE.mdalso covers compact HTTP float text, gRPC metadata text, whole-query maps, exact output-path failures, stricter design validation, OpenAPI snapshot changes, and every exported generator-library change.Review this first
codegen/ARCHITECTURE.mdfor the generation lifecycle, ownership rules, and complete migration contract.codegen/generation.go,codegen/generated_types.go, andcodegen/generator/plan.gofor run and package ownership.codegen/service/plan.goandcodegen/service/generated_package.gofor service declarations and names.codegen/union.go,codegen/validation.go, and the service and HTTP union templates for exclusive union behavior.grpc/codegen/protobuf_catalog.go,grpc/codegen/service_data.go, andcodegen/go_transform.gofor protobuf presence, defaults, views, and conversions.http/codegen/client.go,http/codegen/service_data.go,http/codegen/websocket.go, andhttp/codegen/viewed_client_validation_runtime_test.gofor preview.3 selected-view decoding.jsonrpc/types.goandjsonrpc/codegen/{plan,server,client,sse,viewed_result}.gofor protocol and streaming behavior.codegen/generator/plugin_public_integration_test.gofor released plugin compatibility.Validation performed
v3.31.0-preview.3tag and the public Go proxy both resolve to exact PR head5a434148df83c30b9a6c889e399b6548fbb0c83f.maincommit85dde2403d6bc1c461ff7e682db692b0e41ba835pinsv3.31.0-preview.3. Its exact-head build and integration checks pass.0609dd07f5dc5bd703ad2a148e1d57012ca11899pins Goa-AI85dde2403d6bc1c461ff7e682db692b0e41ba835andv3.31.0-preview.3. Full lint, tests, build, and repeated generation pass without an unexplained generated diff.9733bcdbc0c67114b1f0af2753ebb5b3bd7ba7f6. All 19 modules pinv3.31.0-preview.3, build, and test. The exact-head Linux check passes, and the runnable examples were exercised against their generated clients and servers.b89aeccf8fde768513655d9047343ec9a1198b84. All four modules pinv3.31.0-preview.3, and the exact-head Linux check passes.9f7615f67f3b5ddeae5a5911f4dc70187a79aaffpinsv3.31.0-preview.3, Goa-AI85dde2403d6bc1c461ff7e682db692b0e41ba835, and Flows0609dd07f5dc5bd703ad2a148e1d57012ca11899. All ten PR checks pass. Local full Go tests, lint, stable code generation, architecture checks, and the migration binary's Linux ARM64 build also pass.origin/v3identified the intentional incompatibilities;codegen/ARCHITECTURE.mddocuments each group and its migration.Goa, examples, plugins, and Flows use the remote branch
fix/goa-generation-plan. Goa-AI is merged onmain, and AURA is reviewed in PR #494. This pull request remains draft so the preview can collect community feedback before the stable version is chosen.