Skip to content

Nullable fix #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
May 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4cbabeb
Update schema-object.ts
jawnothin May 4, 2025
6baf966
chore(deps): update dependency msw to v2.7.6 (#2287)
renovate[bot] May 5, 2025
c63f3f3
chore(deps): update dependency @types/node to v22.15.10 (#2293)
renovate[bot] May 6, 2025
6fe84c2
chore(deps): update dependency @shikijs/vitepress-twoslash to v3.4.0 …
renovate[bot] May 7, 2025
5f1f770
chore(deps): update dependency @arethetypeswrong/cli to ^0.18.0 (#2272)
renovate[bot] May 8, 2025
9889b3b
chore(deps): update dependency execa to v9.5.3 (#2303)
renovate[bot] May 9, 2025
a041c2d
fix(metadata): fix thunk type detection (#2125)
kerwanp May 10, 2025
d79efae
Don't remove `null` type if a default is present. (#2145)
luhn May 10, 2025
8ffc045
[ci] release (#2307)
openapi-ts-bot May 10, 2025
80ef2b3
chore(deps): update dependency @types/react to v18.3.21 (#2218)
renovate[bot] May 10, 2025
419d9ac
fix(openapi-fetch): fix overriding baseUrl per request without overri…
Rendez May 10, 2025
67889ba
Support $ref into `paths` (#2185)
duncanbeevers May 10, 2025
694522a
React query handle 204 or zero content length (#2235)
wheelebin May 10, 2025
294143e
chore(deps): update dependency @arethetypeswrong/cli to v0.18.1 (#2306)
renovate[bot] May 10, 2025
8cb1347
chore(deps): update dependency msw to v2.8.2 (#2304)
renovate[bot] May 10, 2025
5fb44a2
Update Speakeasy URL (#2302)
ndimares May 10, 2025
068df6e
chore(deps): update dependency @tanstack/react-query to v5.75.7 (#2301)
renovate[bot] May 10, 2025
03f678c
chore(deps): update dependency lint-staged to v15.5.2 (#2297)
renovate[bot] May 10, 2025
5848759
feat(swr-openapi): add custom error types to query builder (#2147)
SSlime-s May 10, 2025
63b715e
chore(deps): update vitest monorepo to v3 (#2284)
renovate[bot] May 10, 2025
0058128
Encode the request body if Content-Type set (#2096)
obulat May 10, 2025
81c031d
Improve header handling (#2308)
drwpow May 10, 2025
2264d1d
[ci] release (#2309)
openapi-ts-bot May 10, 2025
fb385a3
chore: update openapi-fetch test fixture (#2313)
drwpow May 10, 2025
e66b5ce
Build packages with unbuild to improve CJS support (#2310)
drwpow May 10, 2025
e65c824
[ci] release (#2314)
openapi-ts-bot May 10, 2025
d309753
Update code of conduct source (#2316)
emmanuel-ferdman May 12, 2025
8e5c20c
Merge branch 'openapi-ts:main' into nullable-fix
jawnothin May 17, 2025
5471fed
Merge branch 'main' into nullable-fix
jawnothin May 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Ultra-fast fetching for TypeScript generated automatically from your OpenAPI sch

<p align="center">
<a href="https://zuplo.link/openapi-ts-gh"><img width="288" height="120" alt="Zuplo" src="./docs/public/assets/zuplo.svg"></a>
<a href="https://www.speakeasy.com/product/sdk-generation?utm_source=pow_openapi_ts" target="_blank"><img width="288" height="120" src="./docs/public/assets/speakeasy.svg" /></a>
<a href="https://www.speakeasy.com/editor?utm_source=pow_openapi_ts" target="_blank"><img width="288" height="120" src="./docs/public/assets/speakeasy.svg" /></a>
</p>

### 🥈 Silver Sponsors
Expand Down
2 changes: 1 addition & 1 deletion docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ All of the above are welcome! But if you just opened a PR without discussing the

## Code of Conduct

All docs edits are held accountable to the [Code of Conduct](../../CODE_OF_CONDUCT.md) outlined in this project. Language in the documentation must be inclusive, kind, and respectful.
All docs edits are held accountable to the [Code of Conduct](../CODE_OF_CONDUCT.md) outlined in this project. Language in the documentation must be inclusive, kind, and respectful.
16 changes: 16 additions & 0 deletions docs/openapi-fetch/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,22 @@ or when instantiating the client.

:::

### URL-encoded body

To send a body request in `application/x-www-form-urlencoded` format, which is commonly used to transmit key-value pairs in APIs like OAuth 2.0, pass the appropriate header and body as an object. `openapi-fetch` will automatically encode the body to the correct format.

```ts
const { data, error } = await client.POST("/tokens", {
body: {
clientId: "someClientId",
clientSecret: "someClientSecret",
},
headers: {
"Content-Type": "application/x-www-form-encoded",
},
});
```

## Path serialization

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:
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,23 @@
"version": "pnpm run build && changeset version && pnpm i"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.1",
"@biomejs/biome": "^1.9.4",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.29.2",
"@playwright/test": "^1.52.0",
"@size-limit/preset-small-lib": "^11.2.0",
"@types/node": "^22.15.3",
"del-cli": "^6.0.0",
"prettier": "^3.5.3",
"size-limit": "^11.2.0",
"turbo": "^2.5.2",
"typescript": "^5.8.3",
"vitest": "^2.1.9"
"unbuild": "^3.5.0",
"vitest": "^3.1.3"
},
"size-limit": [
{
"path": "packages/openapi-fetch/dist/index.min.js",
"path": "packages/openapi-fetch/dist/index.mjs",
"limit": "7 kB",
"brotli": false
}
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-fetch/.npmignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.turbo
*.config.*
examples
test
test-results
Expand Down
14 changes: 14 additions & 0 deletions packages/openapi-fetch/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# openapi-fetch

## 0.14.0

### Minor Changes

- [#2310](https://github.com/openapi-ts/openapi-typescript/pull/2310) [`e66b5ce`](https://github.com/openapi-ts/openapi-typescript/commit/e66b5ce63bfcdc57c6ee942e5ed4e7667e64c290) Thanks [@drwpow](https://github.com/drwpow)! - Build package with unbuild. Also remove the minified version (openapi-fetch is only useful in a TypeScript/bundler environment, so there’s no sense in loading it from a CDN clientside).

## 0.13.8

### Patch Changes

- [#2096](https://github.com/openapi-ts/openapi-typescript/pull/2096) [`0058128`](https://github.com/openapi-ts/openapi-typescript/commit/0058128afa258180793ae2071022d89a688d8ddc) Thanks [@obulat](https://github.com/obulat)! - Encode the request body if `Content-Type` header is `x-www-form-urlencoded`
- [#2157](https://github.com/openapi-ts/openapi-typescript/pull/2157) [`419d9ac`](https://github.com/openapi-ts/openapi-typescript/commit/419d9ac6851cf780b81060ddc05c231c5225a17b) Thanks [@Rendez](https://github.com/Rendez)! - Fix overriding baseUrl per request without overriding default baseUrl
- [#2308](https://github.com/openapi-ts/openapi-typescript/pull/2308) [`81c031d`](https://github.com/openapi-ts/openapi-typescript/commit/81c031da8584ed49b033ebfc67bbb3e1ca258699) Thanks [@drwpow](https://github.com/drwpow)! - Improve header handling

## 0.13.7

### Patch Changes
Expand Down
12 changes: 12 additions & 0 deletions packages/openapi-fetch/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineBuildConfig } from "unbuild";

export default defineBuildConfig({
entries: ["./src/index.js"],
declaration: "compatible",
clean: true,
sourcemap: true,
rollup: {
// Ship CommonJS-compatible bundle
emitCJS: true,
},
});
1 change: 0 additions & 1 deletion packages/openapi-fetch/examples/vue-3/tsconfig.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",

"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
Expand Down
36 changes: 13 additions & 23 deletions packages/openapi-fetch/package.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
{
"name": "@augwa/openapi-fetch",
"description": "Fast, type-safe fetch client for your OpenAPI schema. Only 6 kb (min). Works with React, Vue, Svelte, or vanilla JS.",
"version": "0.13.7",
"version": "0.14.0",
"author": {
"name": "Drew Powers",
"email": "[email protected]"
},
"license": "MIT",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./dist/index.mjs",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
}
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.mjs"
},
"./*": "./*"
},
Expand All @@ -47,19 +40,18 @@
"svelte"
],
"scripts": {
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:js-min && pnpm run build:cjs",
"build:clean": "del-cli dist",
"build:js": "mkdir -p dist && cp src/* dist",
"build:js-min": "esbuild --bundle src/index.js --format=esm --minify --outfile=dist/index.min.js && cp dist/index.d.ts dist/index.min.d.ts",
"build:cjs": "esbuild --bundle src/index.js --format=cjs --outfile=dist/cjs/index.cjs && cp dist/index.d.ts dist/cjs/index.d.cts",
"build": "unbuild",
"format": "biome format . --write",
"lint": "biome check .",
"lint": "pnpm run lint:js && pnpm run lint:ts && pnpm run lint:ts-no-strict",
"lint:js": "biome check .",
"lint:ts": "tsc --noEmit",
"lint:ts-no-strict": "tsc --noEmit -p test/no-strict-null-checks/tsconfig.json",
"generate-types": "openapi-typescript -c test/redocly.yaml",
"prepack": "pnpm run build",
"pretest": "pnpm run generate-types",
"test": "pnpm run \"/^test:/\"",
"test": "pnpm run test:js && pnpm run test:exports",
"test:js": "vitest run",
"test:ts": "tsc --noEmit",
"test:ts-no-strict": "tsc --noEmit -p test/no-strict-null-checks/tsconfig.json",
"test:exports": "pnpm run build && attw --pack .",
"test-e2e": "playwright test",
"bench:js": "vitest bench",
"e2e-vite-build": "vite build test/e2e/app",
Expand All @@ -71,8 +63,6 @@
},
"devDependencies": {
"axios": "^1.9.0",
"del-cli": "^6.0.0",
"esbuild": "^0.25.3",
"execa": "^9.5.2",
"express": "^5.0.0",
"feature-fetch": "^0.0.43",
Expand Down
49 changes: 38 additions & 11 deletions packages/openapi-fetch/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ export default function createClient(clientOptions) {
body,
...init
} = fetchOptions || {};
let finalBaseUrl = baseUrl;
if (localBaseUrl) {
baseUrl = removeTrailingSlash(localBaseUrl);
finalBaseUrl = removeTrailingSlash(localBaseUrl) ?? baseUrl;
}

let querySerializer =
Expand All @@ -72,29 +73,46 @@ export default function createClient(clientOptions) {
});
}

const serializedBody = body === undefined ? undefined : bodySerializer(body);

const defaultHeaders =
const serializedBody =
body === undefined
? undefined
: bodySerializer(
body,
// Note: we declare mergeHeaders() both here and below because it’s a bit of a chicken-or-egg situation:
// bodySerializer() needs all headers so we aren’t dropping ones set by the user, however,
// the result of this ALSO sets the lowest-priority content-type header. So we re-merge below,
// setting the content-type at the very beginning to be overwritten.
// Lastly, based on the way headers work, it’s not a simple “present-or-not” check becauase null intentionally un-sets headers.
mergeHeaders(baseHeaders, headers, params.header),
);
const finalHeaders = mergeHeaders(
// with no body, we should not to set Content-Type
serializedBody === undefined ||
// if serialized body is FormData; browser will correctly set Content-Type & boundary expression
serializedBody instanceof FormData
// if serialized body is FormData; browser will correctly set Content-Type & boundary expression
serializedBody instanceof FormData
? {}
: {
"Content-Type": "application/json",
};
},
baseHeaders,
headers,
params.header,
);

const requestInit = {
redirect: "follow",
...baseOptions,
...init,
body: serializedBody,
headers: mergeHeaders(defaultHeaders, baseHeaders, headers, params.header),
headers: finalHeaders,
};

let id;
let options;
let request = new CustomRequest(createFinalURL(schemaPath, { baseUrl, params, querySerializer }), requestInit);
let request = new CustomRequest(
createFinalURL(schemaPath, { baseUrl: finalBaseUrl, params, querySerializer }),
requestInit,
);
let response;

/** Add custom parameters to Request object */
Expand All @@ -109,7 +127,7 @@ export default function createClient(clientOptions) {

// middleware (request)
options = Object.freeze({
baseUrl,
baseUrl: finalBaseUrl,
fetch,
parseAs,
querySerializer,
Expand Down Expand Up @@ -574,10 +592,19 @@ export function defaultPathSerializer(pathname, pathParams) {
* Serialize body object to string
* @type {import("./index.js").defaultBodySerializer}
*/
export function defaultBodySerializer(body) {
export function defaultBodySerializer(body, headers) {
if (body instanceof FormData) {
return body;
}
if (headers) {
const contentType =
headers.get instanceof Function
? (headers.get("Content-Type") ?? headers.get("content-type"))
: (headers["Content-Type"] ?? headers["content-type"]);
if (contentType === "application/x-www-form-urlencoded") {
return new URLSearchParams(body).toString();
}
}
return JSON.stringify(body);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/openapi-fetch/test/bench/index.bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Fetcher } from "openapi-typescript-fetch";
import { createApiFetchClient } from "feature-fetch";
import superagent from "superagent";
import { afterAll, bench, describe, vi } from "vitest";
import createClient, { createPathBasedClient } from "../../dist/index.js";
import createClient, { createPathBasedClient } from "../../dist/index.mjs";
import * as openapiTSCodegen from "./openapi-typescript-codegen.min.js";

const BASE_URL = "https://api.test.local";
Expand Down
19 changes: 19 additions & 0 deletions packages/openapi-fetch/test/common/create-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ describe("createClient options", () => {
expect(actualURL.href).toBe("https://api.foo.bar/v3/resources");
});

test("baseUrl per request causes no override on default baseUrl", async () => {
let actualURL = new URL("https://fakeurl.example");
const client = createObservedClient<paths>({ baseUrl: "https://api.foo.bar/v2/" }, async (req) => {
actualURL = new URL(req.url);
return Response.json([]);
});

const localBaseUrl = "https://api.foo.bar/v3";
await client.GET("/resources", { baseUrl: localBaseUrl });

// assert baseUrl and path mesh as expected
expect(actualURL.href).toBe("https://api.foo.bar/v3/resources");

await client.GET("/resources");

// assert baseUrl and path mesh as expected
expect(actualURL.href).toBe("https://api.foo.bar/v2/resources");
});

describe("content-type", () => {
const BODY_ACCEPTING_METHODS = [["PUT"], ["POST"], ["DELETE"], ["OPTIONS"], ["PATCH"]] as const;
const ALL_METHODS = [...BODY_ACCEPTING_METHODS, ["GET"], ["HEAD"]] as const;
Expand Down
13 changes: 13 additions & 0 deletions packages/openapi-fetch/test/common/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,19 @@ describe("request", () => {
expect(bodyUsed).toBe(true);
expect(bodyText).toBe("0");
});

test("`application/x-www-form-urlencoded` body", async () => {
const { bodyUsed, bodyText } = await fireRequestAndGetBodyInformation({
method: "POST",
fetchOptions: {
body: { key1: "value1", key2: "value2" },
headers: { "Content-Type": "application/x-www-form-urlencoded" },
},
});

expect(bodyUsed).toBe(true);
expect(bodyText).toBe("key1=value1&key2=value2");
});
});

test("cookie header is preserved", async () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/openapi-fetch/test/examples/schemas/github.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32229,7 +32229,7 @@ export interface components {
* @default RIGHT
* @enum {string|null}
*/
start_side: "LEFT" | "RIGHT";
start_side: "LEFT" | "RIGHT" | null;
/**
* @description The line of the blob to which the comment applies. The last line of the range for a multi-line comment
* @example 2
Expand Down Expand Up @@ -33004,7 +33004,7 @@ export interface components {
* @default RIGHT
* @enum {string|null}
*/
start_side: "LEFT" | "RIGHT";
start_side: "LEFT" | "RIGHT" | null;
/**
* @description The line of the blob to which the comment applies. The last line of the range for a multi-line comment
* @example 2
Expand Down Expand Up @@ -108426,7 +108426,7 @@ export interface operations {
/** @description The name of the task for the deployment (e.g., `deploy` or `deploy:migrations`). */
task?: string;
/** @description The name of the environment that was deployed to (e.g., `staging` or `production`). */
environment?: string;
environment?: string | null;
/** @description The number of results per page (max 100). For more information, see "[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api)." */
per_page?: components["parameters"]["per-page"];
/** @description The page number of the results to fetch. For more information, see "[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api)." */
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-metadata/.npmignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.turbo
*.config.*
test
tsconfig*.json
vitest.config.ts
Expand Down
6 changes: 6 additions & 0 deletions packages/openapi-metadata/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# openapi-metadata

## 0.2.1

### Patch Changes

- [#2125](https://github.com/openapi-ts/openapi-typescript/pull/2125) [`a041c2d`](https://github.com/openapi-ts/openapi-typescript/commit/a041c2d340a4f1573de726d1f960169440901b24) Thanks [@kerwanp](https://github.com/kerwanp)! - [#2123](https://github.com/openapi-ts/openapi-typescript/issues/2123) Fix thunk type detection

## 0.2.0

### Minor Changes
Expand Down
20 changes: 20 additions & 0 deletions packages/openapi-metadata/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { defineBuildConfig } from "unbuild";

export default defineBuildConfig({
entries: [
"./src/index.ts",
"./src/decorators/index.ts",
"./src/metadata/index.ts",
"./src/errors/index.ts",
"./src/ui/index.ts",
],
declaration: "compatible",
clean: true,
sourcemap: true,
rollup: {
// Ship CommonJS-compatible bundle
emitCJS: true,
// Don’t bundle .js files together to more closely match old exports (can remove in next major)
output: { preserveModules: true },
},
});
Loading