Skip to content

Generate one exact contract across every Goa output - #3971

Draft
raphael wants to merge 118 commits into
v3from
fix/goa-generation-plan
Draft

Generate one exact contract across every Goa output#3971
raphael wants to merge 118 commits into
v3from
fix/goa-generation-plan

Conversation

@raphael

@raphael raphael commented Aug 24, 2026

Copy link
Copy Markdown
Member

Community preview

v3.31.0-preview.3 is the published, opt-in preview at exact commit 5a434148df83c30b9a6c889e399b6548fbb0c83f. The public Go proxy resolves that tag to the same commit. It is not selected by go get ...@latest while stable Goa v3.30.0 exists.

Testers request it explicitly:

go get goa.design/goa/v3@v3.31.0-preview.3
go install goa.design/goa/v3/cmd/goa@v3.31.0-preview.3

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 tiny may 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.md is the public testing and migration guide. codegen/ARCHITECTURE.md is 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.

  1. Preparation plugins make their authorized design changes.
  2. Goa records the prepared design roots and creates one service plan for each root.
  3. HTTP, gRPC, JSON-RPC, OpenAPI, examples, and factory plugins consume those exact service plans.
  4. Each generated Go package collects every declaration, import, and file that will belong to it.
  5. Goa fixes names and import qualifiers once.
  6. Templates write only those recorded choices.

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.Generation owns the prepared roots, generated packages, names, imports, and output paths for one run.
  • service.Plan owns service types, endpoints, clients, errors, interceptors, views, unions, validators, and service conversions.
  • HTTP owns HTTP bodies, parameters, multipart handling, server-sent events, WebSocket code, codecs, and validation.
  • protobuf and gRPC own .proto declarations, protobuf Go names, field presence, metadata, validation, and conversion.
  • JSON-RPC consumes the exact shared HTTP plan, then owns JSON-RPC requests, responses, batches, errors, and server-sent-event behavior.
  • Plugin planning owns names and declarations added by that plugin.
  • File assembly combines valid contributions to the same file and rejects conflicting or unsafe output paths before writing.

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 OneOf share 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 distinct TypeName values.

Compiler-created branch types use names such as ValueBranchText. HTTP union names describe the body that owns them, for example ValueRequestBody, ValueStreamingBody, ValueResponseBody, and ValueDetailedResponseBody. Relocated unions are written by their owning package in unions.go.

Generated branch fields are private. Callers use New..., Set..., As..., Kind, and Validate. 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 Any values. 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; Any and 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 as ValidateShowResponseTiny. 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. ArrayOfRequired also 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.12 and protoc-gen-go-grpc v1.6.2 before files are written. These are the exact tools covered by the generated-module tests.

HTTP

Incoming JSON arrays declared with ArrayOfRequired use 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 both Result and StreamingResult.

Generated server-sent-event implementations use the transport-independent service stream methods Send, SendWithContext, and Close. Clients use Recv or RecvWithContext. Each result is a JSON-RPC notification. A request with an ID ends with one terminal response: result: null for 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 result member:

{
  "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, and Temporary, Timeout, and Fault settings. 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-only LoggingInfo interface 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, replaceable Generators, and the exported Service, Transport, OpenAPI, and Example functions remain available. Released callback ordering and repeated callback names remain supported. Built-in functions join the shared plan; external Genfunc values 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 PluginFactory and declare it during Plugin.Plan. A preparation plugin that adds services must attach them to the owning root and call EvaluateAttachedServices. 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.md lists 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 gen tree together; declarations and callers from different generations are not compatible. goa example preserves handwritten starter files, so update those separately.

Coordinate client and server deployment and rollback for:

  • caller-selected JSON-RPC views;
  • JSON-RPC server-sent-event streams;
  • dynamic caller-selected gRPC streams;
  • viewed gRPC methods whose selected view omits fields used by the old default conversion; and
  • HTTP server-sent-event streams with optional primitive data.

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 ArrayOfRequired transport 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 gen tree 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.md also 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

  1. codegen/ARCHITECTURE.md for the generation lifecycle, ownership rules, and complete migration contract.
  2. codegen/generation.go, codegen/generated_types.go, and codegen/generator/plan.go for run and package ownership.
  3. codegen/service/plan.go and codegen/service/generated_package.go for service declarations and names.
  4. codegen/union.go, codegen/validation.go, and the service and HTTP union templates for exclusive union behavior.
  5. grpc/codegen/protobuf_catalog.go, grpc/codegen/service_data.go, and codegen/go_transform.go for protobuf presence, defaults, views, and conversions.
  6. http/codegen/client.go, http/codegen/service_data.go, http/codegen/websocket.go, and http/codegen/viewed_client_validation_runtime_test.go for preview.3 selected-view decoding.
  7. jsonrpc/types.go and jsonrpc/codegen/{plan,server,client,sse,viewed_result}.go for protocol and streaming behavior.
  8. codegen/generator/plugin_public_integration_test.go for released plugin compatibility.

Validation performed

  • The published v3.31.0-preview.3 tag and the public Go proxy both resolve to exact PR head 5a434148df83c30b9a6c889e399b6548fbb0c83f.
  • Every check on that Goa head passes: Go 1.25 on Linux and Windows, dependency review, and both CodeQL checks.
  • Focused generated-runtime and golden tests cover selected HTTP views for ordinary responses and server-streaming WebSockets, including a valid view that omits complete-result fields, the default view, unknown views, invalid selected-view bodies, collection results, explicit bodies, and a missing nested object that previously caused a panic.
  • The broader generated-runtime tests cover exclusive unions, required OneOf values, required protobuf primitives and aliases, protobuf defaults, collection wrappers, selected gRPC views, JSON-RPC protocol rules, JSON-RPC viewed unary and server-sent-event results, and released plugin compatibility.
  • Goa-AI main commit 85dde2403d6bc1c461ff7e682db692b0e41ba835 pins v3.31.0-preview.3. Its exact-head build and integration checks pass.
  • Flows commit 0609dd07f5dc5bd703ad2a148e1d57012ca11899 pins Goa-AI 85dde2403d6bc1c461ff7e682db692b0e41ba835 and v3.31.0-preview.3. Full lint, tests, build, and repeated generation pass without an unexplained generated diff.
  • The examples branch is clean and remote-synced at 9733bcdbc0c67114b1f0af2753ebb5b3bd7ba7f6. All 19 modules pin v3.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.
  • The plugins branch is clean and remote-synced at b89aeccf8fde768513655d9047343ec9a1198b84. All four modules pin v3.31.0-preview.3, and the exact-head Linux check passes.
  • AURA PR FormDecoder #494 at 9f7615f67f3b5ddeae5a5911f4dc70187a79aaff pins v3.31.0-preview.3, Goa-AI 85dde2403d6bc1c461ff7e682db692b0e41ba835, and Flows 0609dd07f5dc5bd703ad2a148e1d57012ca11899. 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.
  • Every changed Goa golden was reviewed against its owning contract. An exported-API comparison against origin/v3 identified the intentional incompatibilities; codegen/ARCHITECTURE.md documents each group and its migration.

Goa, examples, plugins, and Flows use the remote branch fix/goa-generation-plan. Goa-AI is merged on main, 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.

raphael added 30 commits August 20, 2026 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants