openapi-chain

Wire comparison with openapi-fetch

An OpenAPI document declares more than types. Parameter style, explode, allowReserved and content, the request body media type and Encoding Objects all decide the bytes a server receives. This page shows what openapi-chain's strict client and openapi-fetch 0.17.0 actually send for the same operation and the same input.

Every row is executed and asserted by test/wire-comparison.test.ts. Run pnpm vitest run test/wire-comparison.test.ts. The test pins openapi-fetch 0.17.0; if an upgrade changes one of its requests, the test fails and this page must be updated.

Why the requests differ

openapi-fetch keeps no runtime copy of the schema. Its generated types describe values, but serialization uses built-in defaults and options that apply to a whole client or request: arrays default to form/explode, objects to deepObject, and bodies to JSON. That design keeps it very small (see bundle sizes), and openapi-fetch documents custom querySerializer, pathSerializer and bodySerializer options for everything else.

openapi-chain's strict client compiles serialization metadata from the same document (CLI or compileOpenAPIMetadata) and applies each parameter's and property's declared rules. The core createClient is schema-free like openapi-fetch, so it cannot infer these styles either; this comparison is about openapi-chain/strict.

Method

  • Each case compiles a one-operation OpenAPI 3.1 document for strict and calls openapi-fetch with the parameters and body its generated types accept. Where an openapi-fetch user would have to add a Content-Type header, the case adds it; otherwise openapi-fetch defaults apply.
  • The expected request follows the OpenAPI 3.1.1 style examples, default styles per location and Encoding Object rules. openapi-chain's request must equal it exactly.
  • Identical means the same bytes. Equivalent means the difference disappears after percent-decoding or HTTP list whitespace normalization, so a conforming server reads the same values. Different means the server receives different data, a different media type, or no data.

Summary: of 20 cases, 1 is identical, 3 are equivalent and 16 are different. Common cases such as scalar parameters, form/explode arrays and JSON object bodies with application/json behave the same in both libraries and are not repeated here.

Query parameters

Declaration and inputOpenAPI request (openapi-chain)openapi-fetch 0.17.0Verdict
color array, default style; ['blue','black','brown']?color=blue&color=black&color=brown?color=blue&color=black&color=brownIdentical
color object, default style (form, explode); {R:100,G:200,B:150}?R=100&G=200&B=150?color[R]=100&color[G]=200&color[B]=150Different
color array, explode: false?color=blue,black,brown?color=blue&color=black&color=brownDifferent
tags array explode: false and ids array default, in one operation?tags=a,b&ids=1&ids=2?tags=a&tags=b&ids=1&ids=2Different
color array, style: pipeDelimited, explode: false?color=blue%7Cblack%7Cbrown?color=blue&color=black&color=brownDifferent
color array, style: spaceDelimited, explode: false?color=blue%20black%20brown?color=blue&color=black&color=brownDifferent
color object, style: deepObject?color%5BR%5D=100&color%5BG%5D=200&color%5BB%5D=150?color[R]=100&color[G]=200&color[B]=150Equivalent
next string, allowReserved: true; '/a/b?c'?next=/a/b?c?next=%2Fa%2Fb%3FcEquivalent
filter with content: application/json; {status:'open',page:2}?filter=%7B%22status%22%3A%22open%22%2C%22page%22%3A2%7D?filter[status]=open&filter[page]=2Different

The mixed row matters most. openapi-fetch's querySerializer: { array: { explode: false } } fixes tags but then sends ids=1,2; its object option is applied to every array in the request. Per-parameter rules need a hand-written serializer function for that operation.

Path parameters

The operation is GET /items/{id}.

Declaration and inputOpenAPI request (openapi-chain)openapi-fetch 0.17.0Verdict
id array, style: label; ['3','4','5']/items/.3,4,5/items/3,4,5Different
id array, style: matrix, explode: true; ['3','4']/items/;id=3;id=4/items/3,4Different
id object, explode: true; {role:'admin',firstName:'Alex'}/items/role=admin,firstName=Alex/items/role,admin,firstName,AlexDifferent

openapi-fetch reads path styles from RFC 6570-like template modifiers such as /items/{.id}. OpenAPI path templates never contain those modifiers and generated paths keys are /items/{id}, so the Parameter Object's style is not applied without a custom pathSerializer.

Headers and cookies

Declaration and inputOpenAPI request (openapi-chain)openapi-fetch 0.17.0Verdict
X-Ids header array; ['3','4','5']x-ids: 3,4,5x-ids: 3, 4, 5Equivalent
X-Filter header object, explode: true; {role:'admin',firstName:'Alex'}x-filter: role=admin,firstName=Alexx-filter: [object Object]Different
session cookie parameter; 'abc'cookie: session=abcNo Cookie headerDifferent

openapi-fetch 0.17.0 accepts params.cookie in its types but does not serialize it; set the Cookie header yourself where the platform allows it. Browsers restrict the Cookie header for both libraries.

Request bodies

Declaration and inputOpenAPI request (openapi-chain)openapi-fetch 0.17.0Verdict
text/plain string 'hello'; openapi-fetch call sets Content-Type: text/plaincontent-type: text/plain, body hellocontent-type: text/plain, body "hello"Different
application/merge-patch+json object {name:null}content-type: application/merge-patch+jsoncontent-type: application/jsonDifferent
application/x-www-form-urlencoded object {name:'Ada Lovelace'}name=Ada+Lovelacecontent-type: application/json, body {"name":"Ada Lovelace"}Different
Form body with tags array and address object, encoding.address.style: deepObject; openapi-fetch call sets the form Content-Typename=Ada&tags=a&tags=b&address%5Bcity%5D=Parisname=Ada&tags=a%2Cb&address=%5Bobject+Object%5DDifferent
multipart/form-data with object metadata, array tags and a PNG file with encoding.file.contentType: image/png; openapi-fetch call sets Content-Type: multipart/form-dataParts: metadata as application/json {"title":"Cat"}, tags=a, tags=b, file as image/pngJSON body {"metadata":{"title":"Cat"},"tags":["a","b"],"file":{}} without a multipart boundaryDifferent

Without a body serializer, openapi-fetch JSON-encodes every non-FormData body and labels it application/json unless the call overrides the header. With application/x-www-form-urlencoded it uses new URLSearchParams(body), which flattens arrays with commas and stringifies nested objects. For multipart it expects the application to build a FormData itself; the Blob in this case is JSON-encoded as {}.

openapi-chain's multipart JSON part is a Blob so it can carry its application/json Content-Type; native FormData therefore gives it the filename blob. See binary bodies and multipart parts for this and other native FormData limits.

Matching these requests with openapi-fetch

Every difference above can be removed in openapi-fetch by writing the serialization yourself:

Differenceopenapi-fetch remedy
One non-default query style for all parameters of a requestquerySerializer: { array: { style, explode } } or { object: { style, explode } } on the client or call
Different styles for different parameters, parameter content, allowReserved for one parameterA querySerializer function for that operation
Path label, matrix or object explodeA pathSerializer function; template modifiers do not exist in generated path keys
Structured header values and cookie parametersSerialize the values and set the headers yourself
Non-JSON media types, form bodies and multipart encodingA bodySerializer that builds the string, URLSearchParams or FormData (with per-part Blobs), plus the matching Content-Type

That is the trade-off: openapi-fetch stays schema-free and small, and each operation whose document declares non-default serialization needs code that restates the document. openapi-chain's strict client derives the same rules from the document at the cost of shipping compiled metadata and a larger runtime (8.6 KB gzip for strict against 2.5 KB for openapi-fetch; see delivery measurements). If you already use openapi-fetch, the openapi-fetch adapter applies openapi-chain's serialization inside your existing client; its tests send all 20 cases above and match the OpenAPI requests.

On this page