Skip to content

Commit 9a8d429

Browse files
Add catchSchemaFailure, and docs for RTKQ schema features (#4934)
* extract common props to separate def and add missing phantom property * try docblocks * add no-transpile to codeblocks * add onSchemaFailure and skipSchemaValidation docs * blurb * usage with typescript * queries and mutations pages * infinite queries * promote heading * try exporting type for docs * export other type used in docblocks * catchSchemaFailure idea * add tests for catchSchemaFailure * include bqMeta on error so we can use it * docs * make sure onSchemaFailure still gets called when caught * make sure errors in user provided callbacks are still handled properly * Tweak schema doc phrasing --------- Co-authored-by: Mark Erikson <[email protected]>
1 parent ad4696c commit 9a8d429

14 files changed

+1137
-81
lines changed

Diff for: docs/rtk-query/api/createApi.mdx

+120
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,20 @@ export type QueryDefinition<
221221
updateCachedData, // available for query endpoints only
222222
}: QueryCacheLifecycleApi,
223223
): Promise<void>
224+
225+
argSchema?: StandardSchemaV1<QueryArg>
226+
227+
/* only available with `query`, not `queryFn` */
228+
rawResponseSchema?: StandardSchemaV1<BaseQueryResult<BaseQuery>>
229+
230+
responseSchema?: StandardSchemaV1<ResultType>
231+
232+
/* only available with `query`, not `queryFn` */
233+
rawErrorResponseSchema?: StandardSchemaV1<BaseQueryError<BaseQuery>>
234+
235+
errorResponseSchema?: StandardSchemaV1<BaseQueryError<BaseQuery>>
236+
237+
metaSchema?: StandardSchemaV1<BaseQueryMeta<BaseQuery>>
224238
}
225239
```
226240
@@ -469,6 +483,36 @@ You can set this globally in `createApi`, but you can also override the default
469483
If you specify `track: false` when manually dispatching queries, RTK Query will not be able to automatically refetch for you.
470484
:::
471485

486+
### `onSchemaFailure`
487+
488+
[summary](docblock://query/createApi.ts?token=CreateApiOptions.onSchemaFailure)
489+
490+
[examples](docblock://query/createApi.ts?token=CreateApiOptions.onSchemaFailure)
491+
492+
:::note
493+
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.
494+
:::
495+
496+
### `catchSchemaFailure`
497+
498+
[summary](docblock://query/createApi.ts?token=CreateApiOptions.catchSchemaFailure)
499+
500+
[examples](docblock://query/createApi.ts?token=CreateApiOptions.catchSchemaFailure)
501+
502+
:::note
503+
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.
504+
:::
505+
506+
### `skipSchemaValidation`
507+
508+
[summary](docblock://query/createApi.ts?token=CreateApiOptions.skipSchemaValidation)
509+
510+
[examples](docblock://query/createApi.ts?token=CreateApiOptions.skipSchemaValidation)
511+
512+
:::note
513+
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.
514+
:::
515+
472516
## Endpoint Definition Parameters
473517

474518
### `query`
@@ -792,6 +836,82 @@ async function onCacheEntryAdded(
792836
): Promise<void>
793837
```
794838

839+
### Schema Validation
840+
841+
Endpoints can have schemas for runtime validation of query args, responses, and errors. Any [Standard Schema](https://standardschema.dev/) compliant library can be used.
842+
843+
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).
844+
845+
:::warning
846+
847+
By default, schema failures are treated as _fatal_, meaning that normal error handling such as tag invalidation will not be executed.
848+
849+
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.
850+
851+
```ts title="catchSchemaFailure example" no-transpile
852+
const api = createApi({
853+
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
854+
catchSchemaFailure: (error, info) => ({
855+
status: 'CUSTOM_ERROR',
856+
error: error.schemaName + ' failed validation',
857+
data: error,
858+
}),
859+
endpoints: (build) => ({
860+
// ...
861+
}),
862+
})
863+
```
864+
865+
:::
866+
867+
#### `argSchema`
868+
869+
_(optional)_
870+
871+
[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.argSchema)
872+
873+
[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.argSchema)
874+
875+
#### `responseSchema`
876+
877+
_(optional)_
878+
879+
[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.responseSchema)
880+
881+
[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.responseSchema)
882+
883+
#### `rawResponseSchema`
884+
885+
_(optional, not applicable with `queryFn`)_
886+
887+
[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawResponseSchema)
888+
889+
[examples](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawResponseSchema)
890+
891+
#### `errorResponseSchema`
892+
893+
_(optional)_
894+
895+
[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.errorResponseSchema)
896+
897+
[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.errorResponseSchema)
898+
899+
#### `rawErrorResponseSchema`
900+
901+
_(optional, not applicable with `queryFn`)_
902+
903+
[summary](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawErrorResponseSchema)
904+
905+
[examples](docblock://query/endpointDefinitions.ts?token=EndpointDefinitionWithQuery.rawErrorResponseSchema)
906+
907+
#### `metaSchema`
908+
909+
_(optional)_
910+
911+
[summary](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.metaSchema)
912+
913+
[examples](docblock://query/endpointDefinitions.ts?token=CommonEndpointDefinition.metaSchema)
914+
795915
## Return value
796916

797917
See [the "created Api" API reference](./created-api/overview)

Diff for: docs/rtk-query/usage-with-typescript.mdx

+132
Original file line numberDiff line numberDiff line change
@@ -703,3 +703,135 @@ function AddPost() {
703703
)
704704
}
705705
```
706+
707+
## Schema Validation
708+
709+
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.
710+
711+
When following the default approach of explicitly specifying type parameters for queries and mutations, the schemas will be required to match the types provided.
712+
713+
```ts title="Explicitly typed endpoint" no-transpile
714+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
715+
import * as v from 'valibot'
716+
717+
const postSchema = v.object({
718+
id: v.number(),
719+
name: v.string(),
720+
})
721+
type Post = v.InferOutput<typeof postSchema>
722+
723+
const api = createApi({
724+
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
725+
endpoints: (build) => ({
726+
getPost: build.query<Post, { id: number }>({
727+
query: ({ id }) => `/post/${id}`,
728+
responseSchema: postSchema, // errors if type mismatch
729+
}),
730+
}),
731+
})
732+
```
733+
734+
Schemas can also be used as a source of inference, meaning that the type parameters can be omitted.
735+
736+
```ts title="Implicitly typed endpoint" no-transpile
737+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
738+
import * as v from 'valibot'
739+
740+
const postSchema = v.object({
741+
id: v.number(),
742+
name: v.string(),
743+
})
744+
type Post = v.InferOutput<typeof postSchema>
745+
746+
const api = createApi({
747+
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
748+
endpoints: (build) => ({
749+
getPost: build.query({
750+
// infer arg from here
751+
query: ({ id }: { id: number }) => `/post/${id}`,
752+
// infer result from here
753+
responseSchema: postSchema,
754+
}),
755+
getTransformedPost: build.query({
756+
// infer arg from here
757+
query: ({ id }: { id: number }) => `/post/${id}`,
758+
// infer untransformed result from here
759+
rawResponseSchema: postSchema,
760+
// infer transformed result from here
761+
transformResponse: (response) => ({
762+
...response,
763+
published_at: new Date(response.published_at),
764+
}),
765+
}),
766+
}),
767+
})
768+
```
769+
770+
:::warning
771+
772+
Schemas should _not_ perform any transformation that would change the type of the value.
773+
774+
```ts title="Incorrect usage" no-transpile
775+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
776+
import * as v from 'valibot'
777+
import { titleCase } from 'lodash'
778+
779+
const postSchema = v.object({
780+
id: v.number(),
781+
name: v.pipe(
782+
v.string(),
783+
v.transform(titleCase), // fine - string -> string
784+
),
785+
published_at: v.pipe(
786+
v.string(),
787+
// highlight-next-line
788+
v.transform((s) => new Date(s)), // not allowed!
789+
v.date(),
790+
),
791+
})
792+
type Post = v.InferOutput<typeof postSchema>
793+
794+
const api = createApi({
795+
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
796+
endpoints: (build) => ({
797+
getPost: build.query<Post, { id: number }>({
798+
query: ({ id }) => `/post/${id}`,
799+
responseSchema: postSchema,
800+
}),
801+
}),
802+
})
803+
```
804+
805+
Instead, transformation should be done with `transformResponse` and `transformErrorResponse` (when using `query`) or inside `queryFn` (when using `queryFn`).
806+
807+
```ts title="Correct usage" no-transpile
808+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
809+
import * as v from 'valibot'
810+
811+
const postSchema = v.object({
812+
id: v.number(),
813+
name: v.string(),
814+
published_at: v.string(),
815+
})
816+
type RawPost = v.InferOutput<typeof postSchema>
817+
type Post = Omit<RawPost, 'published_at'> & { published_at: Date }
818+
819+
const api = createApi({
820+
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
821+
endpoints: (build) => ({
822+
getPost: build.query<Post, { id: number }>({
823+
query: ({ id }) => `/post/${id}`,
824+
// use rawResponseSchema to validate *before* transformation
825+
rawResponseSchema: postSchema,
826+
// highlight-start
827+
transformResponse: (response) => ({
828+
...response,
829+
published_at: new Date(response.published_at),
830+
}),
831+
// highlight-end
832+
}),
833+
}),
834+
})
835+
```
836+
837+
:::

Diff for: docs/rtk-query/usage/infinite-queries.mdx

+58
Original file line numberDiff line numberDiff line change
@@ -531,3 +531,61 @@ const projectsApi = createApi({
531531
}),
532532
})
533533
```
534+
535+
## Runtime Validation using Schemas
536+
537+
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.
538+
539+
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).
540+
541+
Most commonly, you'll want to use `responseSchema` to validate the response from the server (or `rawResponseSchema` when using `transformResponse`).
542+
543+
```ts title="Using responseSchema" no-transpile
544+
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
545+
import * as v from 'valibot'
546+
547+
const pokemonSchema = v.object({
548+
id: v.number(),
549+
name: v.string(),
550+
})
551+
type Pokemon = v.InferOutput<typeof pokemonSchema>
552+
const transformedPokemonSchema = v.object({
553+
...pokemonSchema.entries,
554+
id: v.string(),
555+
})
556+
type TransformedPokemon = v.InferOutput<typeof transformedPokemonSchema>
557+
558+
const api = createApi({
559+
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com/pokemon' }),
560+
endpoints: (build) => ({
561+
getInfinitePokemon: build.infiniteQuery<Pokemon[], string, number>({
562+
query: ({ queryArg, pageParam }) => `type/${queryArg}?page=${pageParam}`,
563+
// argSchema for infinite queries must have both queryArg and pageParam
564+
argSchema: v.object({
565+
queryArg: v.string(),
566+
pageParam: v.number(),
567+
}),
568+
responseSchema: v.array(pokemonSchema),
569+
}),
570+
getTransformedPokemon: build.infiniteQuery<
571+
TransformedPokemon[],
572+
string,
573+
number
574+
>({
575+
query: ({ queryArg, pageParam }) => `type/${queryArg}?page=${pageParam}`,
576+
argSchema: v.object({
577+
queryArg: v.string(),
578+
pageParam: v.number(),
579+
}),
580+
rawResponseSchema: v.array(pokemonSchema),
581+
transformResponse: (response) =>
582+
response.map((pokemon) => ({
583+
...pokemon,
584+
id: String(pokemon.id),
585+
})),
586+
// responseSchema can still be provided, to validate the transformed response
587+
responseSchema: v.array(transformedPokemonSchema),
588+
}),
589+
}),
590+
})
591+
```

0 commit comments

Comments
 (0)