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
2 changes: 2 additions & 0 deletions packages/nuqs/src/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const exports = `
"parseAsStringEnum": "function",
"parseAsStringLiteral": "function",
"parseAsTimestamp": "object",
"parseAsUuid": "function",
"throttle": "function",
"useQueryState": "function",
"useQueryStates": "function",
Expand Down Expand Up @@ -93,6 +94,7 @@ const exports = `
"parseAsStringEnum": "function",
"parseAsStringLiteral": "function",
"parseAsTimestamp": "object",
"parseAsUuid": "function",
"throttle": "function",
},
"./testing": {
Expand Down
34 changes: 33 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 Down Expand Up @@ -135,6 +136,37 @@ describe('parsers', () => {
expect(testSerializeThenParse(parseAsIsoDate, ref)).toBe(true)
expect(isParserBijective(parseAsIsoDate, moment, ref)).toBe(true)
})
it('parseAsUuid', () => {
expect(parseAsUuid().parse('')).toBeNull()
expect(parseAsUuid().parse('foo')).toBeNull()
const uuid = (v = 4) => `01234567-890a-${v}${v}${v}${v}-8bcd-ef0123456789`
expect(parseAsUuid().parse(uuid())).toBe(uuid())
expect(parseAsUuid({ version: 1 }).parse(uuid(1))).toBe(uuid(1))
expect(parseAsUuid({ version: 1 }).parse(uuid(2))).toBe(null)
expect(parseAsUuid({ version: 2 }).parse(uuid(2))).toBe(uuid(2))
expect(parseAsUuid({ version: 2 }).parse(uuid(1))).toBe(null)
expect(parseAsUuid({ version: 3 }).parse(uuid(3))).toBe(uuid(3))
expect(parseAsUuid({ version: 3 }).parse(uuid(1))).toBe(null)
expect(parseAsUuid({ version: 4 }).parse(uuid(4))).toBe(uuid(4))
expect(parseAsUuid({ version: 4 }).parse(uuid(1))).toBe(null)
expect(parseAsUuid({ version: 5 }).parse(uuid(5))).toBe(uuid(5))
expect(parseAsUuid({ version: 5 }).parse(uuid(1))).toBe(null)
expect(parseAsUuid({ version: 6 }).parse(uuid(6))).toBe(uuid(6))
expect(parseAsUuid({ version: 6 }).parse(uuid(1))).toBe(null)
expect(parseAsUuid({ version: 7 }).parse(uuid(7))).toBe(uuid(7))
expect(parseAsUuid({ version: 7 }).parse(uuid(1))).toBe(null)
expect(parseAsUuid({ version: 8 }).parse(uuid(8))).toBe(uuid(8))
expect(parseAsUuid({ version: 8 }).parse(uuid(1))).toBe(null)

expect(parseAsUuid().parse('00000000-0000-0000-0000-000000000000')).toBe(
'00000000-0000-0000-0000-000000000000'
)
expect(parseAsUuid().parse('ffffffff-ffff-ffff-ffff-ffffffffffff')).toBe(
'ffffffff-ffff-ffff-ffff-ffffffffffff'
)

expect(isParserBijective(parseAsUuid(), uuid(), uuid())).toBe(true)
})
it('parseAsStringEnum', () => {
enum Test {
A = 'a',
Expand Down
53 changes: 53 additions & 0 deletions packages/nuqs/src/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,59 @@ 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?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
}): ParserBuilder<string> {
return createParser({
parse(query) {
// Create regex only when needed to reduce bundle size
const v = opts?.version
// Note: using +v to coerce to number (in case of user-controlled inputs,
// to avoid a RegExp DoS attack)
const versionPattern = v ? `[${+v}]` : '[1-8]'
return new RegExp(
`^([0-9a-f]{8}-[0-9a-f]{4}-${versionPattern}[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|0{8}-0{4}-0{4}-0{4}-0{12}|f{8}-f{4}-f{4}-f{4}-f{12})$`,
'i'
).test(query)
? query
: 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
17 changes: 17 additions & 0 deletions packages/nuqs/tests/parsers.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
parseAsStringEnum,
parseAsStringLiteral,
parseAsTimestamp,
parseAsUuid,
type inferParserType
} from '../dist'

Expand Down Expand Up @@ -65,6 +66,22 @@ describe('types/parsers', () => {
assertType<string>(p.serialize(new Date()))
assertType<Date | null>(p.parseServerSide(undefined))
})
test('parseAsUuid (no version specified)', () => {
const p = parseAsUuid()
assertType<string | null>(p.parse('550e8400-e29b-41d4-a716-446655440000'))
assertType<string>(p.serialize('550e8400-e29b-41d4-a716-446655440000'))
assertType<string | null>(p.parseServerSide(undefined))
})
test('parseAsUuid (with version specified)', () => {
const p = parseAsUuid({ version: 4 })
assertType<string | null>(p.parse('550e8400-e29b-41d4-a716-446655440000'))
assertType<string>(p.serialize('550e8400-e29b-41d4-a716-446655440000'))
assertType<string | null>(p.parseServerSide(undefined))
})
test('parseAsUuid (with invalid version specified)', () => {
// @ts-expect-error
parseAsUuid({ version: 123 })
})
test('parseAsStringEnum', () => {
enum Test {
A = 'a',
Expand Down