Custom Search Param Serialization

Purpose and Scope

TanStack Router treats search params as application state, not just a flat string map attached to the browser URL. The default behavior is intentionally more capable than plain platform parsing: Router parses and serializes structured values with JSON, then applies the usual URL escaping and unescaping needed for a safe query string. This page explains when to keep that default, when to replace it, and how to wire a custom serializer into router creation without losing the typed search-param workflow used by links, loaders, validation, and navigation.

Sources: docs/router/guide/custom-search-param-serialization.md, docs/router/guide/search-params.md

Customization matters when your application needs a different representation than the JSON-first default. Some teams want shorter URLs, compatibility with unfurlers or legacy consumers, encoded payloads that survive aggressive URL rewriting, or a library format shared with another system. Router supports that choice through two router-level options: one function for parsing the current location search string into an object, and one function for stringifying an object when links and navigations are generated. The important contract is not the specific algorithm, but preserving state accurately across a serialize and deserialize round trip.

Sources: docs/router/guide/custom-search-param-serialization.md, docs/router/api/router/RouterOptionsType.md

Relevant Source Files

  • docs/router/guide/custom-search-param-serialization.md — Primary guide for replacing the default JSON search serialization with custom functions, including default behavior, helper usage, base64 encoding, and library-oriented examples.
  • docs/router/guide/search-params.md — Conceptual guide explaining why Router models search params as structured URL state rather than relying only on URLSearchParams.
  • docs/router/api/router/RouterOptionsType.md — API reference for the router configuration fields that accept search parsing and stringifying functions, including defaults and signatures.

Core Primitives

The core primitives are the search object, the parser, the stringifier, and route-level validation. A search object is the structured value your app wants to preserve in the URL, such as a page number, a sort direction, or nested filter state. The parser converts the raw search string from the location into that object. The stringifier performs the inverse operation when Router builds hrefs or performs navigation. Validation remains a separate concern: custom serialization changes the transport format, while validation decides which parsed values are safe and meaningful for a route.

Sources: docs/router/guide/search-params.md, docs/router/api/router/RouterOptionsType.md

Router’s guide frames search params as the original global state manager because they survive sharing, bookmarking, refreshing, and browser back or forward navigation. That is why search params often need the same developer experience as other state systems: typed reads, intentional writes, validation, and support for nested arrays or objects. A custom serializer should preserve that user-facing behavior. If a copied URL cannot reconstruct the same route state, the serializer is not fulfilling the role Router expects search params to play in the application.

Sources: docs/router/guide/search-params.md, docs/router/guide/custom-search-param-serialization.md

Default Behavior

By default, TanStack Router serializes search params with JSON and parses them with JSON, while also escaping and unescaping the search string for URL compatibility. The guide demonstrates a search object with primitive fields and a nested filters object, then shows that the nested object becomes an escaped JSON value inside the query string. This default is a strong baseline for application state because it supports JSON-serializable structures rather than forcing everything into strings. It is also the behavior you can recreate explicitly when you want to make the configuration visible.

Sources: docs/router/guide/custom-search-param-serialization.md

import {
  createRouter,
  parseSearchWith,
  stringifySearchWith,
} from '@tanstack/react-router'
 
const router = createRouter({
  // ...
  parseSearch: parseSearchWith(JSON.parse),
  stringifySearch: stringifySearchWith(JSON.stringify),
})

The Solid version follows the same shape, using the Solid package imports instead of the React package imports. That symmetry is useful when documenting shared application conventions across framework packages: the router option names and helper names stay the same, while the package name changes. The point of writing the default explicitly is not to improve on the built-in behavior, but to make the extension seam obvious. Once that seam is clear, you can replace JSON functions with encoding functions or purpose-built library calls while leaving route code unchanged.

Sources: docs/router/guide/custom-search-param-serialization.md

RouterOptions Contract

At the API level, the customization surface is small and precise. The router accepts a stringify function with the shape of a search-record-to-string conversion, and a parse function with the shape of a string-to-search-record conversion. Both are optional. If omitted, Router uses its default stringify and parse implementations. Because these options live on router creation, they apply consistently to generated links, navigation, and current-location parsing. That centralization prevents individual components from inventing incompatible query formats across the same application.

Sources: docs/router/api/router/RouterOptionsType.md

Router optionDocumented typeDefaultRole
stringifySearch(search: Record<string, any>) => stringdefaultStringifySearchConverts structured search state into a URL search representation when links are generated.
parseSearch(search: string) => Record<string, any>defaultParseSearchConverts the current location search string into structured state for Router to consume.
search.strictbooleanfalseControls whether unknown search params that are not returned by route validation are kept or removed.

The related strictness option is worth considering alongside custom serialization. It does not define the wire format, but it controls what happens after parsing and validation. With the default relaxed behavior, unknown search params are retained. With strict behavior enabled, params not returned by any route validation are removed. If you are designing a custom format for long-lived links, decide whether unknown keys should survive navigation, because that affects integrations, gradual migrations, and links produced by older application versions.

Sources: docs/router/api/router/RouterOptionsType.md

Implementation Patterns

The most important rule is idempotency: after serializing and deserializing, you should recover the same meaningful object. The custom serialization guide calls this out directly because some libraries and formats do not preserve nested objects, dates, arrays, or other values in the way an application expects. A compact URL is not useful if it silently erases part of a filter object or changes a number into an unusable string. Treat the serializer as part of your URL-state contract and test representative application states before shipping it.

Sources: docs/router/guide/custom-search-param-serialization.md

A common pattern is base64 encoding. The guide presents base64 as useful for maximum compatibility across browsers, URL unfurlers, and similar environments. In that pattern, the parse side decodes the binary-safe string and then parses JSON, while the stringify side converts the structured value to JSON and then encodes it. The Router integration still uses the same helper functions, so the custom work is isolated to the two conversion callbacks. Your routes, links, and validation code continue to operate on the structured search object.

Sources: docs/router/guide/custom-search-param-serialization.md

const router = createRouter({
  parseSearch: parseSearchWith((value) => JSON.parse(decodeFromBinary(value))),
  stringifySearch: stringifySearchWith((value) =>
    encodeToBinary(JSON.stringify(value)),
  ),
})

Another pattern is delegating to a library designed for query serialization, such as query-string, JSURL2, or Zipson. The guide names these as examples of alternative formats rather than mandating one. When choosing a library, evaluate the data shapes your routes validate and consume, not only the appearance of the URL. If your search schema includes nested filters, arrays, or values that must remain distinguishable from strings, verify the library’s parse output matches what your route validation expects before replacing the default JSON-first behavior.

Sources: docs/router/guide/custom-search-param-serialization.md, docs/router/guide/search-params.md

Execution Flow

The runtime flow begins when Router reads the current location. The configured parser receives the search portion of the URL and returns a record that Router can validate, expose to hooks, pass into loader dependency logic, and use during matching. Later, when a component renders a link or code performs navigation with a new search object, Router calls the configured stringifier to produce the URL representation. Because both directions are configured together, a single application-level decision governs inbound links and outbound navigation.

Sources: docs/router/api/router/RouterOptionsType.md, docs/router/guide/custom-search-param-serialization.md

This flow also explains a common failure mode. If parsing and stringifying are not inverses, the application may appear correct on the first navigation but drift after refresh, sharing, or back navigation. For example, a library that flattens nested structures could make a filter panel work until the URL is copied and reopened, at which point a nested filter may become unavailable or malformed. The practical test is simple: take real route search objects, stringify them, parse the result, and compare the recovered state that route validation will actually consume.

Sources: docs/router/guide/custom-search-param-serialization.md, docs/router/guide/search-params.md

Practical Guidance and Next Steps

Start with the default JSON behavior unless you have a concrete reason to change it. It already addresses the main limitation of URLSearchParams by supporting structured JSON-serializable state while preserving the router’s typed search-param model. Reach for customization when a deployment environment, interoperability requirement, or URL-size concern makes a different format worthwhile. Then implement both router options together, keep route validation in place, and test copy-paste URLs, refreshes, nested objects, arrays, and unknown parameters under your chosen strictness policy.

Sources: docs/router/guide/custom-search-param-serialization.md, docs/router/api/router/RouterOptionsType.md

For adjacent reading, pair this page with the broader Search Params guide before changing formats, because that guide explains the state-management goals behind Router’s search API. Then review RouterOptions when documenting the final app configuration, and read the typed search-param material for route validation and consumption patterns. If the problem is not the URL format but the shape of arrays, objects, or dates in state, continue to the complex search params page before introducing a custom serializer.

Sources: docs/router/guide/search-params.md, docs/router/api/router/RouterOptionsType.md