Repository navigation
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.
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%.
`make e2e` checks what a generated client puts on the wire and how it decodes the answer, for go-fetch, typescript-fetch and dart-fetch alike. The cases are language-neutral scenarios (testdata/e2e/scenarios) in the spec's own names and wire values: - a mock layer: a server built on kin-openapi validates each request and each scenario response against the feature spec and records the request, so a case checks the exact path, query, headers, cookies and body; - a backend layer: a strict FastAPI app (pydantic, uv-locked) whose served OpenAPI 3.1 spec is snapshotted, called through a client generated from it, echoing what it parsed. A runner per generator turns the scenarios into calls of its client the way its users write them (Go by reflection, TypeScript from the compiled sources, Dart with mirrors) and prints one JSON line per case. A gap of a generator is a skip with its reason on the case; a skip that starts passing fails. go-fetch passes every case; typescript-fetch skips 29 and dart-fetch 103, mostly clients that do not compile. The cookie array of parameters-object-styles is explode: false: the OpenAPI serialization table defines no exploded form for a cookie.
Six rounds of looking for realistic FastAPI and pydantic shapes a generated client gets wrong on the wire, each added to the backend with calls that show it (61 -> 81 cases): - discriminated unions as the body, a nullable field, map values and list items, behind a camelCase key, and a callable discriminator; - naive and aware date-times, times with offsets, negative durations, dates in lists, maps and query arrays; runners now run in a non-UTC zone (TZ=Europe/Bratislava); - required nullables and null against a non-null default, with the keys the client sent echoed back; - a typed form model; multipart labels echo joined by "|" so one part "x,y" no longer passes for two; - parameters whose names collide once camelCased, under a router prefix and a shared dependency, and ones named like a client's locals; - enums and Literals of mixed JSON types and with null. The gaps they found are skips with their reasons: go-fetch cannot hold a naive date-time or an enum without a type; typescript-fetch sends a date query array as Date.toString(), a naive date-time in UTC, an explicit null of an optional nullable as absent, and does not compile a union form field; the parser merges parameters of one camelCase name.
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. The e2e runners number a repeated identifier the same way, and the FastAPI name-clash cases now pass for go-fetch and typescript-fetch.
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.
Contributor
Coverage ReportProject coverage is 59.18%. Commit Note No cached baseline for go. Per-file deltas and the diff table will appear once the target branch has coverage cached. Push to the base branch or trigger the workflow manually to seed the cache. Generated at 2026-10-25T19:35:59.373Z by coverage |
Owner
Author
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.
Fixes the generator bugs that the FastAPI e2e backend from #13 found, and adds the cases that found them. After this PR, every go-fetch case passes except two that it cannot pass.
Fixes
item_idin the path anditemIdin a header used to end up as one parameter, and one of the two values was never sent. The second one is now numbered (itemId2), as upstream does. This affects every generator: typescript-fetch on Kubernetes now getspath/path2, which matches upstream.datetime) could not be decoded or sent. The newDateTime{time.Time; Naive bool}round-trips both forms.Literal["auto", 0]was a Go string, so the number could not be sent or decoded. It is nowany.nullamong enum values makes the schema nullable. pydantic writesLiteral["x", None]this way. This also fixes typescript-fetch, which used to drop an explicitnullof such an enum.enum: [a, null]) is still a typed enum with its constants, notany.UploadFileasapplication/x-www-form-urlencoded. go-fetch sent the file as its printed Go struct. Only multipart can carry a file, so such a form now goes as multipart, as upstream's Go client does.New e2e cases (FastAPI backend)
5e-324and1e+308pass everywhere. Integers past int64 are a skip for go-fetch (int) and for typescript-fetch (precision is lost past 2^53).URLSearchParamsholding a Blob.Status (FastAPI scenario)
The typescript-fetch and dart-fetch gaps are still recorded as skips with their reasons. They are not fixed here.
Checks
make testandmake lint(0 issues) pass, and so doesmake e2e.make regress: only go-fetch output changes. typescript-fetch and dart-fetch output for the feature specs is unchanged, except Kubernetes params in TS (path2).