Skip to content

Commit 21fb33c

Browse files
authored
Merge pull request #1534 from drwpow/query-serializers
Improve query + path serialization
2 parents 61f408a + 119260b commit 21fb33c

18 files changed

+1459
-427
lines changed

.changeset/lovely-needles-try.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": patch
3+
---
4+
5+
Add support for automatic label & matrix path serialization.

.changeset/shiny-trees-eat.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": patch
3+
---
4+
5+
Remove leading question marks from querySerializer

.changeset/spicy-kings-wait.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": minor
3+
---
4+
5+
⚠️ Breaking change: no longer supports deeply-nested objects/arrays for query & path serialization.

docs/data/contributors.json

+1-1
Large diffs are not rendered by default.

docs/openapi-fetch/api.md

+96-29
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ description: openapi-fetch API
55

66
# API
77

8-
## Create Client
8+
## createClient
99

1010
**createClient** accepts the following options, which set the default settings for all subsequent fetch calls.
1111

1212
```ts
1313
createClient<paths>(options);
1414
```
1515

16-
| Name | Type | Description |
17-
| :---------------- | :-------------: | :-------------------------------------------------------------------------------------------------------------------------------------- |
18-
| `baseUrl` | `string` | Prefix all fetch URLs with this option (e.g. `"https://myapi.dev/v1/"`) |
19-
| `fetch` | `fetch` | Fetch instance used for requests (default: `globalThis.fetch`) |
16+
| Name | Type | Description |
17+
| :---------------- | :-------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
18+
| `baseUrl` | `string` | Prefix all fetch URLs with this option (e.g. `"https://myapi.dev/v1/"`) |
19+
| `fetch` | `fetch` | Fetch instance used for requests (default: `globalThis.fetch`) |
2020
| `querySerializer` | QuerySerializer | (optional) Provide a [querySerializer](#queryserializer) |
2121
| `bodySerializer` | BodySerializer | (optional) Provide a [bodySerializer](#bodyserializer) |
2222
| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal` …) ([docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options) |
@@ -29,44 +29,98 @@ The following options apply to all request methods (`.GET()`, `.POST()`, etc.)
2929
client.get("/my-url", options);
3030
```
3131

32-
| Name | Type | Description |
33-
| :---------------- | :---------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34-
| `params` | ParamsObject | [path](https://swagger.io/specification/#parameter-locations) and [query](https://swagger.io/specification/#parameter-locations) params for the endpoint |
35-
| `body` | `{ [name]:value }` | [requestBody](https://spec.openapis.org/oas/latest.html#request-body-object) data for the endpoint |
36-
| `querySerializer` | QuerySerializer | (optional) Provide a [querySerializer](#queryserializer) |
37-
| `bodySerializer` | BodySerializer | (optional) Provide a [bodySerializer](#bodyserializer) |
32+
| Name | Type | Description |
33+
| :---------------- | :---------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34+
| `params` | ParamsObject | [path](https://swagger.io/specification/#parameter-locations) and [query](https://swagger.io/specification/#parameter-locations) params for the endpoint |
35+
| `body` | `{ [name]:value }` | [requestBody](https://spec.openapis.org/oas/latest.html#request-body-object) data for the endpoint |
36+
| `querySerializer` | QuerySerializer | (optional) Provide a [querySerializer](#queryserializer) |
37+
| `bodySerializer` | BodySerializer | (optional) Provide a [bodySerializer](#bodyserializer) |
3838
| `parseAs` | `"json"` \| `"text"` \| `"arrayBuffer"` \| `"blob"` \| `"stream"` | (optional) Parse the response using [a built-in instance method](https://developer.mozilla.org/en-US/docs/Web/API/Response#instance_methods) (default: `"json"`). `"stream"` skips parsing altogether and returns the raw stream. |
39-
| `fetch` | `fetch` | Fetch instance used for requests (default: fetch from `createClient`) |
39+
| `fetch` | `fetch` | Fetch instance used for requests (default: fetch from `createClient`) |
4040
| (Fetch options) | | Any valid fetch option (`headers`, `mode`, `cache`, `signal`, …) ([docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch#options)) |
4141

42-
### querySerializer
42+
## querySerializer
4343

44-
By default, this library serializes query parameters using `style: form` and `explode: true` [according to the OpenAPI specification](https://swagger.io/docs/specification/serialization/#query). To change the default behavior, you can supply your own `querySerializer()` function either on the root `createClient()` as well as optionally on an individual request. This is useful if your backend expects modifications like the addition of `[]` for array params:
44+
OpenAPI supports [different ways of serializing objects and arrays](https://swagger.io/docs/specification/serialization/#query) for parameters (strings, numbers, and booleans—primitives—always behave the same way). By default, this library serializes arrays using `style: "form", explode: true`, and objects using `style: "deepObject", explode: true`, but you can customize that behavior with the `querySerializer` option (either on `createClient()` to control every request, or on individual requests for just one).
45+
46+
### Object syntax
47+
48+
openapi-fetch ships the common serialization methods out-of-the-box:
49+
50+
| Option | Type | Description |
51+
| :-------------- | :---------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52+
| `array` | SerializerOptions | Set `style` and `explode` for arrays ([docs](https://swagger.io/docs/specification/serialization/#query)). Default: `{ style: "form", explode: true }`. |
53+
| `object` | SerializerOptions | Set `style` and `explode` for objects ([docs](https://swagger.io/docs/specification/serialization/#query)). Default: `{ style: "deepObject", explode: true }`. |
54+
| `allowReserved` | `boolean` | Set to `true` to skip URL encoding (⚠️ may break the request) ([docs](https://swagger.io/docs/specification/serialization/#query)). Default: `false`. |
4555

4656
```ts
47-
const { data, error } = await GET("/search", {
48-
params: {
49-
query: { tags: ["food", "california", "healthy"] },
57+
const client = createClient({
58+
querySerializer: {
59+
array: {
60+
style: "pipeDelimited", // "form" (default) | "spaceDelimited" | "pipeDelimited"
61+
explode: true,
62+
},
63+
object: {
64+
style: "form", // "form" | "deepObject" (default)
65+
explode: true,
66+
},
5067
},
51-
querySerializer(q) {
52-
let s = [];
53-
for (const [k, v] of Object.entries(q)) {
54-
if (Array.isArray(v)) {
55-
for (const i of v) {
56-
s.push(`${k}[]=${i}`);
68+
});
69+
```
70+
71+
#### Array styles
72+
73+
| Style | Array `id = [3, 4, 5]` |
74+
| :--------------------------- | :---------------------- |
75+
| form | `/users?id=3,4,5` |
76+
| **form (exploded, default)** | `/users?id=3&id=4&id=5` |
77+
| spaceDelimited | `/users?id=3%204%205` |
78+
| spaceDelimited (exploded) | `/users?id=3&id=4&id=5` |
79+
| pipeDelimited | `/users?id=3\|4\|5` |
80+
| pipeDelimited (exploded) | `/users?id=3&id=4&id=5` |
81+
82+
#### Object styles
83+
84+
| Style | Object `id = {"role": "admin", "firstName": "Alex"}` |
85+
| :----------------------- | :--------------------------------------------------- |
86+
| form | `/users?id=role,admin,firstName,Alex` |
87+
| form (exploded) | `/users?role=admin&firstName=Alex` |
88+
| **deepObject (default)** | `/users?id[role]=admin&id[firstName]=Alex` |
89+
90+
> [!NOTE]
91+
>
92+
> **deepObject** is always exploded, so it doesn’t matter if you set `explode: true` or `explode: false`—it’ll generate the same output.
93+
94+
### Alternate function syntax
95+
96+
Sometimes your backend doesn’t use one of the standard serialization methods, in which case you can pass a function to `querySerializer` to serialize the entire string yourself. You’ll also need to use this if you’re handling deeply-nested objects and arrays in your params:
97+
98+
```ts
99+
const client = createClient({
100+
querySerializer(queryParams) {
101+
const search = [];
102+
for (const name in queryParams) {
103+
const value = queryParams[name];
104+
if (Array.isArray(value)) {
105+
for (const item of value) {
106+
s.push(`${name}[]=${encodeURIComponent(item)}`);
57107
}
58108
} else {
59-
s.push(`${k}=${v}`);
109+
s.push(`${name}=${encodeURLComponent(value)}`);
60110
}
61111
}
62-
return s.join("&"); // ?tags[]=food&tags[]=california&tags[]=healthy
112+
return search.join(","); // ?tags[]=food,tags[]=california,tags[]=healthy
63113
},
64114
});
65115
```
66116

67-
### bodySerializer
117+
> [!WARNING]
118+
>
119+
> When serializing yourself, the string will be kept exactly as-authored, so you’ll have to call [encodeURI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) or [encodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) to escape special characters.
68120
69-
Similar to [querySerializer](#querySerializer), bodySerializer allows you to customize how the requestBody is serialized if you don’t want the default [JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) behavior. You probably only need this when using `multipart/form-data`:
121+
## bodySerializer
122+
123+
Similar to [querySerializer](#queryserializer), bodySerializer allows you to customize how the requestBody is serialized if you don’t want the default [JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) behavior. You probably only need this when using `multipart/form-data`:
70124

71125
```ts
72126
const { data, error } = await PUT("/submit", {
@@ -76,10 +130,23 @@ const { data, error } = await PUT("/submit", {
76130
},
77131
bodySerializer(body) {
78132
const fd = new FormData();
79-
for (const [k, v] of Object.entries(body)) {
80-
fd.append(k, v);
133+
for (const name in body) {
134+
fd.append(name, body[name]);
81135
}
82136
return fd;
83137
},
84138
});
85139
```
140+
141+
## Path serialization
142+
143+
openapi-fetch supports path serialization as [outlined in the 3.1 spec](https://swagger.io/docs/specification/serialization/#path). This happens automatically, based on the specific format in your OpenAPI schema:
144+
145+
| Template | Style | Primitive `id = 5` | Array `id = [3, 4, 5]` | Object `id = {"role": "admin", "firstName": "Alex"}` |
146+
| :---------------- | :------------------- | :----------------- | :----------------------- | :--------------------------------------------------- |
147+
| **`/users/{id}`** | **simple (default)** | **`/users/5`** | **`/users/3,4,5`** | **`/users/role,admin,firstName,Alex`** |
148+
| `/users/{id*}` | simple (exploded) | `/users/5` | `/users/3,4,5` | `/users/role=admin,firstName=Alex` |
149+
| `/users/{.id}` | label | `/users/.5` | `/users/.3,4,5` | `/users/.role,admin,firstName,Alex` |
150+
| `/users/{.id*}` | label (exploded) | `/users/.5` | `/users/.3.4.5` | `/users/.role=admin.firstName=Alex` |
151+
| `/users/{;id}` | matrix | `/users/;id=5` | `/users/;id=3,4,5` | `/users/;id=role,admin,firstName,Alex` |
152+
| `/users/{;id*}` | matrix (exploded) | `/users/;id=5` | `/users/;id=3;id=4;id=5` | `/users/;role=admin;firstName=Alex` |

docs/openapi-fetch/examples.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,8 @@ _Note: if you’re using Svelte without SvelteKit, the root example in `src/rout
130130

131131
### Vue
132132

133-
TODO
133+
There isn’t an example app in Vue yet. Are you using it in Vue? Please [open a PR to add it!](https://github.com/drwpow/openapi-typescript/pulls)
134+
135+
---
136+
137+
Additional examples are always welcome! Please [open a PR](https://github.com/drwpow/openapi-typescript/pulls) with your examples.

docs/openapi-fetch/index.md

+10-8
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ title: openapi-fetch
44

55
<img src="/assets/openapi-fetch.svg" alt="openapi-fetch" width="216" height="40" />
66

7-
openapi-fetch is a typesafe fetch client that pulls in your OpenAPI schema. Weighs **2 kb** and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS.
7+
openapi-fetch is a typesafe fetch client that pulls in your OpenAPI schema. Weighs **4 kb** and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS.
88

99
| Library | Size (min) | “GET” request |
1010
| :------------------------- | ---------: | :------------------------- |
11-
| openapi-fetch | `2 kB` | `200k` ops/s (fastest) |
12-
| openapi-typescript-fetch | `4 kB` | `100k` ops/s (2× slower) |
13-
| axios | `32 kB` | `165k` ops/s (1.2× slower) |
14-
| superagent | `55 kB` | `50k` ops/s (6.6× slower) |
15-
| openapi-typescript-codegen | `367 kB` | `75k` ops/s (2.6× slower) |
11+
| openapi-fetch | `4 kB` | `278k` ops/s (fastest) |
12+
| openapi-typescript-fetch | `4 kB` | `130k` ops/s (2.1× slower) |
13+
| axios | `32 kB` | `217k` ops/s (1.3× slower) |
14+
| superagent | `55 kB` | `63k` ops/s (4.4× slower) |
15+
| openapi-typescript-codegen | `367 kB` | `106k` ops/s (2.6× slower) |
1616

17-
The syntax is inspired by popular libraries like react-query or Apollo client, but without all the bells and whistles and in a 2 kb package.
17+
The syntax is inspired by popular libraries like react-query or Apollo client, but without all the bells and whistles and in a 4 kb package.
1818

1919
```ts
2020
import createClient from "openapi-fetch";
@@ -49,7 +49,7 @@ Notice there are no generics, and no manual typing. Your endpoint’s request an
4949
- ✅ No manual typing of your API
5050
- ✅ Eliminates `any` types that hide bugs
5151
- ✅ Also eliminates `as` type overrides that can also hide bugs
52-
- ✅ All of this in a **2 kB** client package 🎉
52+
- ✅ All of this in a **4 kb** client package 🎉
5353

5454
## Setup
5555

@@ -139,6 +139,8 @@ openapi-fetch infers types from the URL. Prefer static string values over dynami
139139

140140
:::
141141

142+
This library also supports the **label** and **matrix** serialization styles as well ([docs](https://swagger.io/docs/specification/serialization/#path)) automatically.
143+
142144
### Request
143145

144146
The `GET()` request shown needed the `params` object that groups [parameters by type](https://spec.openapis.org/oas/latest.html#parameter-object) (`path` or `query`). If a required param is missing, or the wrong type, a type error will be thrown.

docs/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
"update-contributors": "node scripts/update-contributors.js"
1111
},
1212
"devDependencies": {
13-
"vitepress": "1.0.0-rc.40"
13+
"vitepress": "1.0.0-rc.42"
1414
}
1515
}

0 commit comments

Comments
 (0)