diff --git a/.changeset/neat-donkeys-pay.md b/.changeset/neat-donkeys-pay.md
new file mode 100644
index 000000000..5f75bf300
--- /dev/null
+++ b/.changeset/neat-donkeys-pay.md
@@ -0,0 +1,5 @@
+---
+'@hey-api/openapi-ts': patch
+---
+
+fix: do not use a body serializer on text/plain sdks
diff --git a/.changeset/six-horses-report.md b/.changeset/six-horses-report.md
new file mode 100644
index 000000000..e5e017397
--- /dev/null
+++ b/.changeset/six-horses-report.md
@@ -0,0 +1,8 @@
+---
+'@hey-api/client-axios': patch
+'@hey-api/client-fetch': patch
+'@hey-api/client-nuxt': patch
+'@hey-api/openapi-ts': patch
+---
+
+fix: add null to valid bodySerializer types
diff --git a/examples/openapi-ts-sample/.gitignore b/examples/openapi-ts-sample/.gitignore
new file mode 100644
index 000000000..a547bf36d
--- /dev/null
+++ b/examples/openapi-ts-sample/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/examples/openapi-ts-sample/index.html b/examples/openapi-ts-sample/index.html
new file mode 100644
index 000000000..d301a3a91
--- /dev/null
+++ b/examples/openapi-ts-sample/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Hey API + Fetch API Demo
+
+
+
+
+
+
diff --git a/examples/openapi-ts-sample/openapi-ts.config.ts b/examples/openapi-ts-sample/openapi-ts.config.ts
new file mode 100644
index 000000000..2470f065a
--- /dev/null
+++ b/examples/openapi-ts-sample/openapi-ts.config.ts
@@ -0,0 +1,20 @@
+import { defineConfig } from '@hey-api/openapi-ts';
+
+export default defineConfig({
+ client: '@hey-api/client-fetch',
+ input:
+ '../../packages/openapi-ts/test/spec/2.0.x/body-response-text-plain.yaml',
+ output: {
+ format: 'prettier',
+ lint: 'eslint',
+ path: './src/client',
+ },
+ plugins: [
+ '@hey-api/schemas',
+ '@hey-api/sdk',
+ {
+ enums: 'javascript',
+ name: '@hey-api/typescript',
+ },
+ ],
+});
diff --git a/examples/openapi-ts-sample/package.json b/examples/openapi-ts-sample/package.json
new file mode 100644
index 000000000..9801d3ad7
--- /dev/null
+++ b/examples/openapi-ts-sample/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@example/openapi-ts-sample",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "build": "tsc && vite build",
+ "dev": "vite",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "openapi-ts": "openapi-ts",
+ "preview": "vite preview",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@hey-api/client-fetch": "workspace:*",
+ "@radix-ui/react-form": "0.1.1",
+ "@radix-ui/react-icons": "1.3.2",
+ "@radix-ui/themes": "3.1.6",
+ "react": "19.0.0",
+ "react-dom": "19.0.0"
+ },
+ "devDependencies": {
+ "@hey-api/openapi-ts": "workspace:*",
+ "@types/react": "19.0.1",
+ "@types/react-dom": "19.0.1",
+ "@typescript-eslint/eslint-plugin": "7.18.0",
+ "@typescript-eslint/parser": "7.15.0",
+ "@vitejs/plugin-react": "4.3.1",
+ "autoprefixer": "10.4.19",
+ "eslint": "9.17.0",
+ "eslint-plugin-react-hooks": "4.6.2",
+ "eslint-plugin-react-refresh": "0.4.7",
+ "postcss": "8.4.39",
+ "prettier": "3.4.2",
+ "tailwindcss": "3.4.4",
+ "typescript": "5.5.3",
+ "vite": "6.0.7"
+ }
+}
diff --git a/examples/openapi-ts-sample/postcss.config.js b/examples/openapi-ts-sample/postcss.config.js
new file mode 100644
index 000000000..9eef821c4
--- /dev/null
+++ b/examples/openapi-ts-sample/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ autoprefixer: {},
+ tailwindcss: {},
+ },
+};
diff --git a/examples/openapi-ts-sample/src/App.css b/examples/openapi-ts-sample/src/App.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/examples/openapi-ts-sample/src/App.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/examples/openapi-ts-sample/src/App.tsx b/examples/openapi-ts-sample/src/App.tsx
new file mode 100644
index 000000000..dbd10f3bb
--- /dev/null
+++ b/examples/openapi-ts-sample/src/App.tsx
@@ -0,0 +1,40 @@
+import './App.css';
+
+import {
+ Box,
+ Button,
+ Container,
+ Flex,
+ Heading,
+ Section,
+} from '@radix-ui/themes';
+
+import { postFoo } from './client/sdk.gen';
+
+function App() {
+ const onClick = async () => {
+ postFoo({
+ body: 'foo',
+ });
+ };
+
+ return (
+
+
+
+
+ sample for internal testing
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/examples/openapi-ts-sample/src/client/index.ts b/examples/openapi-ts-sample/src/client/index.ts
new file mode 100644
index 000000000..688e3c912
--- /dev/null
+++ b/examples/openapi-ts-sample/src/client/index.ts
@@ -0,0 +1,3 @@
+// This file is auto-generated by @hey-api/openapi-ts
+export * from './sdk.gen';
+export * from './types.gen';
diff --git a/examples/openapi-ts-sample/src/client/sdk.gen.ts b/examples/openapi-ts-sample/src/client/sdk.gen.ts
new file mode 100644
index 000000000..eb9f289e4
--- /dev/null
+++ b/examples/openapi-ts-sample/src/client/sdk.gen.ts
@@ -0,0 +1,24 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import {
+ createClient,
+ createConfig,
+ type Options,
+} from '@hey-api/client-fetch';
+
+import type { PostFooData, PostFooResponse } from './types.gen';
+
+export const client = createClient(createConfig());
+
+export const postFoo = (
+ options: Options,
+) =>
+ (options?.client ?? client).post({
+ bodySerializer: null,
+ url: '/foo',
+ ...options,
+ headers: {
+ 'Content-Type': 'text/plain',
+ ...options?.headers,
+ },
+ });
diff --git a/examples/openapi-ts-sample/src/client/types.gen.ts b/examples/openapi-ts-sample/src/client/types.gen.ts
new file mode 100644
index 000000000..bdb7d6bc0
--- /dev/null
+++ b/examples/openapi-ts-sample/src/client/types.gen.ts
@@ -0,0 +1,17 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+export type PostFooData = {
+ body: string;
+ path?: never;
+ query?: never;
+ url: '/foo';
+};
+
+export type PostFooResponses = {
+ /**
+ * OK
+ */
+ 200: string;
+};
+
+export type PostFooResponse = PostFooResponses[keyof PostFooResponses];
diff --git a/examples/openapi-ts-sample/src/main.tsx b/examples/openapi-ts-sample/src/main.tsx
new file mode 100644
index 000000000..44ee3f3f4
--- /dev/null
+++ b/examples/openapi-ts-sample/src/main.tsx
@@ -0,0 +1,26 @@
+import '@radix-ui/themes/styles.css';
+
+import { Theme } from '@radix-ui/themes';
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+
+import App from './App.tsx';
+import { client } from './client/sdk.gen';
+
+// configure internal service client
+client.setConfig({
+ // set default base url for requests
+ baseUrl: 'https://petstore3.swagger.io/api/v3',
+ // set default headers for requests
+ headers: {
+ Authorization: 'Bearer ',
+ },
+});
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
+);
diff --git a/examples/openapi-ts-sample/src/vite-env.d.ts b/examples/openapi-ts-sample/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/examples/openapi-ts-sample/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/openapi-ts-sample/tailwind.config.js b/examples/openapi-ts-sample/tailwind.config.js
new file mode 100644
index 000000000..0284c5614
--- /dev/null
+++ b/examples/openapi-ts-sample/tailwind.config.js
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ['./index.html', './src/**/*.{html,js,ts,jsx,tsx}'],
+ plugins: [],
+ theme: {
+ extend: {},
+ },
+};
diff --git a/examples/openapi-ts-sample/tsconfig.json b/examples/openapi-ts-sample/tsconfig.json
new file mode 100644
index 000000000..a7fc6fbf2
--- /dev/null
+++ b/examples/openapi-ts-sample/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/examples/openapi-ts-sample/tsconfig.node.json b/examples/openapi-ts-sample/tsconfig.node.json
new file mode 100644
index 000000000..97ede7ee6
--- /dev/null
+++ b/examples/openapi-ts-sample/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/examples/openapi-ts-sample/vite.config.ts b/examples/openapi-ts-sample/vite.config.ts
new file mode 100644
index 000000000..4e7004ebc
--- /dev/null
+++ b/examples/openapi-ts-sample/vite.config.ts
@@ -0,0 +1,7 @@
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+});
diff --git a/packages/client-axios/src/types.ts b/packages/client-axios/src/types.ts
index 333e77d69..501a4a291 100644
--- a/packages/client-axios/src/types.ts
+++ b/packages/client-axios/src/types.ts
@@ -32,7 +32,7 @@ export interface Config
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
- bodySerializer?: BodySerializer;
+ bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
diff --git a/packages/client-fetch/src/types.ts b/packages/client-fetch/src/types.ts
index d23859671..d48f7e59d 100644
--- a/packages/client-fetch/src/types.ts
+++ b/packages/client-fetch/src/types.ts
@@ -24,7 +24,7 @@ export interface Config
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
- bodySerializer?: BodySerializer;
+ bodySerializer?: BodySerializer | null;
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
diff --git a/packages/client-nuxt/src/types.ts b/packages/client-nuxt/src/types.ts
index d866af3b3..0aff7480f 100644
--- a/packages/client-nuxt/src/types.ts
+++ b/packages/client-nuxt/src/types.ts
@@ -65,7 +65,7 @@ export interface Config
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
- bodySerializer?: BodySerializer;
+ bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
diff --git a/packages/openapi-ts/src/ir/__tests__/mediaType.test.ts b/packages/openapi-ts/src/ir/__tests__/mediaType.test.ts
index de8395ad9..6d69c8c2a 100644
--- a/packages/openapi-ts/src/ir/__tests__/mediaType.test.ts
+++ b/packages/openapi-ts/src/ir/__tests__/mediaType.test.ts
@@ -1,70 +1,106 @@
import { describe, expect, it } from 'vitest';
-import { isMediaTypeFileLike } from '../mediaType';
+import type { IRMediaType } from '../mediaType';
+import { isMediaTypeFileLike, mediaTypeToIrMediaType } from '../mediaType';
describe('isMediaTypeFileLike', () => {
const scenarios: Array<{
- fileLike: ReturnType;
- mediaType: Parameters[0]['mediaType'];
+ mediaType: string;
+ response: boolean;
}> = [
{
- fileLike: false,
mediaType: 'application/json',
+ response: false,
},
{
- fileLike: true,
mediaType: 'application/json+download',
+ response: true,
},
{
- fileLike: false,
mediaType: 'application/json; charset=ascii',
+ response: false,
},
{
- fileLike: true,
mediaType: 'application/octet-stream',
+ response: true,
},
{
- fileLike: true,
mediaType: 'application/pdf',
+ response: true,
},
{
- fileLike: true,
mediaType: 'application/xml; charset=utf-8',
+ response: true,
},
{
- fileLike: true,
mediaType: 'application/zip',
+ response: true,
},
{
- fileLike: false,
mediaType: 'image/jpeg',
+ response: false,
},
{
- fileLike: false,
mediaType: 'image/jpeg; charset=utf-8',
+ response: false,
},
{
- fileLike: false,
mediaType: 'text/html; charset=utf-8',
+ response: false,
},
{
- fileLike: true,
mediaType: 'text/javascript; charset=ISO-8859-1',
+ response: true,
},
{
- fileLike: false,
mediaType: 'text/plain; charset=utf-8',
+ response: false,
},
{
- fileLike: true,
mediaType: 'video/mp4',
+ response: true,
},
];
it.each(scenarios)(
- 'detects $mediaType as file-like? $fileLike',
- async ({ fileLike, mediaType }) => {
- expect(isMediaTypeFileLike({ mediaType })).toEqual(fileLike);
+ 'detects $mediaType as file-like? $response',
+ async ({ mediaType, response }) => {
+ expect(isMediaTypeFileLike({ mediaType })).toEqual(response);
+ },
+ );
+});
+
+describe('mediaTypeToIrMediaType', () => {
+ const scenarios: Array<{
+ mediaType: string;
+ response: IRMediaType | undefined;
+ }> = [
+ {
+ mediaType: 'multipart/form-data',
+ response: 'form-data',
+ },
+ {
+ mediaType: 'application/json',
+ response: 'json',
+ },
+ {
+ mediaType: 'text/plain; charset=utf-8',
+ response: 'text',
+ },
+ {
+ mediaType: 'application/x-www-form-urlencoded',
+ response: 'url-search-params',
+ },
+ {
+ mediaType: 'application/foo',
+ response: undefined,
+ },
+ ];
+
+ it.each(scenarios)(
+ 'ir media type for $mediaType: $response',
+ async ({ mediaType, response }) => {
+ expect(mediaTypeToIrMediaType({ mediaType })).toEqual(response);
},
);
});
diff --git a/packages/openapi-ts/src/ir/mediaType.ts b/packages/openapi-ts/src/ir/mediaType.ts
index 6883cfa38..0db232aaa 100644
--- a/packages/openapi-ts/src/ir/mediaType.ts
+++ b/packages/openapi-ts/src/ir/mediaType.ts
@@ -2,10 +2,11 @@ const fileLikeRegExp =
/^(application\/(pdf|rtf|msword|vnd\.(ms-|openxmlformats-officedocument\.)|zip|x-(7z|tar|rar|zip|iso)|octet-stream|gzip|x-msdownload|json\+download|xml|x-yaml|x-7z-compressed|x-tar)|text\/(yaml|css|javascript)|audio\/(mpeg|wav)|video\/(mp4|x-matroska)|image\/(vnd\.adobe\.photoshop|svg\+xml))(; ?charset=[^;]+)?$/i;
const jsonMimeRegExp = /^application\/(.*\+)?json(;.*)?$/i;
const multipartFormDataMimeRegExp = /^multipart\/form-data(;.*)?$/i;
+const textMimeRegExp = /^text\/[a-z0-9.+-]+(;.*)?$/i;
const xWwwFormUrlEncodedMimeRegExp =
/^application\/x-www-form-urlencoded(;.*)?$/i;
-export type IRMediaType = 'form-data' | 'json' | 'url-search-params';
+export type IRMediaType = 'form-data' | 'json' | 'text' | 'url-search-params';
export const isMediaTypeFileLike = ({
mediaType,
@@ -31,6 +32,11 @@ export const mediaTypeToIrMediaType = ({
return 'form-data';
}
+ textMimeRegExp.lastIndex = 0;
+ if (textMimeRegExp.test(mediaType)) {
+ return 'text';
+ }
+
xWwwFormUrlEncodedMimeRegExp.lastIndex = 0;
if (xWwwFormUrlEncodedMimeRegExp.test(mediaType)) {
return 'url-search-params';
diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts
index 8d1bdfa2b..85b46f40f 100644
--- a/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts
+++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts
@@ -254,6 +254,14 @@ const operationStatements = ({
});
break;
case 'json':
+ // jsonBodySerializer is the default, no need to specify
+ break;
+ case 'text':
+ // ensure we don't use any serializer by default
+ requestOptions.push({
+ key: 'bodySerializer',
+ value: null,
+ });
break;
case 'url-search-params':
requestOptions.push({ spread: 'urlSearchParamsBodySerializer' });
diff --git a/packages/openapi-ts/test/2.0.x.test.ts b/packages/openapi-ts/test/2.0.x.test.ts
index 66fedebff..f67970fa9 100644
--- a/packages/openapi-ts/test/2.0.x.test.ts
+++ b/packages/openapi-ts/test/2.0.x.test.ts
@@ -38,6 +38,7 @@ describe(`OpenAPI ${version}`, () => {
config: createConfig({
input: 'body-response-text-plain.yaml',
output: 'body-response-text-plain',
+ plugins: ['@hey-api/typescript', '@hey-api/sdk'],
}),
description: 'handle text/plain content type',
},
diff --git a/packages/openapi-ts/test/3.0.x.test.ts b/packages/openapi-ts/test/3.0.x.test.ts
index 245525960..6a772738f 100644
--- a/packages/openapi-ts/test/3.0.x.test.ts
+++ b/packages/openapi-ts/test/3.0.x.test.ts
@@ -67,6 +67,7 @@ describe(`OpenAPI ${version}`, () => {
config: createConfig({
input: 'body-response-text-plain.yaml',
output: 'body-response-text-plain',
+ plugins: ['@hey-api/typescript', '@hey-api/sdk'],
}),
description: 'handle text/plain content type',
},
diff --git a/packages/openapi-ts/test/3.1.x.test.ts b/packages/openapi-ts/test/3.1.x.test.ts
index 63adc6454..1dae4f653 100644
--- a/packages/openapi-ts/test/3.1.x.test.ts
+++ b/packages/openapi-ts/test/3.1.x.test.ts
@@ -67,6 +67,7 @@ describe(`OpenAPI ${version}`, () => {
config: createConfig({
input: 'body-response-text-plain.yaml',
output: 'body-response-text-plain',
+ plugins: ['@hey-api/typescript', '@hey-api/sdk'],
}),
description: 'handle text/plain content type',
},
diff --git a/packages/openapi-ts/test/__snapshots__/2.0.x/body-response-text-plain/index.ts b/packages/openapi-ts/test/__snapshots__/2.0.x/body-response-text-plain/index.ts
index 56bade120..e64537d21 100644
--- a/packages/openapi-ts/test/__snapshots__/2.0.x/body-response-text-plain/index.ts
+++ b/packages/openapi-ts/test/__snapshots__/2.0.x/body-response-text-plain/index.ts
@@ -1,2 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
-export * from './types.gen';
\ No newline at end of file
+export * from './types.gen';
+export * from './sdk.gen';
\ No newline at end of file
diff --git a/packages/openapi-ts/test/__snapshots__/2.0.x/body-response-text-plain/sdk.gen.ts b/packages/openapi-ts/test/__snapshots__/2.0.x/body-response-text-plain/sdk.gen.ts
new file mode 100644
index 000000000..e8ec14a9a
--- /dev/null
+++ b/packages/openapi-ts/test/__snapshots__/2.0.x/body-response-text-plain/sdk.gen.ts
@@ -0,0 +1,18 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import { createClient, createConfig, type Options } from '@hey-api/client-fetch';
+import type { PostFooData, PostFooResponse } from './types.gen';
+
+export const client = createClient(createConfig());
+
+export const postFoo = (options: Options) => {
+ return (options?.client ?? client).post({
+ bodySerializer: null,
+ url: '/foo',
+ ...options,
+ headers: {
+ 'Content-Type': 'text/plain',
+ ...options?.headers
+ }
+ });
+};
\ No newline at end of file
diff --git a/packages/openapi-ts/test/__snapshots__/3.0.x/body-response-text-plain/index.ts b/packages/openapi-ts/test/__snapshots__/3.0.x/body-response-text-plain/index.ts
index 56bade120..e64537d21 100644
--- a/packages/openapi-ts/test/__snapshots__/3.0.x/body-response-text-plain/index.ts
+++ b/packages/openapi-ts/test/__snapshots__/3.0.x/body-response-text-plain/index.ts
@@ -1,2 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
-export * from './types.gen';
\ No newline at end of file
+export * from './types.gen';
+export * from './sdk.gen';
\ No newline at end of file
diff --git a/packages/openapi-ts/test/__snapshots__/3.0.x/body-response-text-plain/sdk.gen.ts b/packages/openapi-ts/test/__snapshots__/3.0.x/body-response-text-plain/sdk.gen.ts
new file mode 100644
index 000000000..e8ec14a9a
--- /dev/null
+++ b/packages/openapi-ts/test/__snapshots__/3.0.x/body-response-text-plain/sdk.gen.ts
@@ -0,0 +1,18 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import { createClient, createConfig, type Options } from '@hey-api/client-fetch';
+import type { PostFooData, PostFooResponse } from './types.gen';
+
+export const client = createClient(createConfig());
+
+export const postFoo = (options: Options) => {
+ return (options?.client ?? client).post({
+ bodySerializer: null,
+ url: '/foo',
+ ...options,
+ headers: {
+ 'Content-Type': 'text/plain',
+ ...options?.headers
+ }
+ });
+};
\ No newline at end of file
diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/body-response-text-plain/index.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/body-response-text-plain/index.ts
index 56bade120..e64537d21 100644
--- a/packages/openapi-ts/test/__snapshots__/3.1.x/body-response-text-plain/index.ts
+++ b/packages/openapi-ts/test/__snapshots__/3.1.x/body-response-text-plain/index.ts
@@ -1,2 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
-export * from './types.gen';
\ No newline at end of file
+export * from './types.gen';
+export * from './sdk.gen';
\ No newline at end of file
diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/body-response-text-plain/sdk.gen.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/body-response-text-plain/sdk.gen.ts
new file mode 100644
index 000000000..e8ec14a9a
--- /dev/null
+++ b/packages/openapi-ts/test/__snapshots__/3.1.x/body-response-text-plain/sdk.gen.ts
@@ -0,0 +1,18 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import { createClient, createConfig, type Options } from '@hey-api/client-fetch';
+import type { PostFooData, PostFooResponse } from './types.gen';
+
+export const client = createClient(createConfig());
+
+export const postFoo = (options: Options) => {
+ return (options?.client ?? client).post({
+ bodySerializer: null,
+ url: '/foo',
+ ...options,
+ headers: {
+ 'Content-Type': 'text/plain',
+ ...options?.headers
+ }
+ });
+};
\ No newline at end of file
diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/bundle/client/types.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/bundle/client/types.ts
index 333e77d69..501a4a291 100644
--- a/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/bundle/client/types.ts
+++ b/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/bundle/client/types.ts
@@ -32,7 +32,7 @@ export interface Config
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
- bodySerializer?: BodySerializer;
+ bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-fetch/bundle/client/types.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-fetch/bundle/client/types.ts
index d23859671..d48f7e59d 100644
--- a/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-fetch/bundle/client/types.ts
+++ b/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-fetch/bundle/client/types.ts
@@ -24,7 +24,7 @@ export interface Config
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
- bodySerializer?: BodySerializer;
+ bodySerializer?: BodySerializer | null;
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-nuxt/bundle/client/types.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-nuxt/bundle/client/types.ts
index d866af3b3..0aff7480f 100644
--- a/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-nuxt/bundle/client/types.ts
+++ b/packages/openapi-ts/test/__snapshots__/3.1.x/clients/@hey-api/client-nuxt/bundle/client/types.ts
@@ -65,7 +65,7 @@ export interface Config
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
- bodySerializer?: BodySerializer;
+ bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4f84f77f9..dc8aa5592 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -275,6 +275,73 @@ importers:
specifier: workspace:*
version: link:../../packages/openapi-ts
+ examples/openapi-ts-sample:
+ dependencies:
+ '@hey-api/client-fetch':
+ specifier: workspace:*
+ version: link:../../packages/client-fetch
+ '@radix-ui/react-form':
+ specifier: 0.1.1
+ version: 0.1.1(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ '@radix-ui/react-icons':
+ specifier: 1.3.2
+ version: 1.3.2(react@19.0.0)
+ '@radix-ui/themes':
+ specifier: 3.1.6
+ version: 3.1.6(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ react:
+ specifier: 19.0.0
+ version: 19.0.0
+ react-dom:
+ specifier: 19.0.0
+ version: 19.0.0(react@19.0.0)
+ devDependencies:
+ '@hey-api/openapi-ts':
+ specifier: workspace:*
+ version: link:../../packages/openapi-ts
+ '@types/react':
+ specifier: 19.0.1
+ version: 19.0.1
+ '@types/react-dom':
+ specifier: 19.0.1
+ version: 19.0.1
+ '@typescript-eslint/eslint-plugin':
+ specifier: 7.18.0
+ version: 7.18.0(@typescript-eslint/parser@7.15.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.5.3))(eslint@9.17.0(jiti@2.4.2))(typescript@5.5.3)
+ '@typescript-eslint/parser':
+ specifier: 7.15.0
+ version: 7.15.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.5.3)
+ '@vitejs/plugin-react':
+ specifier: 4.3.1
+ version: 4.3.1(vite@6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.0)(sass@1.80.7)(terser@5.36.0)(yaml@2.7.0))
+ autoprefixer:
+ specifier: 10.4.19
+ version: 10.4.19(postcss@8.4.39)
+ eslint:
+ specifier: 9.17.0
+ version: 9.17.0(jiti@2.4.2)
+ eslint-plugin-react-hooks:
+ specifier: 4.6.2
+ version: 4.6.2(eslint@9.17.0(jiti@2.4.2))
+ eslint-plugin-react-refresh:
+ specifier: 0.4.7
+ version: 0.4.7(eslint@9.17.0(jiti@2.4.2))
+ postcss:
+ specifier: 8.4.39
+ version: 8.4.39
+ prettier:
+ specifier: 3.4.2
+ version: 3.4.2
+ tailwindcss:
+ specifier: 3.4.4
+ version: 3.4.4(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.5.3))
+ typescript:
+ specifier: 5.5.3
+ version: 5.5.3
+ vite:
+ specifier: 6.0.7
+ version: 6.0.7(@types/node@22.10.5)(jiti@2.4.2)(less@4.2.0)(sass@1.80.7)(terser@5.36.0)(yaml@2.7.0)
+
examples/openapi-ts-tanstack-angular-query-experimental:
dependencies:
'@angular/animations':