Skip to content
30 changes: 29 additions & 1 deletion packages/docs/content/docs/parsers/built-in.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
DateISOParserDemo,
DatetimeISOParserDemo,
DateTimestampParserDemo,
JsonParserDemo
JsonParserDemo,
UuidParserDemo
} from '@/content/docs/parsers/demos'

Search params are strings by default, but chances are your state is more complex than that.
Expand Down Expand Up @@ -58,6 +59,33 @@ export const searchParamsParsers = {
}
```

## UUID

Validates and parses UUID strings. Accepts all valid UUID formats by default, or you can specify a particular version.

```ts
import { parseAsUuid } from 'nuqs'

// Accept any valid UUID
const [id, setId] = useQueryState('id', parseAsUuid())

// Only accept UUID v4
const [sessionId, setSessionId] = useQueryState(
'sessionId',
parseAsUuid({ version: 4 })
)
```

<Suspense fallback={<DemoFallback />}>
<UuidParserDemo />
</Suspense>

<Callout title="UUID Validation">
When no version is specified, the parser accepts any valid UUID format (versions 1-8).
When a specific version is provided, only UUIDs of that version will be accepted.
Invalid UUIDs will result in a `null` value.
</Callout>

## Numbers

### Integers
Expand Down
23 changes: 22 additions & 1 deletion packages/docs/content/docs/parsers/demos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ import {
parseAsIsoDate,
parseAsIsoDateTime,
parseAsJson,
parseAsUuid,
parseAsStringLiteral,
parseAsTimestamp,
useQueryState
} from 'nuqs'
import React from 'react'
import React, { useState } from 'react'
import { z } from 'zod'

export function DemoFallback() {
Expand Down Expand Up @@ -109,6 +110,26 @@ export function StringParserDemo() {
)
}

export function UuidParserDemo() {
const [value, setValue] = useQueryState('uuid', parseAsUuid())

return (
<DemoContainer demoKey="uuid" className="items-start">
<pre className="bg-background flex-1 rounded-md border p-2 text-sm text-zinc-500">
{value || 'null'}
</pre>
<Button onClick={() => setValue(crypto.randomUUID())}>Try it</Button>
<Button
variant="secondary"
className="ml-auto"
onClick={() => setValue(null)}
>
Clear
</Button>
</DemoContainer>
)
}

export function IntegerParserDemo() {
const [value, setValue] = useQueryState('int', parseAsInteger)
return (
Expand Down
29 changes: 28 additions & 1 deletion packages/nuqs/src/parsers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
parseAsString,
parseAsStringEnum,
parseAsStringLiteral,
parseAsTimestamp
parseAsTimestamp,
parseAsUuid
} from './parsers'
import {
isParserBijective,
Expand All @@ -30,6 +31,32 @@ describe('parsers', () => {
expect(parseAsString.parse('foo')).toBe('foo')
expect(isParserBijective(parseAsString, 'foo', 'foo')).toBe(true)
})
it('parseAsUuid', () => {
expect(parseAsUuid().parse('')).toBeNull()
expect(parseAsUuid().parse('foo')).toBeNull()

expect(parseAsUuid().parse('3c1b65c0-84de-11f0-a3d8-b511344ab1d8')).toBe(
'3c1b65c0-84de-11f0-a3d8-b511344ab1d8'
)

expect(
parseAsUuid({ version: 1 }).parse('3c1b65c0-84de-11f0-a3d8-b511344ab1d8')
).toBe('3c1b65c0-84de-11f0-a3d8-b511344ab1d8') // V1
expect(
parseAsUuid({ version: 4 }).parse('067431d0-1d24-438c-a25d-42607d85495d')
).toBe('067431d0-1d24-438c-a25d-42607d85495d') // V4
expect(
parseAsUuid({ version: 7 }).parse('0198f612-4e10-74ad-babb-e9c3c0a46984')
).toBe('0198f612-4e10-74ad-babb-e9c3c0a46984') // V7

expect(
isParserBijective(
parseAsUuid(),
'3c1b65c0-84de-11f0-a3d8-b511344ab1d8',
'3c1b65c0-84de-11f0-a3d8-b511344ab1d8'
)
).toBe(true)
})
it('parseAsInteger', () => {
expect(parseAsInteger.parse('')).toBeNull()
expect(parseAsInteger.parse('1')).toBe(1)
Expand Down
50 changes: 50 additions & 0 deletions packages/nuqs/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,56 @@ export const parseAsIsoDate: ParserBuilder<Date> = createParser({
eq: compareDates
})

/**
* Parse and validate UUID strings from the query string.
*
* By default, accepts any valid UUID format (v1-v8) including the special
* nil UUID (all zeros) and max UUID (all Fs). You can optionally specify
* a specific UUID version to validate against.
*
* @param opts - Optional configuration object
* @param opts.version - Specific UUID version to validate (1-8)
* @returns A parser that validates UUID strings
*
* @example
* ```ts
* // Accept any valid UUID
* const [id, setId] = useQueryState('id', parseAsUuid())
*
* // URL: ?id=550e8400-e29b-41d4-a716-446655440000
* console.log(id) // "550e8400-e29b-41d4-a716-446655440000"
* ```
*
* @example
* ```ts
* // Only accept UUID v4
* const [sessionId, setSessionId] = useQueryState(
* 'sessionId',
* parseAsUuid({ version: 4 }).withDefault('00000000-0000-0000-0000-000000000000')
* )
*
* // URL: ?sessionId=f47ac10b-58cc-4372-a567-0e02b2c3d479
* console.log(sessionId) // "f47ac10b-58cc-4372-a567-0e02b2c3d479"
* ```
*/
export function parseAsUuid(opts?: {
version?: number
}): ParserBuilder<string> {
return createParser({
parse: v => {
let uuidRegex =
/^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/
if (opts?.version) {
uuidRegex = new RegExp(
`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${opts.version}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`
)
}
return uuidRegex.test(v) ? v : null
},
serialize: String
})
}

/**
* String-based enums provide better type-safety for known sets of values.
* You will need to pass the parseAsStringEnum function a list of your enum values
Expand Down
Loading