Skip to content

Instantly share code, notes, and snippets.

@JacobWeisenburger
Last active April 8, 2024 07:07
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save JacobWeisenburger/9256eae415f6b0a04b718d633266a4e0 to your computer and use it in GitHub Desktop.
Save JacobWeisenburger/9256eae415f6b0a04b718d633266a4e0 to your computer and use it in GitHub Desktop.
a way to parse URLSearchParams with Zod
import { z } from 'zod'
function safeParseJSON ( string: string ): any {
try { return JSON.parse( string ) }
catch { return string }
}
function searchParamsToValues ( searchParams: URLSearchParams ): Record<string, any> {
return Array.from( searchParams.keys() ).reduce( ( record, key ) => {
const values = searchParams.getAll( key ).map( safeParseJSON )
return { ...record, [ key ]: values.length > 1 ? values : values[ 0 ] }
}, {} as Record<string, any> )
}
function makeSearchParamsObjSchema<
Schema extends z.ZodObject<z.ZodRawShape>
> ( schema: Schema ) {
return z.instanceof( URLSearchParams )
.transform( searchParamsToValues )
.pipe( schema )
}
function coerceToArray<
Schema extends z.ZodArray<z.ZodTypeAny>
> ( schema: Schema ) {
return z.union( [
z.any().array(),
z.any().transform( x => [ x ] ),
] ).pipe( schema )
}
// `objSchema` isn't coupled to URLSearchParams,
// so it can easily be used elsewhere or it can come from elsewhere
const objSchema = z.object( {
manyStrings: coerceToArray( z.string().array().min( 1 ) ),
manyNumbers: coerceToArray( z.number().array().min( 1 ) ),
oneStringInArray: coerceToArray( z.string().array().min( 1 ).max( 1 ) ),
string: z.string().min( 1 ),
posNumber: z.number().positive(),
range: z.number().min( 0 ).max( 5 ),
boolean: z.boolean(),
true: z.literal( true ),
false: z.literal( false ),
null: z.null(),
plainDate: z.string().refine(
value => /\d{4}-\d{2}-\d{2}/.test( value ),
value => ( { message: `Invalid plain date: ${ value }` } ),
),
tuple: z.tuple( [ z.string(), z.number() ] ),
object: z.object( {
foo: z.string(),
bar: z.number(),
} ),
} )
const searchParamsObjSchema = makeSearchParamsObjSchema( objSchema )
// happy path
{
const searchParams = new URLSearchParams( {
manyStrings: 'hello',
manyNumbers: '123',
oneStringInArray: 'Leeeeeeeeeroyyyyyyy Jenkiiiiiins!',
boolean: 'true',
true: 'true',
false: 'false',
string: 'foo',
posNumber: '42.42',
} )
searchParams.append( 'manyStrings', 'world' )
searchParams.append( 'manyNumbers', '456' )
searchParams.append( 'range', '4' )
searchParams.append( 'null', 'null' )
searchParams.append( 'plainDate', '2021-01-01' )
searchParams.append( 'tuple', '["foo",42]' )
searchParams.append( 'object', '{"foo":"foo","bar":42}' )
const result = searchParamsObjSchema.safeParse( searchParams )
console.log(
result.success
? result.data
: result.error.format()
)
// {
// manyStrings: [ "hello", "world" ],
// manyNumbers: [ 123, 456 ],
// oneStringInArray: [ "Leeeeeeeeeroyyyyyyy Jenkiiiiiins!" ],
// string: "foo",
// posNumber: 42.42,
// range: 4,
// boolean: true,
// true: true,
// false: false,
// null: null,
// plainDate: "2021-01-01",
// tuple: [ "foo", 42 ],
// object: { foo: "foo", bar: 42 }
// }
}
// sad path
{
const searchParams = new URLSearchParams( {
manyStrings: 'hello',
manyNumbers: '123',
oneStringInArray: 'Leeeeeeeeeroyyyyyyy',
string: '',
boolean: 'foo',
true: 'foo',
false: 'foo',
posNumber: '-42',
} )
searchParams.append( 'oneStringInArray', 'Jenkiiiiiins!' )
searchParams.append( 'range', '6' )
searchParams.append( 'null', 'undefined' )
searchParams.append( 'plainDate', '2021-0-01' )
searchParams.append( 'tuple', '[42,"foo"]' )
searchParams.append( 'object', '{"foo":42,"bar":"foo"}' )
const result = searchParamsObjSchema.safeParse( searchParams )
console.log(
result.success
? result.data
: result.error.format()
)
// {
// _errors: [],
// oneStringInArray: { _errors: [ "Array must contain at most 1 element(s)" ] },
// string: { _errors: [ "String must contain at least 1 character(s)" ] },
// posNumber: { _errors: [ "Number must be greater than 0" ] },
// range: { _errors: [ "Number must be less than or equal to 5" ] },
// boolean: { _errors: [ "Expected boolean, received string" ] },
// true: { _errors: [ "Invalid literal value, expected true" ] },
// false: { _errors: [ "Invalid literal value, expected false" ] },
// null: { _errors: [ "Expected null, received string" ] },
// plainDate: { _errors: [ "Invalid plain date: 2021-0-01" ] },
// tuple: {
// "0": { _errors: [ "Expected string, received number" ] },
// "1": { _errors: [ "Expected number, received string" ] },
// _errors: []
// },
// object: {
// _errors: [],
// foo: { _errors: [ "Expected string, received number" ] },
// bar: { _errors: [ "Expected number, received string" ] }
// }
// }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment