Repository navigation
feat(go-fetch): Go client generator, OpenAPI 3.1 normalization, auth fix - #12
Merged
Merged
Conversation
The parser recorded only a security scheme's name and scopes, so the
templates never saw its type or key location and rendered no auth at all.
Each operation's auth methods now carry the scheme from components, an
operation without its own security takes the document's, and
"security: []" opts out. Scopes carry -last, as the mustache engine has
no {{^-last}}, so typescript-fetch lists them without a trailing comma.
The auth blocks of typescript-fetch and dart-fetch, rendered for the
first time, are indented as the code around them, and dart-fetch sends
an API key that lives in a cookie.
An array model built its items from the resolved schema, so `items: $ref` lost the name of the model it refers to. The items are now built from the reference, as a property's are. typescript-fetch and dart-fetch output is unchanged for $ref items of objects, enums, aliases and arrays.
…fetch go-fetch writes a Go client that imports only the standard library: an API type per tag with a typed, a Raw and a Request method per operation, a Params struct per operation, and models for structs, enums, aliases and oneOf/anyOf unions. Optional and nullable values are told apart (`*T`, `Opt[T]`), errors are a *ResponseError with the declared model decoded when the body has its required properties, auth comes from the Configuration, and urlencoded and multipart bodies follow the OpenAPI encoding defaults (form, exploded; deepObject key by key). Inline objects and unions get types named after their owner, union As methods check a variant's required and pinned properties, and a property every variant pins to its own value is the union's discriminator. The parser hands generators what go-fetch reads and the TypeScript and Dart templates ignore: the properties of inline objects (Vars), the members of inline unions (ComposedSchemas), a property's $ref, and the encoding of form fields. Tags are post-processed in sorted order, so names given across operations are stable. Generated files are formatted with go/format after unused imports are dropped. Sample in samples/go-fetch; options and type mapping in templates/go-fetch/README.md.
A 3.1 spec (FastAPI and pydantic write one) spells nullable as a null member of anyOf/oneOf or a type list with null, a literal as const, and a file as contentMediaType: application/octet-stream. The parser knew none of them: every such property typed as a union with null or any, and a file went out as a text field. A normalize step after loading ports the upstream OpenAPINormalizer defaults: SIMPLIFY_ONEOF_ANYOF (a null member makes the schema nullable; one member left is the schema, a $ref as `allOf: [$ref]`), SIMPLIFY_ONEOF_ANYOF_ENUM (a union of enums is one enum) and the NORMALIZE_31SPEC parts above. Also as upstream: a wrapper of an enum model (`allOf: [$ref]`, nullable) is typed as that model, not as a copy of its values (typescript-fetch `Status | null`, dart-fetch `Status.fromJson`), and a nullable parameter or form field is nullable (`q?: string | null`).
`-p packageName=petshop,withGoMod=true` set packageName to the whole string. One -p may now hold several key=value pairs, as the upstream CLI takes them; a piece without "=" belongs to the value before it, so a value with a comma (`npmDescription=cats, dogs`) still works.
Contributor
Coverage ReportProject coverage is 61.54%. Comparing base ( @@ Coverage Diff @@
## base head +/- ##
Coverage 61.51% 61.54% +0.03%
========================================
Files 37 37 0
Lines 3853 3856 +3
========================================
+ Hits 2370 2373 +3
========================================Generated at 2026-10-25T21:15:29.527Z by coverage |
Found by driving a client generated from a FastAPI backend with every common shape against the live server: - A model with properties and additionalProperties keeps the other keys in an AdditionalProperties map, read and written with the fields. - A 2xx whose body is of another type than the result (a 202 Problem next to a 201 Order) is a *ResponseError with that body in Model; the typed method used to return an empty result without an error. - A nullable result is a pointer, nil for null. - An any result is the text when the response is not JSON (FastAPI writes an empty JSON schema next to application/x-ndjson). - A parameter that refers to an enum model has that type; the parser hands parameters their $ref.
Feature specs for what the go-fetch tests left out: objects and arrays in every path, query, header and cookie style of the OpenAPI serialization table; text/plain, octet-stream and multipart object bodies with the Configuration options; the text of scalar query parameters. Unit tests for alias models, nullable items, bool and float enums, union variants, FooRaw naming and an operation under two tags. Generator coverage (with the internal/gen tests) 92% -> 96%, the generated runtime under the feature tests 79% -> 92%.
This was referenced Oct 25, 2026
Parameters were deduplicated by their camelCase name, so two different parameters became one and the other was never sent: a path item_id and a header Item-Id, a query sort_by and sortBy, a query and a form field, or Kubernetes' proxy path parameter and its path query parameter. A parameter now replaces only one of the same location and wire name (an operation's over its path item's), and a second parameter of one name is renamed itemId2, as upstream's ensureUniqueParams does. typescript-fetch gets path and path2 on those Kubernetes operations, as the upstream generator does.
Found by the e2e FastAPI backend: - A date-time is a DateTime, a time.Time that also reads a time without an offset, as FastAPI sends a naive datetime (a database column without a zone). Such a time stays Naive and goes back without an offset; before, one such value failed the whole call. - An enum whose values are of more than one JSON type (pydantic's Literal["auto", 0, 1], an Enum of 0 and "always") is any: no Go constant type holds them, and a string type sent 0 as "0". - A null among an enum's values (Literal["x", None]) makes the schema nullable, so the field can hold and send null.
A form declared application/x-www-form-urlencoded with a binary field (FastAPI writes a form model with an UploadFile so) sent the File as its fmt text. Only multipart carries a file, so such a form now goes as multipart, as upstream's Go client does.
EnumBaseType counted null as a JSON type of its own, so a nullable enum (enum: [a, null]) became any and lost its constants. null now only makes it nullable; values of two JSON types (1 and true) are any, as a string mixed with another already was.
openapi2conv leaves an empty properties map on every schema, so a definition without a type (Kubernetes' JSON, JSONSchemaPropsOrArray) was read as an object: go-fetch typed it map[string]any and failed to decode any other JSON value, every CRD with an enum among them. Only properties that are there make a typeless schema an object now, as in 3.0.
A format: binary property (3.1 contentMediaType, what pydantic writes for bytes) was a []byte, so it went out base64 and failed to decode any value that is not base64. A JSON string carries such bytes as they are: it is a string now. format: byte stays []byte, base64 on the wire.
Found by schema-driven values through the client: - An array item that refers to a nullable schema is a pointer ([]*App), as a map value already was, and so is a result of a nullable model (*App): null became a zero value. - Another 2xx with an inline object body gets a type of its own (SetRecords207Response) and comes back as a *ResponseError: it was decoded as the result, its other fields dropped. - A model named Blob, the parser's name for binary, is a JSON body, not an io.Reader.
Only a JSON document was recognised as Swagger 2.0, so a YAML one went to the OpenAPI 3 loader and failed with "value of openapi must be a non-empty string". A top-level swagger: "2.x" key marks one in YAML too; it is converted to JSON for kin's Swagger 2.0 model, and the property order is read from the source as before.
FastAPI describes a yield endpoint with 3.2's itemSchema in a 3.1 document: the spec failed validation and go-fetch dropped every item. - The parser accepts 3.2 fields in a 3.1 document, normalizes and marks as used what an itemSchema refers to, and records a streamed response's media type and item as x-stream and x-stream-item; it keeps no return type, so typescript-fetch and dart-fetch are unchanged. - go-fetch returns an iter.Seq2[T, error] of the items as they arrive: JSON values for JSON Lines, a ServerSentEvent[T] per event for text/event-stream, T the contentSchema of its data or the text itself. An inline item gets a type named <Op>Item.
A required body that may be null (GitHub's gists/update) was a value, so
null went out as {}. It is a pointer now, and nil sends null; an optional
body still sends none for nil.
A oneOf/anyOf whose member is nullable (GitHub's expires_at: a nullable date or 0) takes null too, but the union was not nullable: go-fetch dropped an explicit null and typescript-fetch typed it without null.
Open
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.
Adds
go-fetch, a Go client generator in the shape of typescript-fetch whose output imports only the standard library, plus the parser and CLI fixes it surfaced. Every commit builds and passesgo vetandgo test.Changes
*ResponseError;null;DateTime), and enums of mixed types asany;itemSchema, which FastAPI writes foryield) asiter.Seq2[T, error], for JSON Lines and server-sent events.typelists,const,contentMediaTypebinary,nullin an enum;nullis nullable;id,id2), not merged;any;itemSchema, butqueryandadditionalOperationsare still an error.-p a=1,b=2, as upstream does.typescript-fetch and dart-fetch output changes only for security (now rendered), the enum specs (now equal to upstream) and clashing parameter names (
path2on Kubernetes, as upstream).Checks
make test,make lint(0 issues) andmake regress.make e2e(test(e2e): generated clients against a validating mock and a FastAPI backend #13): go-fetch passes 144 of 146 cases against a validating mock and a strict FastAPI backend. The 2 skips are values it cannot represent: integers past int64, and list headers that FastAPI reads differently from the spec.Known gaps
format: uuidis astring.oneOfparameter isjson.RawMessage.oneOf(allOf inheritance) leaves the parent a plain struct.additionalPropertiesinherited throughallOfis dropped.