ParsersCommunity
Zod codecs
Using Zod codecs for (de)serialisation in custom nuqs parser
Since zod@^4.1
, you can use codecs
to add bidirectional serialisation / deserialisation to your validation schemas:
import { z } from 'zod'
// Similar to parseAsTimestamp in nuqs:
const dateTimestampCodec = z.codec(z.string().regex(/^\d+$/), z.date(), {
decode: (query) => new Date(parseInt(query)),
encode: (date) => date.valueOf().toFixed()
})
Demo
Zod Codecs Demo
This demo shows how Zod codecs can transform complex data structures into URL-safe strings using base64url encoding and JSON serialization.
Loading demo…
Source code:
import { createParser } from 'nuqs/server' import { z } from 'zod' function createZodCodecParser< Input extends z.ZodCoercedString<string> | z.ZodPipe<any, any>, Output extends z.ZodType >( codec: z.ZodCodec<Input, Output> | z.ZodPipe<Input, Output>, eq: (a: z.output<Output>, b: z.output<Output>) => boolean = (a, b) => a === b ) { return createParser<z.output<Output>>({ parse(query) { return codec.parse(query) }, serialize(value) { return codec.encode(value) }, eq }) } // -- // All parsers from the Zod docs: const jsonCodec = <T extends z.core.$ZodType>(schema: T) => z.codec(z.string(), schema, { decode: (jsonString, ctx) => { try { return JSON.parse(jsonString) } catch (err: any) { ctx.issues.push({ code: 'invalid_format', format: 'json', input: jsonString, message: err.message }) return z.NEVER } }, encode: value => JSON.stringify(value) }) const base64urlToBytes = z.codec(z.base64url(), z.instanceof(Uint8Array), { decode: base64urlString => z.util.base64urlToUint8Array(base64urlString), encode: bytes => z.util.uint8ArrayToBase64url(bytes) }) const utf8ToBytes = z.codec(z.string(), z.instanceof(Uint8Array), { decode: str => new TextEncoder().encode(str), encode: bytes => new TextDecoder().decode(bytes) }) const bytesToUtf8 = invertCodec(utf8ToBytes) // -- function invertCodec<A extends z.ZodType, B extends z.ZodType>( codec: z.ZodCodec<A, B> ): z.ZodCodec<B, A> { return z.codec<B, A>(codec.out, codec.in, { decode(value, ctx) { try { return codec.encode(value) } catch (err) { ctx.issues.push({ code: 'invalid_format', format: 'invert.decode', input: String(value), message: err instanceof z.ZodError ? err.message : String(err) }) return z.NEVER } }, encode(value, ctx) { try { return codec.decode(value) } catch (err) { ctx.issues.push({ code: 'invalid_format', format: 'invert.encode', input: String(value), message: err instanceof z.ZodError ? err.message : String(err) }) return z.NEVER } } }) } // -- const userSchema = z.object({ name: z.string(), age: z.number() }) // Composition always wins. const codec = base64urlToBytes.pipe(bytesToUtf8).pipe(jsonCodec(userSchema)) export const userJsonBase64Parser = createZodCodecParser( codec, (a, b) => a === b || (a.name === b.name && a.age === b.age) )
Refinements
The cool part is being able to add string constraints to the first type in a codec. It has to be rooted as a string data type (because that’s what the URL will give us), but you can add refinements:
z.codec(z.uuid(), ...)
z.codec(z.email(), ...)
z.codec(z.base64url(), ...)
See the complete list of string-based refinements you can use.
Caveats
As stated in the Zod docs, you cannot use transforms in codecs.