Skip to content

Commit

Permalink
fix: parseAsJson now requires a runtime validation function
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Pass in a validation function like a Zod schema parse function
to validate at runtime and infer the type of the returned data.
  • Loading branch information
franky47 committed Oct 22, 2024
1 parent 97f35fe commit 53a37d4
Show file tree
Hide file tree
Showing 4 changed files with 28 additions and 29 deletions.
2 changes: 2 additions & 0 deletions packages/docs/content/docs/migrations/v2.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,5 @@ The following breaking changes only apply to exported types:
- The `Options{:ts}` type is no longer generic
- The `UseQueryStatesOptions{:ts}` is now a type rather than an interface, and is now
generic over the type of the object you pass to `useQueryStates{:ts}`.
- [`parseAsJson{:ts}`](/docs/parsers/built-in#json) now requires a runtime
validation function to infer the type of the parsed JSON data.
37 changes: 16 additions & 21 deletions packages/docs/content/docs/parsers/built-in.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,23 @@ parseAsArrayOf(parseAsInteger, ';')

If primitive types are not enough, you can encode JSON in the query string.

```ts /parseAsJson()/
Pass it a validation function to validate and infer the type of the parsed data,
like a [Zod](https://zod.dev) schema:

```ts
import { parseAsJson } from 'nuqs'
import { z } from 'zod'

const schema = z.object({
pkg: z.string(),
version: z.number(),
worksWith: z.array(z.string())
})

// This parser is a function, don't forget to call it:
const [json, setJson] = useQueryState('json', parseAsJson())
// This parser is a function, don't forget to call it with the parse function
// as an argument.
// [!code word:parseAsJson()]
const [json, setJson] = useQueryState('json', parseAsJson(schema.parse))

setJson({
pkg: "nuqs",
Expand All @@ -239,25 +251,8 @@ setJson({
<JsonParserDemo/>
</Suspense>

### Validation with Zod

Note that by itself, `parseAsJson` isn't type-safe, and will return `unknown{:ts}`,
as the value could be anything.

You can pass it an optional callback argument to validate the parsed data.
Using a Zod schema looks like this:

```ts
const zodSchema = z.object({
foo: z.string(),
bar: z.number()
})

const [obj, setObj] = useQueryState('zod', parseAsJson(zodSchema.parse))
```

Using other validation libraries is possible, as long as they throw an error
when the data is invalid.
when the data is invalid (eg: Valibot, Yup, etc).

## Using parsers server-side

Expand Down
9 changes: 7 additions & 2 deletions packages/e2e/src/app/app/clearOnDefault/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default function Page() {
}

const defaultJSON = { foo: 'bar' }
const runtimePassthrough = (x: unknown) => x

function Client() {
const [, setA] = useQueryState('a')
Expand All @@ -32,11 +33,15 @@ function Client() {
)
const [, setJsonRef] = useQueryState(
'json-ref',
parseAsJson().withDefault(defaultJSON).withOptions({ clearOnDefault: true })
parseAsJson(runtimePassthrough)
.withDefault(defaultJSON)
.withOptions({ clearOnDefault: true })
)
const [, setJsonNew] = useQueryState(
'json-new',
parseAsJson().withDefault(defaultJSON).withOptions({ clearOnDefault: true })
parseAsJson(runtimePassthrough)
.withDefault(defaultJSON)
.withOptions({ clearOnDefault: true })
)
return (
<>
Expand Down
9 changes: 3 additions & 6 deletions packages/nuqs/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,17 +328,14 @@ export function parseAsNumberLiteral<Literal extends number>(
* Note: you may want to use `useQueryStates` for finer control over
* multiple related query keys.
*
* @param parser optional parser (eg: Zod schema) to validate after JSON.parse
* @param runtimeParser Runtime parser (eg: Zod schema) to validate after JSON.parse
*/
export function parseAsJson<T>(parser?: (value: unknown) => T) {
export function parseAsJson<T>(runtimeParser: (value: unknown) => T) {
return createParser({
parse: query => {
try {
const obj = JSON.parse(query)
if (typeof parser === 'function') {
return parser(obj)
}
return obj as T
return runtimeParser(obj)
} catch {
return null
}
Expand Down

0 comments on commit 53a37d4

Please sign in to comment.