diff --git a/.changeset/refactor-pinia-colada-query.md b/.changeset/refactor-pinia-colada-query.md
new file mode 100644
index 0000000000..8e7d8c3b37
--- /dev/null
+++ b/.changeset/refactor-pinia-colada-query.md
@@ -0,0 +1,5 @@
+---
+"@hey-api/openapi-ts": patch
+---
+
+feat(pinia-colada): query options use `defineQueryOptions`
diff --git a/docs/data/people.ts b/docs/data/people.ts
index 662cb18a73..dc7e75b306 100644
--- a/docs/data/people.ts
+++ b/docs/data/people.ts
@@ -3,6 +3,11 @@ type Person = {
name: string;
};
+export const dmitriyBrolnickij: Person = {
+ github: 'https://github.com/brolnickij',
+ name: 'Dmitriy Brolnickij',
+};
+
export const jacobCohen: Person = {
github: 'https://github.com/jacobinu',
name: 'Jacob Cohen',
diff --git a/docs/openapi-ts/clients/ofetch.md b/docs/openapi-ts/clients/ofetch.md
index 4798a0b1b0..14bb3ed01a 100644
--- a/docs/openapi-ts/clients/ofetch.md
+++ b/docs/openapi-ts/clients/ofetch.md
@@ -3,6 +3,11 @@ title: OFetch Client
description: Generate a type-safe ofetch client from OpenAPI with the ofetch client for openapi-ts. Fully compatible with validators, transformers, and all core features.
---
+
+
# OFetch
### About
@@ -11,6 +16,10 @@ description: Generate a type-safe ofetch client from OpenAPI with the ofetch cli
The `ofetch` client for Hey API generates a type-safe client from your OpenAPI spec, fully compatible with validators, transformers, and all core features.
+### Collaborators
+
+
+
## Features
- seamless integration with `@hey-api/openapi-ts` ecosystem
diff --git a/docs/openapi-ts/plugins/pinia-colada.md b/docs/openapi-ts/plugins/pinia-colada.md
index 5ec2a00cc3..6c3c94cdb5 100644
--- a/docs/openapi-ts/plugins/pinia-colada.md
+++ b/docs/openapi-ts/plugins/pinia-colada.md
@@ -7,7 +7,7 @@ description: Generate Pinia Colada v0 functions and query keys from OpenAPI with
import AuthorsList from '@components/AuthorsList.vue';
import Heading from '@components/Heading.vue';
import VersionLabel from '@components/VersionLabel.vue';
-import { joshHemphill, sebastiaanWouters } from '@data/people.js';
+import { joshHemphill, sebastiaanWouters, dmitriyBrolnickij } from '@data/people.js';
@@ -23,7 +23,7 @@ The Pinia Colada plugin for Hey API generates functions and query keys from your
### Collaborators
-
+
## Features
@@ -65,13 +65,11 @@ Queries are generated from [query operations](/openapi-ts/configuration/parser#h
::: code-group
```ts [example]
-const query = useQuery({
- ...getPetByIdQuery({
- path: {
- petId: 1,
- },
- }),
-});
+const query = useQuery(getPetByIdQuery, () => ({
+ path: {
+ petId: 1,
+ },
+}));
```
```js [config]
diff --git a/examples/openapi-ts-pinia-colada/openapi-ts.config.ts b/examples/openapi-ts-pinia-colada/openapi-ts.config.ts
index 7a00915a40..a231ae4d61 100644
--- a/examples/openapi-ts-pinia-colada/openapi-ts.config.ts
+++ b/examples/openapi-ts-pinia-colada/openapi-ts.config.ts
@@ -18,7 +18,8 @@ export default defineConfig({
},
{
exportFromIndex: true,
- name: '@pinia/colada'
+ name: '@pinia/colada',
+ queryKeys: false
}
]
})
diff --git a/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts b/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts
index 21f0097478..1564abf607 100644
--- a/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts
+++ b/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts
@@ -1,5 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts
+import { type _JSONValue, defineQueryOptions, type UseMutationOptions } from '@pinia/colada'
+
+import { client } from '../client.gen'
+import { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'
import {
addPet,
createUser,
@@ -24,8 +28,11 @@ import {
} from '../sdk.gen'
import type {
AddPetData,
+ AddPetResponse,
CreateUserData,
+ CreateUserResponse,
CreateUsersWithListInputData,
+ CreateUsersWithListInputResponse,
DeleteOrderData,
DeletePetData,
DeleteUserData,
@@ -38,19 +45,29 @@ import type {
LoginUserData,
LogoutUserData,
PlaceOrderData,
+ PlaceOrderResponse,
UpdatePetData,
+ UpdatePetResponse,
UpdatePetWithFormData,
+ UpdatePetWithFormResponse,
UpdateUserData,
- UploadFileData
+ UploadFileData,
+ UploadFileResponse
} from '../types.gen'
/**
* Add a new pet to the store.
* Add a new pet to the store.
*/
-export const addPetMutation = () => ({
- mutation: async (options: Options) => {
- const { data } = await addPet(options)
+export const addPetMutation = (
+ options?: Partial>
+): UseMutationOptions, Error> => ({
+ mutation: async (fnOptions) => {
+ const { data } = await addPet({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ })
return data
}
})
@@ -59,52 +76,106 @@ export const addPetMutation = () => ({
* Update an existing pet.
* Update an existing pet by Id.
*/
-export const updatePetMutation = () => ({
- mutation: async (options: Options) => {
- const { data } = await updatePet(options)
+export const updatePetMutation = (
+ options?: Partial>
+): UseMutationOptions, Error> => ({
+ mutation: async (fnOptions) => {
+ const { data } = await updatePet({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ })
return data
}
})
+export type QueryKey = [
+ Pick & {
+ _id: string
+ baseUrl?: _JSONValue
+ body?: _JSONValue
+ query?: _JSONValue
+ tags?: _JSONValue
+ }
+]
+
+const createQueryKey = (
+ id: string,
+ options?: TOptions,
+ tags?: ReadonlyArray
+): [QueryKey[0]] => {
+ const params: QueryKey[0] = {
+ _id: id,
+ baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl
+ } as QueryKey[0]
+ if (tags) {
+ params.tags = tags as unknown as _JSONValue
+ }
+ if (options?.body !== undefined) {
+ const normalizedBody = serializeQueryKeyValue(options.body)
+ if (normalizedBody !== undefined) {
+ params.body = normalizedBody
+ }
+ }
+ if (options?.path) {
+ params.path = options.path
+ }
+ if (options?.query !== undefined) {
+ const normalizedQuery = serializeQueryKeyValue(options.query)
+ if (normalizedQuery !== undefined) {
+ params.query = normalizedQuery
+ }
+ }
+ return [params]
+}
+
/**
* Finds Pets by status.
* Multiple status values can be provided with comma separated strings.
*/
-export const findPetsByStatusQuery = (options: Options) => ({
- key: ['findPetsByStatus', options?.path],
- query: async (context: { signal: AbortSignal }) => {
- const { data } = await findPetsByStatus({
- ...options,
- signal: context.signal,
- throwOnError: true
- })
- return data
- }
-})
+export const findPetsByStatusQuery = defineQueryOptions(
+ (options: Options) => ({
+ key: createQueryKey('findPetsByStatus', options),
+ query: async (context) => {
+ const { data } = await findPetsByStatus({
+ ...options,
+ ...context,
+ throwOnError: true
+ })
+ return data
+ }
+ })
+)
/**
* Finds Pets by tags.
* Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
*/
-export const findPetsByTagsQuery = (options: Options) => ({
- key: ['findPetsByTags', options?.path],
- query: async (context: { signal: AbortSignal }) => {
+export const findPetsByTagsQuery = defineQueryOptions((options: Options) => ({
+ key: createQueryKey('findPetsByTags', options),
+ query: async (context) => {
const { data } = await findPetsByTags({
...options,
- signal: context.signal,
+ ...context,
throwOnError: true
})
return data
}
-})
+}))
/**
* Deletes a pet.
* Delete a pet.
*/
-export const deletePetMutation = () => ({
- mutation: async (options: Options) => {
- const { data } = await deletePet(options)
+export const deletePetMutation = (
+ options?: Partial>
+): UseMutationOptions, Error> => ({
+ mutation: async (fnOptions) => {
+ const { data } = await deletePet({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ })
return data
}
})
@@ -113,25 +184,31 @@ export const deletePetMutation = () => ({
* Find pet by ID.
* Returns a single pet.
*/
-export const getPetByIdQuery = (options: Options) => ({
- key: ['getPetById', options?.path],
- query: async (context: { signal: AbortSignal }) => {
+export const getPetByIdQuery = defineQueryOptions((options: Options) => ({
+ key: createQueryKey('getPetById', options),
+ query: async (context) => {
const { data } = await getPetById({
...options,
- signal: context.signal,
+ ...context,
throwOnError: true
})
return data
}
-})
+}))
/**
* Updates a pet in the store with form data.
* Updates a pet resource based on the form data.
*/
-export const updatePetWithFormMutation = () => ({
- mutation: async (options: Options) => {
- const { data } = await updatePetWithForm(options)
+export const updatePetWithFormMutation = (
+ options?: Partial>
+): UseMutationOptions, Error> => ({
+ mutation: async (fnOptions) => {
+ const { data } = await updatePetWithForm({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ })
return data
}
})
@@ -140,9 +217,15 @@ export const updatePetWithFormMutation = () => ({
* Uploads an image.
* Upload image of the pet.
*/
-export const uploadFileMutation = () => ({
- mutation: async (options: Options) => {
- const { data } = await uploadFile(options)
+export const uploadFileMutation = (
+ options?: Partial>
+): UseMutationOptions, Error> => ({
+ mutation: async (fnOptions) => {
+ const { data } = await uploadFile({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ })
return data
}
})
@@ -151,25 +234,31 @@ export const uploadFileMutation = () => ({
* Returns pet inventories by status.
* Returns a map of status codes to quantities.
*/
-export const getInventoryQuery = (options?: Options) => ({
- key: ['getInventory', options?.path],
- query: async (context: { signal: AbortSignal }) => {
+export const getInventoryQuery = defineQueryOptions((options?: Options) => ({
+ key: createQueryKey('getInventory', options),
+ query: async (context) => {
const { data } = await getInventory({
...options,
- signal: context.signal,
+ ...context,
throwOnError: true
})
return data
}
-})
+}))
/**
* Place an order for a pet.
* Place a new order in the store.
*/
-export const placeOrderMutation = () => ({
- mutation: async (options: Options) => {
- const { data } = await placeOrder(options)
+export const placeOrderMutation = (
+ options?: Partial>
+): UseMutationOptions, Error> => ({
+ mutation: async (fnOptions) => {
+ const { data } = await placeOrder({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ })
return data
}
})
@@ -178,9 +267,15 @@ export const placeOrderMutation = () => ({
* Delete purchase order by identifier.
* For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors.
*/
-export const deleteOrderMutation = () => ({
- mutation: async (options: Options) => {
- const { data } = await deleteOrder(options)
+export const deleteOrderMutation = (
+ options?: Partial>
+): UseMutationOptions, Error> => ({
+ mutation: async (fnOptions) => {
+ const { data } = await deleteOrder({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ })
return data
}
})
@@ -189,25 +284,31 @@ export const deleteOrderMutation = () => ({
* Find purchase order by ID.
* For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.
*/
-export const getOrderByIdQuery = (options: Options) => ({
- key: ['getOrderById', options?.path],
- query: async (context: { signal: AbortSignal }) => {
+export const getOrderByIdQuery = defineQueryOptions((options: Options) => ({
+ key: createQueryKey('getOrderById', options),
+ query: async (context) => {
const { data } = await getOrderById({
...options,
- signal: context.signal,
+ ...context,
throwOnError: true
})
return data
}
-})
+}))
/**
* Create user.
* This can only be done by the logged in user.
*/
-export const createUserMutation = () => ({
- mutation: async (options: Options) => {
- const { data } = await createUser(options)
+export const createUserMutation = (
+ options?: Partial>
+): UseMutationOptions, Error> => ({
+ mutation: async (fnOptions) => {
+ const { data } = await createUser({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ })
return data
}
})
@@ -216,9 +317,19 @@ export const createUserMutation = () => ({
* Creates list of users with given input array.
* Creates list of users with given input array.
*/
-export const createUsersWithListInputMutation = () => ({
- mutation: async (options: Options) => {
- const { data } = await createUsersWithListInput(options)
+export const createUsersWithListInputMutation = (
+ options?: Partial>
+): UseMutationOptions<
+ CreateUsersWithListInputResponse,
+ Options,
+ Error
+> => ({
+ mutation: async (fnOptions) => {
+ const { data } = await createUsersWithListInput({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ })
return data
}
})
@@ -227,41 +338,47 @@ export const createUsersWithListInputMutation = () => ({
* Logs user into the system.
* Log into the system.
*/
-export const loginUserQuery = (options?: Options) => ({
- key: ['loginUser', options?.path],
- query: async (context: { signal: AbortSignal }) => {
+export const loginUserQuery = defineQueryOptions((options?: Options) => ({
+ key: createQueryKey('loginUser', options),
+ query: async (context) => {
const { data } = await loginUser({
...options,
- signal: context.signal,
+ ...context,
throwOnError: true
})
return data
}
-})
+}))
/**
* Logs out current logged in user session.
* Log user out of the system.
*/
-export const logoutUserQuery = (options?: Options) => ({
- key: ['logoutUser', options?.path],
- query: async (context: { signal: AbortSignal }) => {
+export const logoutUserQuery = defineQueryOptions((options?: Options) => ({
+ key: createQueryKey('logoutUser', options),
+ query: async (context) => {
const { data } = await logoutUser({
...options,
- signal: context.signal,
+ ...context,
throwOnError: true
})
return data
}
-})
+}))
/**
* Delete user resource.
* This can only be done by the logged in user.
*/
-export const deleteUserMutation = () => ({
- mutation: async (options: Options) => {
- const { data } = await deleteUser(options)
+export const deleteUserMutation = (
+ options?: Partial>
+): UseMutationOptions, Error> => ({
+ mutation: async (fnOptions) => {
+ const { data } = await deleteUser({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ })
return data
}
})
@@ -270,25 +387,31 @@ export const deleteUserMutation = () => ({
* Get user by user name.
* Get user detail based on username.
*/
-export const getUserByNameQuery = (options: Options) => ({
- key: ['getUserByName', options?.path],
- query: async (context: { signal: AbortSignal }) => {
+export const getUserByNameQuery = defineQueryOptions((options: Options) => ({
+ key: createQueryKey('getUserByName', options),
+ query: async (context) => {
const { data } = await getUserByName({
...options,
- signal: context.signal,
+ ...context,
throwOnError: true
})
return data
}
-})
+}))
/**
* Update user resource.
* This can only be done by the logged in user.
*/
-export const updateUserMutation = () => ({
- mutation: async (options: Options) => {
- const { data } = await updateUser(options)
+export const updateUserMutation = (
+ options?: Partial>
+): UseMutationOptions, Error> => ({
+ mutation: async (fnOptions) => {
+ const { data } = await updateUser({
+ ...options,
+ ...fnOptions,
+ throwOnError: true
+ })
return data
}
})
diff --git a/examples/openapi-ts-pinia-colada/src/client/client.gen.ts b/examples/openapi-ts-pinia-colada/src/client/client.gen.ts
index 1984dc18c0..3431b130d4 100644
--- a/examples/openapi-ts-pinia-colada/src/client/client.gen.ts
+++ b/examples/openapi-ts-pinia-colada/src/client/client.gen.ts
@@ -1,12 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts
-import {
- type ClientOptions as DefaultClientOptions,
- type Config,
- createClient,
- createConfig
-} from './client'
-import type { ClientOptions } from './types.gen'
+import { type ClientOptions, type Config, createClient, createConfig } from './client'
+import type { ClientOptions as ClientOptions2 } from './types.gen'
/**
* The `createClientConfig()` function will be called on client initialization
@@ -16,12 +11,12 @@ import type { ClientOptions } from './types.gen'
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
-export type CreateClientConfig = (
- override?: Config
-) => Config & T>
+export type CreateClientConfig = (
+ override?: Config
+) => Config & T>
export const client = createClient(
- createConfig({
+ createConfig({
baseUrl: 'https://petstore3.swagger.io/api/v3'
})
)
diff --git a/examples/openapi-ts-pinia-colada/src/client/client/client.gen.ts b/examples/openapi-ts-pinia-colada/src/client/client/client.gen.ts
index 65910541f0..4a4fc4648f 100644
--- a/examples/openapi-ts-pinia-colada/src/client/client/client.gen.ts
+++ b/examples/openapi-ts-pinia-colada/src/client/client/client.gen.ts
@@ -2,6 +2,7 @@
import { createSseClient } from '../core/serverSentEvents.gen'
import type { HttpMethod } from '../core/types.gen'
+import { getValidRequestBody } from '../core/utils.gen'
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'
import {
buildUrl,
@@ -50,12 +51,12 @@ export const createClient = (config: Config = {}): Client => {
await opts.requestValidator(opts)
}
- if (opts.body && opts.bodySerializer) {
+ if (opts.body !== undefined && opts.bodySerializer) {
opts.serializedBody = opts.bodySerializer(opts.body)
}
// remove Content-Type header if body is empty to avoid sending invalid requests
- if (opts.serializedBody === undefined || opts.serializedBody === '') {
+ if (opts.body === undefined || opts.serializedBody === '') {
opts.headers.delete('Content-Type')
}
@@ -70,12 +71,12 @@ export const createClient = (config: Config = {}): Client => {
const requestInit: ReqInit = {
redirect: 'follow',
...opts,
- body: opts.serializedBody
+ body: getValidRequestBody(opts)
}
let request = new Request(url, requestInit)
- for (const fn of interceptors.request._fns) {
+ for (const fn of interceptors.request.fns) {
if (fn) {
request = await fn(request, opts)
}
@@ -86,7 +87,7 @@ export const createClient = (config: Config = {}): Client => {
const _fetch = opts.fetch!
let response = await _fetch(request)
- for (const fn of interceptors.response._fns) {
+ for (const fn of interceptors.response.fns) {
if (fn) {
response = await fn(response, request, opts)
}
@@ -98,20 +99,38 @@ export const createClient = (config: Config = {}): Client => {
}
if (response.ok) {
+ const parseAs =
+ (opts.parseAs === 'auto'
+ ? getParseAs(response.headers.get('Content-Type'))
+ : opts.parseAs) ?? 'json'
+
if (response.status === 204 || response.headers.get('Content-Length') === '0') {
+ let emptyData: any
+ switch (parseAs) {
+ case 'arrayBuffer':
+ case 'blob':
+ case 'text':
+ emptyData = await response[parseAs]()
+ break
+ case 'formData':
+ emptyData = new FormData()
+ break
+ case 'stream':
+ emptyData = response.body
+ break
+ case 'json':
+ default:
+ emptyData = {}
+ break
+ }
return opts.responseStyle === 'data'
- ? {}
+ ? emptyData
: {
- data: {},
+ data: emptyData,
...result
}
}
- const parseAs =
- (opts.parseAs === 'auto'
- ? getParseAs(response.headers.get('Content-Type'))
- : opts.parseAs) ?? 'json'
-
let data: any
switch (parseAs) {
case 'arrayBuffer':
@@ -160,7 +179,7 @@ export const createClient = (config: Config = {}): Client => {
const error = jsonError ?? textError
let finalError = error
- for (const fn of interceptors.error._fns) {
+ for (const fn of interceptors.error.fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string
}
@@ -191,6 +210,15 @@ export const createClient = (config: Config = {}): Client => {
body: opts.body as BodyInit | null | undefined,
headers: opts.headers as unknown as Record,
method,
+ onRequest: async (url, init) => {
+ let request = new Request(url, init)
+ for (const fn of interceptors.request.fns) {
+ if (fn) {
+ request = await fn(request, opts)
+ }
+ }
+ return request
+ },
url
})
}
diff --git a/examples/openapi-ts-pinia-colada/src/client/client/types.gen.ts b/examples/openapi-ts-pinia-colada/src/client/client/types.gen.ts
index 273baf7860..b97f75e3fc 100644
--- a/examples/openapi-ts-pinia-colada/src/client/client/types.gen.ts
+++ b/examples/openapi-ts-pinia-colada/src/client/client/types.gen.ts
@@ -20,7 +20,7 @@ export interface Config
*
* @default globalThis.fetch
*/
- fetch?: (request: Request) => ReturnType
+ fetch?: typeof fetch
/**
* Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect.
diff --git a/examples/openapi-ts-pinia-colada/src/client/client/utils.gen.ts b/examples/openapi-ts-pinia-colada/src/client/client/utils.gen.ts
index f701b3d78c..b42b5d9516 100644
--- a/examples/openapi-ts-pinia-colada/src/client/client/utils.gen.ts
+++ b/examples/openapi-ts-pinia-colada/src/client/client/utils.gen.ts
@@ -176,16 +176,24 @@ export const mergeConfigs = (a: Config, b: Config): Config => {
return config
}
+const headersEntries = (headers: Headers): Array<[string, string]> => {
+ const entries: Array<[string, string]> = []
+ headers.forEach((value, key) => {
+ entries.push([key, value])
+ })
+ return entries
+}
+
export const mergeHeaders = (
...headers: Array['headers'] | undefined>
): Headers => {
const mergedHeaders = new Headers()
for (const header of headers) {
- if (!header || typeof header !== 'object') {
+ if (!header) {
continue
}
- const iterator = header instanceof Headers ? header.entries() : Object.entries(header)
+ const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header)
for (const [key, value] of iterator) {
if (value === null) {
@@ -223,61 +231,58 @@ type ResInterceptor = (
) => Res | Promise
class Interceptors {
- _fns: (Interceptor | null)[]
+ fns: Array = []
- constructor() {
- this._fns = []
+ clear(): void {
+ this.fns = []
}
- clear() {
- this._fns = []
- }
-
- getInterceptorIndex(id: number | Interceptor): number {
- if (typeof id === 'number') {
- return this._fns[id] ? id : -1
- } else {
- return this._fns.indexOf(id)
+ eject(id: number | Interceptor): void {
+ const index = this.getInterceptorIndex(id)
+ if (this.fns[index]) {
+ this.fns[index] = null
}
}
- exists(id: number | Interceptor) {
+
+ exists(id: number | Interceptor): boolean {
const index = this.getInterceptorIndex(id)
- return !!this._fns[index]
+ return Boolean(this.fns[index])
}
- eject(id: number | Interceptor) {
- const index = this.getInterceptorIndex(id)
- if (this._fns[index]) {
- this._fns[index] = null
+ getInterceptorIndex(id: number | Interceptor): number {
+ if (typeof id === 'number') {
+ return this.fns[id] ? id : -1
}
+ return this.fns.indexOf(id)
}
- update(id: number | Interceptor, fn: Interceptor) {
+ update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false {
const index = this.getInterceptorIndex(id)
- if (this._fns[index]) {
- this._fns[index] = fn
+ if (this.fns[index]) {
+ this.fns[index] = fn
return id
- } else {
- return false
}
+ return false
}
- use(fn: Interceptor) {
- this._fns = [...this._fns, fn]
- return this._fns.length - 1
+ use(fn: Interceptor): number {
+ this.fns.push(fn)
+ return this.fns.length - 1
}
}
-// `createInterceptors()` response, meant for external use as it does not
-// expose internals
export interface Middleware {
- error: Pick>, 'eject' | 'use'>
- request: Pick>, 'eject' | 'use'>
- response: Pick>, 'eject' | 'use'>
+ error: Interceptors>
+ request: Interceptors>
+ response: Interceptors>
}
-// do not add `Middleware` as return type so we can use _fns internally
-export const createInterceptors = () => ({
+export const createInterceptors = (): Middleware<
+ Req,
+ Res,
+ Err,
+ Options
+> => ({
error: new Interceptors>(),
request: new Interceptors>(),
response: new Interceptors>()
diff --git a/examples/openapi-ts-pinia-colada/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-pinia-colada/src/client/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..7e9d0d1ec8
--- /dev/null
+++ b/examples/openapi-ts-pinia-colada/src/client/core/queryKeySerializer.gen.ts
@@ -0,0 +1,117 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue }
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
+ return undefined
+ }
+ if (typeof value === 'bigint') {
+ return value.toString()
+ }
+ if (value instanceof Date) {
+ return value.toISOString()
+ }
+ return value
+}
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer)
+ if (json === undefined) {
+ return undefined
+ }
+ return JSON.parse(json) as JsonValue
+ } catch {
+ return undefined
+ }
+}
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false
+ }
+ const prototype = Object.getPrototypeOf(value as object)
+ return prototype === Object.prototype || prototype === null
+}
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b))
+ const result: Record = {}
+
+ for (const [key, value] of entries) {
+ const existing = result[key]
+ if (existing === undefined) {
+ result[key] = value
+ continue
+ }
+
+ if (Array.isArray(existing)) {
+ ;(existing as string[]).push(value)
+ } else {
+ result[key] = [existing, value]
+ }
+ }
+
+ return result
+}
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => {
+ if (value === null) {
+ return null
+ }
+
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
+ return value
+ }
+
+ if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
+ return undefined
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString()
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString()
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value)
+ }
+
+ if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) {
+ return serializeSearchParams(value)
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value)
+ }
+
+ return undefined
+}
diff --git a/examples/openapi-ts-pinia-colada/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-pinia-colada/src/client/core/serverSentEvents.gen.ts
index de07ebaf27..372e50cc26 100644
--- a/examples/openapi-ts-pinia-colada/src/client/core/serverSentEvents.gen.ts
+++ b/examples/openapi-ts-pinia-colada/src/client/core/serverSentEvents.gen.ts
@@ -4,6 +4,17 @@ import type { Config } from './types.gen'
export type ServerSentEventsOptions = Omit &
Pick & {
+ /**
+ * Fetch API implementation. You can use this option to provide a custom
+ * fetch instance.
+ *
+ * @default globalThis.fetch
+ */
+ fetch?: typeof fetch
+ /**
+ * Implementing clients can call request interceptors inside this hook.
+ */
+ onRequest?: (url: string, init: RequestInit) => Promise
/**
* Callback invoked when a network or parsing error occurs during streaming.
*
@@ -21,6 +32,7 @@ export type ServerSentEventsOptions = Omit) => void
+ serializedBody?: RequestInit['body']
/**
* Default retry delay in milliseconds.
*
@@ -68,6 +80,7 @@ export type ServerSentEventsResult({
+ onRequest,
onSseError,
onSseEvent,
responseTransformer,
@@ -103,7 +116,21 @@ export const createSseClient = ({
}
try {
- const response = await fetch(url, { ...options, headers, signal })
+ const requestInit: RequestInit = {
+ redirect: 'follow',
+ ...options,
+ body: options.serializedBody,
+ headers,
+ signal
+ }
+ let request = new Request(url, requestInit)
+ if (onRequest) {
+ request = await onRequest(url, requestInit)
+ }
+ // fetch must be assigned here, otherwise it would throw the error:
+ // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
+ const _fetch = options.fetch ?? globalThis.fetch
+ const response = await _fetch(request)
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`)
diff --git a/examples/openapi-ts-pinia-colada/src/client/core/utils.gen.ts b/examples/openapi-ts-pinia-colada/src/client/core/utils.gen.ts
index b99c527aac..bd078be240 100644
--- a/examples/openapi-ts-pinia-colada/src/client/core/utils.gen.ts
+++ b/examples/openapi-ts-pinia-colada/src/client/core/utils.gen.ts
@@ -1,6 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts
-import type { QuerySerializer } from './bodySerializer.gen'
+import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'
import {
type ArraySeparatorStyle,
serializeArrayParam,
@@ -109,3 +109,32 @@ export const getUrl = ({
}
return url
}
+
+export function getValidRequestBody(options: {
+ body?: unknown
+ bodySerializer?: BodySerializer | null
+ serializedBody?: unknown
+}) {
+ const hasBody = options.body !== undefined
+ const isSerializedBody = hasBody && options.bodySerializer
+
+ if (isSerializedBody) {
+ if ('serializedBody' in options) {
+ const hasSerializedBody =
+ options.serializedBody !== undefined && options.serializedBody !== ''
+
+ return hasSerializedBody ? options.serializedBody : null
+ }
+
+ // not all clients implement a serializedBody property (i.e. client-axios)
+ return options.body !== '' ? options.body : null
+ }
+
+ // plain/text body
+ if (hasBody) {
+ return options.body
+ }
+
+ // no body was provided
+ return undefined
+}
diff --git a/examples/openapi-ts-pinia-colada/src/client/index.ts b/examples/openapi-ts-pinia-colada/src/client/index.ts
index 2543e57505..b190962d22 100644
--- a/examples/openapi-ts-pinia-colada/src/client/index.ts
+++ b/examples/openapi-ts-pinia-colada/src/client/index.ts
@@ -1,4 +1,5 @@
// This file is auto-generated by @hey-api/openapi-ts
+
export * from './@pinia/colada.gen'
export * from './sdk.gen'
-export * from './types.gen'
+export type * from './types.gen'
diff --git a/examples/openapi-ts-pinia-colada/src/client/sdk.gen.ts b/examples/openapi-ts-pinia-colada/src/client/sdk.gen.ts
index f373c7cf6f..37778ac7f8 100644
--- a/examples/openapi-ts-pinia-colada/src/client/sdk.gen.ts
+++ b/examples/openapi-ts-pinia-colada/src/client/sdk.gen.ts
@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts
-import type { Client, Options as ClientOptions, TDataShape } from './client'
-import { client as _heyApiClient } from './client.gen'
+import type { Client, Options as Options2, TDataShape } from './client'
+import { client } from './client.gen'
import type {
AddPetData,
AddPetErrors,
@@ -65,7 +65,7 @@ import type {
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean
-> = ClientOptions & {
+> = Options2 & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
@@ -86,7 +86,7 @@ export type Options<
export const addPet = (
options: Options
) =>
- (options.client ?? _heyApiClient).post({
+ (options.client ?? client).post({
security: [
{
scheme: 'bearer',
@@ -108,7 +108,7 @@ export const addPet = (
export const updatePet = (
options: Options
) =>
- (options.client ?? _heyApiClient).put({
+ (options.client ?? client).put({
security: [
{
scheme: 'bearer',
@@ -130,11 +130,7 @@ export const updatePet = (
export const findPetsByStatus = (
options: Options
) =>
- (options.client ?? _heyApiClient).get<
- FindPetsByStatusResponses,
- FindPetsByStatusErrors,
- ThrowOnError
- >({
+ (options.client ?? client).get({
security: [
{
scheme: 'bearer',
@@ -152,11 +148,7 @@ export const findPetsByStatus = (
export const findPetsByTags = (
options: Options
) =>
- (options.client ?? _heyApiClient).get<
- FindPetsByTagsResponses,
- FindPetsByTagsErrors,
- ThrowOnError
- >({
+ (options.client ?? client).get({
security: [
{
scheme: 'bearer',
@@ -174,7 +166,7 @@ export const findPetsByTags = (
export const deletePet = (
options: Options
) =>
- (options.client ?? _heyApiClient).delete({
+ (options.client ?? client).delete({
security: [
{
scheme: 'bearer',
@@ -192,7 +184,7 @@ export const deletePet = (
export const getPetById = (
options: Options
) =>
- (options.client ?? _heyApiClient).get({
+ (options.client ?? client).get({
security: [
{
name: 'api_key',
@@ -214,7 +206,7 @@ export const getPetById = (
export const updatePetWithForm = (
options: Options
) =>
- (options.client ?? _heyApiClient).post<
+ (options.client ?? client).post<
UpdatePetWithFormResponses,
UpdatePetWithFormErrors,
ThrowOnError
@@ -236,7 +228,7 @@ export const updatePetWithForm = (
export const uploadFile = (
options: Options
) =>
- (options.client ?? _heyApiClient).post({
+ (options.client ?? client).post({
bodySerializer: null,
security: [
{
@@ -259,7 +251,7 @@ export const uploadFile = (
export const getInventory = (
options?: Options
) =>
- (options?.client ?? _heyApiClient).get({
+ (options?.client ?? client).get({
security: [
{
name: 'api_key',
@@ -277,7 +269,7 @@ export const getInventory = (
export const placeOrder = (
options?: Options
) =>
- (options?.client ?? _heyApiClient).post({
+ (options?.client ?? client).post({
url: '/store/order',
...options,
headers: {
@@ -293,7 +285,7 @@ export const placeOrder = (
export const deleteOrder = (
options: Options
) =>
- (options.client ?? _heyApiClient).delete({
+ (options.client ?? client).delete({
url: '/store/order/{orderId}',
...options
})
@@ -305,7 +297,7 @@ export const deleteOrder = (
export const getOrderById = (
options: Options
) =>
- (options.client ?? _heyApiClient).get({
+ (options.client ?? client).get({
url: '/store/order/{orderId}',
...options
})
@@ -317,7 +309,7 @@ export const getOrderById = (
export const createUser = (
options?: Options
) =>
- (options?.client ?? _heyApiClient).post({
+ (options?.client ?? client).post({
url: '/user',
...options,
headers: {
@@ -333,7 +325,7 @@ export const createUser = (
export const createUsersWithListInput = (
options?: Options
) =>
- (options?.client ?? _heyApiClient).post<
+ (options?.client ?? client).post<
CreateUsersWithListInputResponses,
CreateUsersWithListInputErrors,
ThrowOnError
@@ -353,7 +345,7 @@ export const createUsersWithListInput = (
export const loginUser = (
options?: Options
) =>
- (options?.client ?? _heyApiClient).get({
+ (options?.client ?? client).get({
url: '/user/login',
...options
})
@@ -365,7 +357,7 @@ export const loginUser = (
export const logoutUser = (
options?: Options
) =>
- (options?.client ?? _heyApiClient).get({
+ (options?.client ?? client).get({
url: '/user/logout',
...options
})
@@ -377,7 +369,7 @@ export const logoutUser = (
export const deleteUser = (
options: Options
) =>
- (options.client ?? _heyApiClient).delete({
+ (options.client ?? client).delete({
url: '/user/{username}',
...options
})
@@ -389,7 +381,7 @@ export const deleteUser = (
export const getUserByName = (
options: Options
) =>
- (options.client ?? _heyApiClient).get({
+ (options.client ?? client).get({
url: '/user/{username}',
...options
})
@@ -401,7 +393,7 @@ export const getUserByName = (
export const updateUser = (
options: Options
) =>
- (options.client ?? _heyApiClient).put({
+ (options.client ?? client).put({
url: '/user/{username}',
...options,
headers: {
diff --git a/examples/openapi-ts-pinia-colada/src/client/types.gen.ts b/examples/openapi-ts-pinia-colada/src/client/types.gen.ts
index f7044e662a..99ce8e7d8e 100644
--- a/examples/openapi-ts-pinia-colada/src/client/types.gen.ts
+++ b/examples/openapi-ts-pinia-colada/src/client/types.gen.ts
@@ -1,5 +1,9 @@
// This file is auto-generated by @hey-api/openapi-ts
+export type ClientOptions = {
+ baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {})
+}
+
export type Order = {
complete?: boolean
id?: number
@@ -687,7 +691,3 @@ export type UpdateUserResponses = {
*/
200: unknown
}
-
-export type ClientOptions = {
- baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {})
-}
diff --git a/examples/openapi-ts-pinia-colada/src/views/PiniaColadaExample.vue b/examples/openapi-ts-pinia-colada/src/views/PiniaColadaExample.vue
index 6930fc78a2..a0e5557da4 100644
--- a/examples/openapi-ts-pinia-colada/src/views/PiniaColadaExample.vue
+++ b/examples/openapi-ts-pinia-colada/src/views/PiniaColadaExample.vue
@@ -1,11 +1,12 @@
@@ -218,4 +207,6 @@ watch(error, (error) => {
+
+
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/body-response-text-plain/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/body-response-text-plain/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/body-response-text-plain/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/body-response-text-plain/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/body-response-text-plain/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/body-response-text-plain/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/body-response-text-plain/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/form-data/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/form-data/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/form-data/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/form-data/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/form-data/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/form-data/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/form-data/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default-class/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default-class/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default-class/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default-class/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default-class/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default-class/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default-class/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@angular/common/default/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes-instance/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes-instance/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes-instance/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes-instance/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes-instance/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes-instance/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes-instance/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/client-fetch/sdk-nested-classes/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/default/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/default/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/default/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/default/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/default/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/default/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/default/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/instance/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/instance/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/instance/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/instance/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/instance/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/instance/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/instance/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/throwOnError/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/throwOnError/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/throwOnError/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/throwOnError/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/throwOnError/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/throwOnError/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/sdk/throwOnError/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-valibot/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-valibot/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-valibot/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-valibot/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-valibot/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-valibot/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-valibot/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-zod/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-zod/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-zod/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-zod/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-zod/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-zod/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-zod/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-custom-name/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-custom-name/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-custom-name/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-custom-name/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-custom-name/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-custom-name/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-custom-name/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-ignore/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-ignore/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-ignore/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-ignore/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-ignore/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-ignore/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/typescript/transforms-read-write-ignore/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/asClass/@pinia/colada.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/asClass/@pinia/colada.gen.ts
index a8d9f65a68..1d845eca9b 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/asClass/@pinia/colada.gen.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/asClass/@pinia/colada.gen.ts
@@ -1,16 +1,17 @@
// This file is auto-generated by @hey-api/openapi-ts
-import type { _JSONValue, UseMutationOptions, UseQueryOptions } from '@pinia/colada';
+import { type _JSONValue, defineQueryOptions, type UseMutationOptions } from '@pinia/colada';
+import { serializeQueryKeyValue } from '../client';
import { client } from '../client.gen';
import { BarBazService, FooBazService, type Options } from '../sdk.gen';
-import type { FooBarPostData, FooBarPostResponse, FooBarPutData, FooBarPutResponse, FooPostData, FooPostResponse, FooPutData, FooPutResponse, GetFooBarData, GetFooBarResponse, GetFooData, GetFooResponse } from '../types.gen';
+import type { FooBarPostData, FooBarPostResponse, FooBarPutData, FooBarPutResponse, FooPostData, FooPostResponse, FooPutData, FooPutResponse, GetFooBarData, GetFooData } from '../types.gen';
export type QueryKey = [
- Pick & {
+ Pick & {
_id: string;
baseUrl?: _JSONValue;
- headers?: _JSONValue;
+ body?: _JSONValue;
query?: _JSONValue;
tags?: _JSONValue;
}
@@ -21,19 +22,22 @@ const createQueryKey = (id: string, options?: TOptions
] => {
const params: QueryKey[0] = { _id: id, baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl } as QueryKey[0];
if (tags) {
- params.tags = tags as unknown as undefined;
+ params.tags = tags as unknown as _JSONValue;
}
- if (options?.body) {
- params.body = options.body;
- }
- if (options?.headers) {
- params.headers = options.headers as unknown as undefined;
+ if (options?.body !== undefined) {
+ const normalizedBody = serializeQueryKeyValue(options.body);
+ if (normalizedBody !== undefined) {
+ params.body = normalizedBody;
+ }
}
if (options?.path) {
params.path = options.path;
}
- if (options?.query) {
- params.query = options.query as unknown as undefined;
+ if (options?.query !== undefined) {
+ const normalizedQuery = serializeQueryKeyValue(options.query);
+ if (normalizedQuery !== undefined) {
+ params.query = normalizedQuery;
+ }
}
return [
params
@@ -42,19 +46,17 @@ const createQueryKey = (id: string, options?: TOptions
export const getFooQueryKey = (options?: Options) => createQueryKey('getFoo', options);
-export const getFooQuery = (options?: Options): UseQueryOptions => {
- return {
- key: getFooQueryKey(options),
- query: async (context) => {
- const { data } = await FooBazService.getFoo({
- ...options,
- ...context,
- throwOnError: true
- });
- return data;
- }
- };
-};
+export const getFooQuery = defineQueryOptions((options?: Options) => ({
+ key: getFooQueryKey(options),
+ query: async (context) => {
+ const { data } = await FooBazService.getFoo({
+ ...options,
+ ...context,
+ throwOnError: true
+ });
+ return data;
+ }
+}));
export const fooPostMutation = (options?: Partial>): UseMutationOptions, Error> => {
return {
@@ -84,19 +86,17 @@ export const fooPutMutation = (options?: Partial>): UseMutat
export const getFooBarQueryKey = (options?: Options) => createQueryKey('getFooBar', options);
-export const getFooBarQuery = (options?: Options): UseQueryOptions => {
- return {
- key: getFooBarQueryKey(options),
- query: async (context) => {
- const { data } = await BarBazService.getFooBar({
- ...options,
- ...context,
- throwOnError: true
- });
- return data;
- }
- };
-};
+export const getFooBarQuery = defineQueryOptions((options?: Options) => ({
+ key: getFooBarQueryKey(options),
+ query: async (context) => {
+ const { data } = await BarBazService.getFooBar({
+ ...options,
+ ...context,
+ throwOnError: true
+ });
+ return data;
+ }
+}));
export const fooBarPostMutation = (options?: Partial>): UseMutationOptions, Error> => {
return {
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/asClass/client/index.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/asClass/client/index.ts
index 318a84b6a8..cbf8dfeedb 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/asClass/client/index.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/asClass/client/index.ts
@@ -8,6 +8,7 @@ export {
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
+export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from './client.gen';
export type {
Client,
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/asClass/core/queryKeySerializer.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/asClass/core/queryKeySerializer.gen.ts
new file mode 100644
index 0000000000..d3bb68396e
--- /dev/null
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/asClass/core/queryKeySerializer.gen.ts
@@ -0,0 +1,136 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+/**
+ * JSON-friendly union that mirrors what Pinia Colada can hash.
+ */
+export type JsonValue =
+ | null
+ | string
+ | number
+ | boolean
+ | JsonValue[]
+ | { [key: string]: JsonValue };
+
+/**
+ * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
+ */
+export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+ return value;
+};
+
+/**
+ * Safely stringifies a value and parses it back into a JsonValue.
+ */
+export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
+ try {
+ const json = JSON.stringify(input, queryKeyJsonReplacer);
+ if (json === undefined) {
+ return undefined;
+ }
+ return JSON.parse(json) as JsonValue;
+ } catch {
+ return undefined;
+ }
+};
+
+/**
+ * Detects plain objects (including objects with a null prototype).
+ */
+const isPlainObject = (value: unknown): value is Record => {
+ if (value === null || typeof value !== 'object') {
+ return false;
+ }
+ const prototype = Object.getPrototypeOf(value as object);
+ return prototype === Object.prototype || prototype === null;
+};
+
+/**
+ * Turns URLSearchParams into a sorted JSON object for deterministic keys.
+ */
+const serializeSearchParams = (params: URLSearchParams): JsonValue => {
+ const entries = Array.from(params.entries()).sort(([a], [b]) =>
+ a.localeCompare(b),
+ );
+ const result: Record = {};
+
+ for (const [key, value] of entries) {
+ const existing = result[key];
+ if (existing === undefined) {
+ result[key] = value;
+ continue;
+ }
+
+ if (Array.isArray(existing)) {
+ (existing as string[]).push(value);
+ } else {
+ result[key] = [existing, value];
+ }
+ }
+
+ return result;
+};
+
+/**
+ * Normalizes any accepted value into a JSON-friendly shape for query keys.
+ */
+export const serializeQueryKeyValue = (
+ value: unknown,
+): JsonValue | undefined => {
+ if (value === null) {
+ return null;
+ }
+
+ if (
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'boolean'
+ ) {
+ return value;
+ }
+
+ if (
+ value === undefined ||
+ typeof value === 'function' ||
+ typeof value === 'symbol'
+ ) {
+ return undefined;
+ }
+
+ if (typeof value === 'bigint') {
+ return value.toString();
+ }
+
+ if (value instanceof Date) {
+ return value.toISOString();
+ }
+
+ if (Array.isArray(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ if (
+ typeof URLSearchParams !== 'undefined' &&
+ value instanceof URLSearchParams
+ ) {
+ return serializeSearchParams(value);
+ }
+
+ if (isPlainObject(value)) {
+ return stringifyToJsonValue(value);
+ }
+
+ return undefined;
+};
diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/fetch/@pinia/colada.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/fetch/@pinia/colada.gen.ts
index d30902d0be..b25e9919a4 100644
--- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/fetch/@pinia/colada.gen.ts
+++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@pinia/colada/fetch/@pinia/colada.gen.ts
@@ -1,16 +1,17 @@
// This file is auto-generated by @hey-api/openapi-ts
-import type { _JSONValue, UseMutationOptions, UseQueryOptions } from '@pinia/colada';
+import { type _JSONValue, defineQueryOptions, type UseMutationOptions } from '@pinia/colada';
+import { serializeQueryKeyValue } from '../client';
import { client } from '../client.gen';
import { callToTestOrderOfParams, callWithDefaultOptionalParameters, callWithDefaultParameters, callWithDescriptions, callWithDuplicateResponses, callWithNoContentResponse, callWithParameters, callWithResponse, callWithResponseAndNoContentResponse, callWithResponses, callWithResultFromHeader, callWithWeirdParameterNames, collectionFormat, complexTypes, deleteCallWithoutParametersAndResponse, dummyA, dummyB, duplicateName, duplicateName2, duplicateName3, duplicateName4, fooWow, getCallWithoutParametersAndResponse, nonAsciiæøåÆøÅöôêÊå—符串, type Options, patchApiVbyApiVersionNoTag, patchCallWithoutParametersAndResponse, postApiVbyApiVersionBody, postCallWithoutParametersAndResponse, putCallWithoutParametersAndResponse, serviceWithEmptyTag, testErrorCode, types } from '../sdk.gen';
-import type { CallToTestOrderOfParamsData, CallWithDefaultOptionalParametersData, CallWithDefaultParametersData, CallWithDescriptionsData, CallWithDuplicateResponsesData, CallWithDuplicateResponsesError, CallWithDuplicateResponsesResponse, CallWithNoContentResponseData, CallWithParametersData, CallWithResponseAndNoContentResponseData, CallWithResponseAndNoContentResponseResponse, CallWithResponseData, CallWithResponseResponse, CallWithResponsesData, CallWithResponsesError, CallWithResponsesResponse, CallWithResultFromHeaderData, CallWithWeirdParameterNamesData, CollectionFormatData, ComplexTypesData, ComplexTypesResponse, DeleteCallWithoutParametersAndResponseData, DummyAData, DummyBData, DuplicateName2Data, DuplicateName3Data, DuplicateName4Data, DuplicateNameData, FooWowData, GetCallWithoutParametersAndResponseData, NonAsciiæøåÆøÅöôêÊå—符串Data, NonAsciiæøåÆøÅöôêÊå—符串Response, PatchApiVbyApiVersionNoTagData, PatchCallWithoutParametersAndResponseData, PostApiVbyApiVersionBodyData, PostApiVbyApiVersionBodyError, PostApiVbyApiVersionBodyResponse, PostCallWithoutParametersAndResponseData, PutCallWithoutParametersAndResponseData, ServiceWithEmptyTagData, TestErrorCodeData, TypesData, TypesResponse } from '../types.gen';
+import type { CallToTestOrderOfParamsData, CallWithDefaultOptionalParametersData, CallWithDefaultParametersData, CallWithDescriptionsData, CallWithDuplicateResponsesData, CallWithDuplicateResponsesError, CallWithDuplicateResponsesResponse, CallWithNoContentResponseData, CallWithParametersData, CallWithResponseAndNoContentResponseData, CallWithResponseData, CallWithResponsesData, CallWithResponsesError, CallWithResponsesResponse, CallWithResultFromHeaderData, CallWithWeirdParameterNamesData, CollectionFormatData, ComplexTypesData, DeleteCallWithoutParametersAndResponseData, DummyAData, DummyBData, DuplicateName2Data, DuplicateName3Data, DuplicateName4Data, DuplicateNameData, FooWowData, GetCallWithoutParametersAndResponseData, NonAsciiæøåÆøÅöôêÊå—符串Data, NonAsciiæøåÆøÅöôêÊå—符串Response, PatchApiVbyApiVersionNoTagData, PatchCallWithoutParametersAndResponseData, PostApiVbyApiVersionBodyData, PostApiVbyApiVersionBodyError, PostApiVbyApiVersionBodyResponse, PostCallWithoutParametersAndResponseData, PutCallWithoutParametersAndResponseData, ServiceWithEmptyTagData, TestErrorCodeData, TypesData } from '../types.gen';
export type QueryKey = [
- Pick & {
+ Pick & {
_id: string;
baseUrl?: _JSONValue;
- headers?: _JSONValue;
+ body?: _JSONValue;
query?: _JSONValue;
tags?: _JSONValue;
}
@@ -21,19 +22,22 @@ const createQueryKey = (id: string, options?: TOptions
] => {
const params: QueryKey[0] = { _id: id, baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl } as QueryKey[0];
if (tags) {
- params.tags = tags as unknown as undefined;
+ params.tags = tags as unknown as _JSONValue;
}
- if (options?.body) {
- params.body = options.body;
- }
- if (options?.headers) {
- params.headers = options.headers as unknown as undefined;
+ if (options?.body !== undefined) {
+ const normalizedBody = serializeQueryKeyValue(options.body);
+ if (normalizedBody !== undefined) {
+ params.body = normalizedBody;
+ }
}
if (options?.path) {
params.path = options.path;
}
- if (options?.query) {
- params.query = options.query as unknown as undefined;
+ if (options?.query !== undefined) {
+ const normalizedQuery = serializeQueryKeyValue(options.query);
+ if (normalizedQuery !== undefined) {
+ params.query = normalizedQuery;
+ }
}
return [
params
@@ -42,19 +46,17 @@ const createQueryKey = (id: string, options?: TOptions
export const serviceWithEmptyTagQueryKey = (options?: Options) => createQueryKey('serviceWithEmptyTag', options);
-export const serviceWithEmptyTagQuery = (options?: Options): UseQueryOptions => {
- return {
- key: serviceWithEmptyTagQueryKey(options),
- query: async (context) => {
- const { data } = await serviceWithEmptyTag({
- ...options,
- ...context,
- throwOnError: true
- });
- return data;
- }
- };
-};
+export const serviceWithEmptyTagQuery = defineQueryOptions((options?: Options) => ({
+ key: serviceWithEmptyTagQueryKey(options),
+ query: async (context) => {
+ const { data } = await serviceWithEmptyTag({
+ ...options,
+ ...context,
+ throwOnError: true
+ });
+ return data;
+ }
+}));
export const patchApiVbyApiVersionNoTagMutation = (options?: Partial>): UseMutationOptions, Error> => {
return {
@@ -97,19 +99,17 @@ export const deleteCallWithoutParametersAndResponseMutation = (options?: Partial
export const getCallWithoutParametersAndResponseQueryKey = (options?: Options) => createQueryKey('getCallWithoutParametersAndResponse', options);
-export const getCallWithoutParametersAndResponseQuery = (options?: Options): UseQueryOptions => {
- return {
- key: getCallWithoutParametersAndResponseQueryKey(options),
- query: async (context) => {
- const { data } = await getCallWithoutParametersAndResponse({
- ...options,
- ...context,
- throwOnError: true
- });
- return data;
- }
- };
-};
+export const getCallWithoutParametersAndResponseQuery = defineQueryOptions((options?: Options) => ({
+ key: getCallWithoutParametersAndResponseQueryKey(options),
+ query: async (context) => {
+ const { data } = await getCallWithoutParametersAndResponse({
+ ...options,
+ ...context,
+ throwOnError: true
+ });
+ return data;
+ }
+}));
export const patchCallWithoutParametersAndResponseMutation = (options?: Partial>): UseMutationOptions, Error> => {
return {
@@ -191,19 +191,17 @@ export const callWithWeirdParameterNamesMutation = (options?: Partial) => createQueryKey('callWithDefaultParameters', options);
-export const callWithDefaultParametersQuery = (options: Options): UseQueryOptions