valibot-search-params
v0.8.0
Published
`valibot-search-params` is a lightweight, type-safe bidirectional serialization and validation utility that maps plain JavaScript objects to `URLSearchParams` and parses `URLSearchParams` back into structured objects validated against **Valibot** schemas.
Readme
valibot-search-params
valibot-search-params is a lightweight, type-safe bidirectional serialization and validation utility that maps plain JavaScript objects to URLSearchParams and parses URLSearchParams back into structured objects validated against Valibot schemas.
Requirements and Guardrails
- Peer Dependency: Requires
valibotversion1.0.0or greater. - Runtime Compatibility: Fully compatible with modern browser environments, server-side runtimes (Node.js, Deno, Bun), and Edge workers.
Installation
Install the package via npm:
npm install valibot-search-paramsQuick Start
1. Serialize an Object to URLSearchParams
Convert a flat or nested JavaScript object into an instance of URLSearchParams using objectToSearchParams:
import { objectToSearchParams } from "valibot-search-params";
const filters = {
search: "typescript",
page: 2,
tags: ["oss", "web"],
meta: { active: true }
};
const searchParams = objectToSearchParams(filters);
console.log(searchParams.toString());
// Output: "search=typescript&page=2&tags=oss&tags=web&meta=%7B%22active%22%3Atrue%7D"2. Parse URLSearchParams to a Validated Object
Parse a URLSearchParams instance back into a validated JavaScript object using searchParamsToObject guided by a Valibot schema:
import * as v from "valibot";
import { searchParamsToObject } from "valibot-search-params";
const Schema = v.object({
search: v.string(),
page: v.number(),
tags: v.array(v.string()),
meta: v.object({ active: v.boolean() })
});
const params = new URLSearchParams("search=typescript&page=2&tags=oss&tags=web&meta=%7B%22active%22%3Atrue%7D");
const parsed = searchParamsToObject(Schema, params);
const validation = v.safeParse(Schema, parsed);
if (validation.success) {
console.log(validation.output);
// Output: { search: 'typescript', page: 2, tags: ['oss', 'web'], meta: { active: true } }
}Key Features
- Type Coercion: Automatically casts raw query parameter strings into correct runtime types (e.g. converting "123" to
123for anumberschema, or "true" totruefor abooleanschema). - Nested JSON Serialization: Serializes nested objects and arrays of complex structures into clean, URI-safe JSON strings rather than using verbose dot-notation or brackets.
- Variant Schema Resolution: Safely resolves discriminated unions (Valibot
variantschemas) based on an explicit discriminator query field. - BigInt Support: Robust, out-of-the-box BigInt serialization and parsing, converting
bigintvalues into clean numeric strings for URL parameters and handling raw numeric representations inside JSON payloads. - Smart Merging: Securely patches URL parameters while preserving unrelated tracking state and strictly enforcing schema shapes.
API Reference
objectToSearchParams
Converts a plain JavaScript object into a URLSearchParams instance.
Signature
export function objectToSearchParams(
obj: Record<string, unknown>,
prefix?: string
): URLSearchParams;Parameters
obj: The plain object containing properties to serialize.prefix: An optional namespace prefix. If provided, prepends${prefix}.to all serialized parameter keys.
Returns
Returns a URLSearchParams instance populated with the serialized object properties.
searchParamsToObject
Parses values from a URLSearchParams instance into a plain object matching the structure of a given Valibot schema.
Signature
export function searchParamsToObject(
schema: ObjectSchema<any, any> | VariantSchema<any, VariantOptions<any>, any>,
searchParams: URLSearchParams,
prefix?: string
): Record<string, unknown>;Parameters
schema: The Valibotobjectorvariant(discriminated union) schema to use as a blueprint.searchParams: TheURLSearchParamsinstance containing the query parameter strings.prefix: An optional namespace prefix. If provided, retrieves values only from parameter keys prefixed with${prefix}..
Returns
Returns a plain Record<string, unknown> object containing the parsed and type-casted properties ready for validation.
Merging Search Params
applySearchParams securely patches existing URL parameters with new values while enforcing a strict schema structure.
[!IMPORTANT] Schema Enforcement Only: This utility treats the schema as the absolute source of truth. It will actively discard any parameter from the new state that does not explicitly exist in the schema or the currently active variant.
When building web applications, you often need to update the URL with new filter or state values without destroying unrelated parameters (like utm_source tracking tags or other app state). Concurrently, when switching between complex UI states (like changing a discriminated union variant), you must ensure old, irrelevant parameters are cleaned up.
applySearchParams solves both problems simultaneously:
- Preserves Unknown State: It securely copies non-schema parameters from your current URL into the new URL.
- Enforces Variant Cleanup: It strictly cleans up obsolete keys when switching variants by only accepting new parameters that are valid for the currently active variant option.
Signature
export function applySearchParams(
schema: ObjectSchema<any, any> | VariantSchema<any, VariantOptions<any>, any>,
currentSearchParams: URLSearchParams,
nextSearchParams: URLSearchParams,
prefix?: string
): URLSearchParams;Parameters
schema: The Valibotobjectorvariantschema defining the valid parameter structure.currentSearchParams: The existingURLSearchParamsrepresenting the current URL state.nextSearchParams: The newURLSearchParamsrepresenting the desired schema state.prefix: An optional namespace prefix. If provided, applies merging logic only to parameter keys prefixed with${prefix}..
Returns
Returns a new URLSearchParams instance containing the merged and sanitized parameters.
Example: Variant Switching and Preservation
This example demonstrates how applySearchParams preserves unrelated tracking tags while perfectly cleaning up obsolete variant keys when switching from a user state to a bot state.
import * as v from "valibot";
import { applySearchParams } from "valibot-search-params";
// 1. Define a variant schema
const EntitySchema = v.variant("type", [
v.object({
type: v.literal("user"),
name: v.string(),
roles: v.array(v.string())
}),
v.object({
type: v.literal("bot"),
model: v.string()
})
]);
// 2. Define the current URL state (contains 'user' variant data + a tracking tag)
const currentParams = new URLSearchParams(
"type=user&name=Alice&roles=admin&utm_source=twitter"
);
// 3. Define the next URL state (switching to 'bot' variant)
// Notice how we don't need to manually delete 'name' or 'roles'
const nextParams = new URLSearchParams(
"type=bot&model=gemini"
);
// 4. Apply the changes
const mergedParams = applySearchParams(EntitySchema, currentParams, nextParams);
console.log(mergedParams.toString());
// Output: "utm_source=twitter&type=bot&model=gemini"
// - 'utm_source' was preserved because it is not in the schema.
// - 'name' and 'roles' were automatically discarded because they belong to the inactive 'user' variant.Advanced Examples
Handling Arrays
Simple primitive arrays are serialized as multiple identical key entries in URLSearchParams. Complex structures (like arrays of objects) are JSON-stringified.
import * as v from "valibot";
import { objectToSearchParams, searchParamsToObject } from "valibot-search-params";
const Schema = v.object({
tags: v.array(v.string()), // Primitive array
items: v.array(v.object({ id: v.number() })) // Array of objects
});
const data = {
tags: ["oss", "web"],
items: [{ id: 1 }, { id: 2 }]
};
const params = objectToSearchParams(data);
console.log(params.toString());
// Output: "tags=oss&tags=web&items=%7B%22id%22%3A1%7D&items=%7B%22id%22%3A2%7D"
const parsed = searchParamsToObject(Schema, params);
console.log(parsed);
// Output: { tags: ['oss', 'web'], items: [ { id: 1 }, { id: 2 } ] }Handling Nested Objects
Nested objects are automatically serialized as JSON strings in the query parameters and parsed back safely during extraction.
import * as v from "valibot";
import { objectToSearchParams, searchParamsToObject } from "valibot-search-params";
const Schema = v.object({
meta: v.object({
active: v.boolean(),
config: v.object({
theme: v.string()
})
})
});
const data = {
meta: {
active: true,
config: { theme: "dark" }
}
};
const params = objectToSearchParams(data);
console.log(params.toString());
// Output: "meta=%7B%22active%22%3Atrue%2C%22config%22%3A%7B%22theme%22%3A%22dark%22%7D%7D"
const parsed = searchParamsToObject(Schema, params);
console.log(parsed);
// Output: { meta: { active: true, config: { theme: 'dark' } } }Handling Variants (Discriminated Unions)
searchParamsToObject evaluates the discriminator field from the query string to select and parse the matching sub-schema.
import * as v from "valibot";
import { objectToSearchParams, searchParamsToObject } from "valibot-search-params";
const ShapeSchema = v.variant("type", [
v.object({
type: v.literal("circle"),
radius: v.number()
}),
v.object({
type: v.literal("rect"),
width: v.number(),
height: v.number()
})
]);
const rectData = {
type: "rect",
width: 10,
height: 20
};
const params = objectToSearchParams(rectData);
// Output: "type=rect&width=10&height=20"
const parsed = searchParamsToObject(ShapeSchema, params);
console.log(parsed);
// Output: { type: 'rect', width: 10, height: 20 }Behavioral Guardrails & Edge Cases
1. JSON Serialization of Nested Structures
Instead of generating verbose bracketed dot-notation (e.g. meta[config][theme]=dark), objectToSearchParams serializes all sub-objects and arrays of objects using standard JSON stringification. During parsing, searchParamsToObject utilizes schema metadata to selectively identify these complex fields and deserialize them before passing the structure to Valibot.
2. Empty String Literals
[!NOTE]
searchParamsToObjectskips literal keys that are empty strings. If your schema defines a literal parameter as an empty string (e.g.v.literal("")),searchParamsToObjecttreats the query key as missing (undefined), resulting in a Valibot validation failure. Make literal keys non-empty or optional if they may be omitted.
3. BigInt Serialization Format
When converting raw objects to query parameters, bigint values are formatted as standard numeric strings (e.g. "42"). These numeric strings are then reconstructed back into JavaScript BigInt instances during parsing, guided by the bigint schema entries (both for top-level query fields and nested JSON structures).
