Skip to content

Add catchSchemaFailure, and docs for RTKQ schema features #4934

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions docs/rtk-query/api/createApi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,20 @@ export type QueryDefinition<
updateCachedData, // available for query endpoints only
}: QueryCacheLifecycleApi,
): Promise<void>

argSchema?: StandardSchemaV1<QueryArg>

/* only available with `query`, not `queryFn` */
rawResponseSchema?: StandardSchemaV1<BaseQueryResult<BaseQuery>>

responseSchema?: StandardSchemaV1<ResultType>

/* only available with `query`, not `queryFn` */
rawErrorResponseSchema?: StandardSchemaV1<BaseQueryError<BaseQuery>>

errorResponseSchema?: StandardSchemaV1<BaseQueryError<BaseQuery>>

metaSchema?: StandardSchemaV1<BaseQueryMeta<BaseQuery>>
}
```

Expand Down Expand Up @@ -469,6 +483,36 @@ You can set this globally in `createApi`, but you can also override the default
If you specify `track: false` when manually dispatching queries, RTK Query will not be able to automatically refetch for you.
:::

### `onSchemaFailure`

[summary](docblock://query/createApi.ts?token=CreateApiOptions.onSchemaFailure)

[examples](docblock://query/createApi.ts?token=CreateApiOptions.onSchemaFailure)

:::note
You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `onSchemaFailure` to each individual endpoint definition.
:::

### `catchSchemaFailure`

[summary](docblock://query/createApi.ts?token=CreateApiOptions.catchSchemaFailure)

[examples](docblock://query/createApi.ts?token=CreateApiOptions.catchSchemaFailure)

:::note
You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `catchSchemaFailure` to each individual endpoint definition.
:::

### `skipSchemaValidation`

[summary](docblock://query/createApi.ts?token=CreateApiOptions.skipSchemaValidation)

[examples](docblock://query/createApi.ts?token=CreateApiOptions.skipSchemaValidation)

:::note
You can set this globally in `createApi`, but you can also override the default value and have more granular control by passing `skipSchemaValidation` to each individual endpoint definition.
:::

## Endpoint Definition Parameters

### `query`
Expand Down Expand Up @@ -792,6 +836,82 @@ async function onCacheEntryAdded(
): Promise<void>
```

### Schema Validation

Endpoints can have schemas for runtime validation of query args, responses, and errors. Any [Standard Schema](https://standardschema.dev/) compliant library can be used.

When used with TypeScript, schemas can also be used to [infer the type of that value instead of having to declare it](../usage-with-typescript.mdx#schema-validation).

:::warning

By default, schema failures are treated as _fatal_, meaning that normal error handling such as tag invalidation will not be executed.

In order for schema failures to be treated as non-fatal, you must provide a [`catchSchemaFailure`](#catchschemafailure) function, to convert the schema failure into an error shape matching the base query errors.

```ts title="catchSchemaFailure example" no-transpile
const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
catchSchemaFailure: (error, info) => ({
status: 'CUSTOM_ERROR',
error: error.schemaName + ' failed validation',
data: error,
}),
endpoints: (build) => ({
// ...
}),
})
```

:::

#### `argSchema`

_(optional)_

[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.argSchema)

[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.argSchema)

#### `responseSchema`

_(optional)_

[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.responseSchema)

[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.responseSchema)

#### `rawResponseSchema`

_(optional, not applicable with `queryFn`)_

[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawResponseSchema)

[examples](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawResponseSchema)

#### `errorResponseSchema`

_(optional)_

[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.errorResponseSchema)

[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.errorResponseSchema)

#### `rawErrorResponseSchema`

_(optional, not applicable with `queryFn`)_

[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawErrorResponseSchema)

[examples](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawErrorResponseSchema)

#### `metaSchema`

_(optional)_

[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.metaSchema)

[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.metaSchema)

## Return value

See [the "created Api" API reference](./created-api/overview)
132 changes: 132 additions & 0 deletions docs/rtk-query/usage-with-typescript.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -703,3 +703,135 @@ function AddPost() {
)
}
```

## Schema Validation

Endpoints can have schemas for runtime validation of query args, responses, and errors. Any [Standard Schema](https://standardschema.dev/) compliant library can be used. See [API reference](./api/createApi.mdx#schema-validation) for full list of available schemas.

When following the default approach of explicitly specifying type parameters for queries and mutations, the schemas will be required to match the types provided.

```ts title="Explicitly typed endpoint" no-transpile
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import * as v from 'valibot'

const postSchema = v.object({
id: v.number(),
name: v.string(),
})
type Post = v.InferOutput<typeof postSchema>

const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query<Post, { id: number }>({
query: ({ id }) => `/post/${id}`,
responseSchema: postSchema, // errors if type mismatch
}),
}),
})
```

Schemas can also be used as a source of inference, meaning that the type parameters can be omitted.

```ts title="Implicitly typed endpoint" no-transpile
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import * as v from 'valibot'

const postSchema = v.object({
id: v.number(),
name: v.string(),
})
type Post = v.InferOutput<typeof postSchema>

const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query({
// infer arg from here
query: ({ id }: { id: number }) => `/post/${id}`,
// infer result from here
responseSchema: postSchema,
}),
getTransformedPost: build.query({
// infer arg from here
query: ({ id }: { id: number }) => `/post/${id}`,
// infer untransformed result from here
rawResponseSchema: postSchema,
// infer transformed result from here
transformResponse: (response) => ({
...response,
published_at: new Date(response.published_at),
}),
}),
}),
})
```

:::warning

Schemas should _not_ perform any transformation that would change the type of the value.

```ts title="Incorrect usage" no-transpile
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import * as v from 'valibot'
import { titleCase } from 'lodash'

const postSchema = v.object({
id: v.number(),
name: v.pipe(
v.string(),
v.transform(titleCase), // fine - string -> string
),
published_at: v.pipe(
v.string(),
// highlight-next-line
v.transform((s) => new Date(s)), // not allowed!
v.date(),
),
})
type Post = v.InferOutput<typeof postSchema>

const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query<Post, { id: number }>({
query: ({ id }) => `/post/${id}`,
responseSchema: postSchema,
}),
}),
})
```

Instead, transformation should be done with `transformResponse` and `transformErrorResponse` (when using `query`) or inside `queryFn` (when using `queryFn`).

```ts title="Correct usage" no-transpile
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import * as v from 'valibot'

const postSchema = v.object({
id: v.number(),
name: v.string(),
published_at: v.string(),
})
type RawPost = v.InferOutput<typeof postSchema>
type Post = Omit<RawPost, 'published_at'> & { published_at: Date }

const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
getPost: build.query<Post, { id: number }>({
query: ({ id }) => `/post/${id}`,
// use rawResponseSchema to validate *before* transformation
rawResponseSchema: postSchema,
// highlight-start
transformResponse: (response) => ({
...response,
published_at: new Date(response.published_at),
}),
// highlight-end
}),
}),
})
```

:::
58 changes: 58 additions & 0 deletions docs/rtk-query/usage/infinite-queries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,61 @@ const projectsApi = createApi({
}),
})
```

## Runtime Validation using Schemas

Endpoints can use any [Standard Schema](https://standardschema.dev/) compliant library for runtime validation of query args, responses, and errors. See [API reference](../api/createApi.mdx#schema-validation) for full list of available schemas.

When used with TypeScript, schemas can also be used to [infer the type of that value instead of having to declare it](../usage-with-typescript.mdx#schema-validation).

Most commonly, you'll want to use `responseSchema` to validate the response from the server (or `rawResponseSchema` when using `transformResponse`).

```ts title="Using responseSchema" no-transpile
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import * as v from 'valibot'

const pokemonSchema = v.object({
id: v.number(),
name: v.string(),
})
type Pokemon = v.InferOutput<typeof pokemonSchema>
const transformedPokemonSchema = v.object({
...pokemonSchema.entries,
id: v.string(),
})
type TransformedPokemon = v.InferOutput<typeof transformedPokemonSchema>

const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com/pokemon' }),
endpoints: (build) => ({
getInfinitePokemon: build.infiniteQuery<Pokemon[], string, number>({
query: ({ queryArg, pageParam }) => `type/${queryArg}?page=${pageParam}`,
// argSchema for infinite queries must have both queryArg and pageParam
argSchema: v.object({
queryArg: v.string(),
pageParam: v.number(),
}),
responseSchema: v.array(pokemonSchema),
}),
getTransformedPokemon: build.infiniteQuery<
TransformedPokemon[],
string,
number
>({
query: ({ queryArg, pageParam }) => `type/${queryArg}?page=${pageParam}`,
argSchema: v.object({
queryArg: v.string(),
pageParam: v.number(),
}),
rawResponseSchema: v.array(pokemonSchema),
transformResponse: (response) =>
response.map((pokemon) => ({
...pokemon,
id: String(pokemon.id),
})),
// responseSchema can still be provided, to validate the transformed response
responseSchema: v.array(transformedPokemonSchema),
}),
}),
})
```
Loading