Repository navigation
fix(typescript-fetch): fix what the e2e tests find, hold it to strict e2e - #15
Merged
Merged
Conversation
What typescript-fetch got wrong traced back to the parser in places every generator shares: - an array parameter joins its items as its style says (csv, ssv, pipes, one per value for an exploded form) instead of always "multi"; - the base path takes the defaults of the first server's variables; - an enum's string literal escapes line breaks (U+2028 and U+2029 too), and go-fetch reads them back; - a description holding "/*/*" no longer ends its doc comment: the two replacements run one after the other, as upstream's do; - a union with an inline-object member is any, not its other members (string | object read as string); - a nested container in a response imports its innermost model, not "ArrayCell"; - a form field keeps its free-form flag, and an anyOf with inline members says so (x-any-of-inline). dart-fetch gains the same import and enum escape; go-fetch's output is unchanged.
…ember Fixes what the e2e tests and generated clients of the GitHub, Kubernetes and Stripe specs showed. Every client of the feature specs and of the three real specs now compiles with tsc --strict. Values: a recursive converter (convert.go) writes FromJSON and ToJSON: - lists of lists and maps of models or dates, a nested response or body; - a $ref to a union by its model; an inline union by the property every member pins (pydantic's Literal), else by the member it fits best (bestFit: required keys there, most keys declared); scalar unions by a date's text; an anyOf of models as a oneOf; - an optional nullable null stays null; the keys next to the properties (additionalProperties) go through, converted by their schema; a binary property of a JSON model is a string; keys every object inherits (constructor, __proto__) are read and written as own keys; - a discriminator without mapping maps the allOf children by schema name. Requests: path, header, query and cookie parameters go by style and explode (label, matrix, objects, delimiters); cookies and an API key in a cookie as a Cookie header; form fields by their encoding, a list a field or part per item, an object a JSON part; a form with a file as multipart. Responses: a 2xx without a body (a 204 beside a 200, a HEAD) is null; a 2xx of another body than the result's throws ResponseError; a string or any sent as text is the text; JSON Lines and server-sent events (3.2's itemSchema) are an AsyncIterable, of ServerSentEvent for events. Names: a model named like a runtime export (BaseAPI) is prefixed, an operation named like a BaseAPI member (request) suffixed, a model mapped twice imported once, an enum value with a line break escaped. A parsed vendor-extension map is the document's own, shared by every property of one $ref: the generator writes to a copy.
…lues typescript-fetch now passes every scenario it can and every generated value case, as go-fetch does; only dart-fetch stays lenient. The harness hid gaps: tsc compiled without strict and ignored errors, and a FromJSON and a ToJSON that both leave a value alone pass a round trip. - tsc runs strict; a client with a type error fails its own suite, and a client whose JavaScript does not parse no longer stops the run; - the runner checks every argument and result against the client's types: TypeScript property names in a model, a Date where the interface says; it puts back the read-only properties ToJSON leaves out; - typescript-fetch compares date-times as instants to the millisecond, a Content-Type by its media type; its error model is the body only where the spec declares one for the status (`bodies` in the runner input); - streams are read as the list of their items. New and wider feature specs cover what a critic found the scenarios missed: nested containers of models and dates, a discriminator without mapping, oneOf members told apart by value, runtime name clashes, date headers, an integer sent as text, __proto__ and constructor properties. The skips typescript-fetch passes now are gone; seven stay: integers past 2^53, naive date-times, the FastAPI list header and a multipart JSON part (FormData gives a part a type only as a file).
…embers
- A schema that only names an array, a map or a scalar (`Pets: {type:
array, items: Pet}`, `Day: {format: date}`) is a type alias that
converts as its value does, not an empty interface whose FromJSON and
ToJSON leave a body or a result as it came.
- A oneOf member that is an allOf of models is their intersection
("A & B"), converted by each and merged, not its first model: an object
typed `type: object` over an allOf is typed so too, and a string model
of format date or date-time says so (IsDate, IsDateTime).
- A discriminator parent writes a value no mapping names as itself, not
as it came.
A feature spec of an array, a map and a date alias as a body and a result, and the discriminator parent sent as itself. The TypeScript runner writes a result with its model's ToJSON before taking it for a plain Date.
Two places a typescript-fetch bug passed the e2e: - A day (format: date) parsed as UTC midnight is the right day in Europe/Bratislava, where the runners run, and the day before west of UTC. The runner now requires a decoded day at local midnight, and makes a day argument with the client's parseDate, as its users do, not as UTC midnight. - A generated case checked the body only. Its query and headers must now hold the required parameters' values and none of the optional ones (headers an HTTP client sends itself aside), so an empty `?q=` fails. Both were proven with a broken template: 759 and 145 cases fail now.
- An inline union as a request body converts as one in a property does: the parameter now carries its members to the converter. - bestFit picks a list member too, by all its items: `Array<SimpleUser> | Array<Stargazer>` converts each item. - A union with an inline object is typed any; it now also stays as it came, not converted by its one model member.
From the round-2 harness critic, each shown by a mutation the e2e passed: - Union members with snake_case names (composition-oneof-inline): with the same names in JSON and TypeScript a union body sent unconverted looked right. The runner now builds a union argument from the member whose JSON names hold the value's keys, and writes a union result with the member whose type the value fits; it also reads `T | null | undefined ` as T. - An empty header and cookie must still be sent. - Server-sent events with CRLF line ends, an id that stays across events. - A generated case is named after its operation.
…d every option - An object parameter or form field sent its unset optional properties as the text "undefined" (`g=a,1,b,undefined`, `h[b]=undefined`, a header, an urlencoded field): styleParam, formFields and querystring now leave them out. - A `format: date` request body went out as a UTC date-time: the body parameter now carries the date flags, as every other parameter does. - With useSingleRequestParameter=false, required arguments come first (an optional one before a required one does not compile), and the method keeps no local variable a parameter could be named like. - With withoutRuntimeChecks, a model or alias imports the types it names and a oneOf model no functions its members do not have.
- TestTypeScriptOptions generates every feature spec with each boolean option of typescript-fetch flipped and compiles them strict; two options produced clients that did not compile. - The color of parameters-object-styles and the filter of parameters-deepobject gain an optional property the cases leave unset. - dates gains a request body that is a day.
…, read binary any - An inline union discriminates by a tag every member pins to values of its own, an int Literal or a Literal of several values too: before, only one string value counted, and the members, alike by their keys, converted as the first (a day sent as a date-time, 422). - A union model compares its discriminator as text, so an int tag matches its mapping (`case "1"` did not compile against `1 | 2`), and a member writes and reads its own tag instead of the mapping's text. - An any result the server sends as an image or as octets is a file, not text that loses its bytes (FastAPI's documented binary response).
An any result decoded as text whatever its media type, so the bytes of FastAPI's documented binary response (`image/png` without a schema) came back changed. Text media types still decode as text.
…dies From round 3 of the typescript-fetch gauntlet: - The FastAPI backend gains unions tagged by an int and by a Literal of several values, as a body, a result and a schema of their own, and a binary response without a schema. - request-body-other-media gains a merge-patch+json body answered with problem+json: no +json media type was exercised. - A server-sent event after a named one must carry no name. - The TypeScript runner checks an enum holds one of its values, and picks a union member for an argument by its keys and its tags.
From the final review of the gauntlet fixes: - A oneOf with a discriminator sets the tag on its member again, the member's own value first: a member that does not declare the tag lost it both ways once the mapping's text stopped being forced. - An alias of a list of models as a union member is a list for bestFit; another alias (a scalar, a map) is left out, not called on an object (`json.map is not a function`). - A $ref to a union model is typed as that model. - An int tag of a million or more matches its mapping (1000000, not 1e+06); a media type is text by its subtype (`xlsx` is no xml text).
An xlsx download (`...spreadsheetml.sheet`) holds "xml" in its media type and read as text; an any result is text only for text/* or a json, xml, yaml, csv, javascript or form subtype.
- composition-oneof-scalars gains a union of a list alias and a model and a discriminated union whose members do not declare the tag. - A generated case expects a string parameter as it is (not JSON-escaped) and the properties of an optional exploded query object absent. - The TypeScript runner lets a union's tag stand on its member.
Contributor
Coverage ReportProject coverage is 59.47%. Comparing base ( @@ Coverage Diff @@
## base head +/- ##
Coverage 59.50% 59.47% -0.03%
========================================
Files 38 38 0
Lines 4244 4246 +2
========================================
Hits 2525 2525 0
========================================Generated at 2026-10-26T19:51:58.677Z by coverage |
…body An empty body read as null for every response broke clients that rely on it throwing. The xxxRaw method now returns null by status: a 2xx the spec declares without a body, every 2xx of a HEAD. Any other empty body stays a JSON parse error, and the body is no longer read twice.
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.
Fixes the typescript-fetch bugs the end-to-end tests found and holds typescript-fetch to the same bar as go-fetch: every scenario it can pass and every generated value case, exactly, with the client compiled
tsc --strict.What was wrong, now fixed
Values (FromJSON / ToJSON). A recursive converter (
internal/generator/typescript/convert.go) replaces the per-shape template branches:format: datevalues (as local days) at any depth;bestFit, lists too); a$refunion by its model; an allOf member as an intersection; an anyOf of models as a oneOf;type Pets = Array<Pet>,type Day = Date);nullstaysnull; keys next to declared properties (additionalProperties) go through; a binary property of a JSON model is a string;__proto__/constructorkeys are read and written as own keys.Requests. Parameters by
styleandexplode(label, matrix, delimiters, objects in path/header/query, unset properties left out); cookies and API keys in aCookieheader; form fields by theirencoding(a list a field or part per item, an object a JSON part, a form with a file as multipart); a date body as a day.Responses. A 2xx the spec declares without a body (204 beside 200, HEAD) is
null, decided by status; any other empty body stays a parse error; a 2xx of another body than the result's throwsResponseError; ananysent as text is text, as an image or octets a file; JSON Lines and server-sent events (3.2itemSchema) areAsyncIterable(ServerSentEvent<T>for events).Compiles. Every client of the feature specs and of the GitHub, Kubernetes and Stripe specs builds strict, also with each boolean option flipped (
withoutRuntimeChecks,useSingleRequestParameter=false, ...). Name clashes with the runtime (BaseAPI, an operationrequest) are renamed.Parser fixes shared by the generators: array params joined by style, server variable defaults in the base path, enum line breaks escaped,
/*/*in a description no longer ends its comment, nested container imports, allOf members of a oneOf. go-fetch also reads ananysent as an image as bytes; its output is otherwise unchanged.Behaviour changes for existing typescript-fetch users
ResponseError;null(the type says| null);Modelprefix (ApiResponse→ModelApiResponse, as upstream); an operationrequestbecomesrequestOperation; a propertyconstructoris_constructor;typealiases instead of empty interfaces.The e2e harness, stricter
tsc --strict; a client with a type error fails its own suite;Dates, days at local midnight, enum values), builds union arguments and days as users do, restores read-only properties;TestTypeScriptOptionscompiles every feature spec with each option flipped;fails: true), not read asnull;+jsonbodies, CRLF event streams, binary responses.Optional real-spec run (
E2E_SPECS): typescript-fetch GitHub 11,633 / 22, Kubernetes 2,239 / 0, Stripe 2,662 / 6; go-fetch 0 failures on all three.Left as they are
Integers past 2^53, naive date-times (a
Datekeeps no missing offset), FastAPI's repeated list headers, a multipart JSON part (FormData sends it as a file namedblob), decoded error models. The remaining real-spec cases are union members whose keys tell them apart only partly.