REST API compliant search param serializer for TanStack Router
npm install tanstack-router-rest-search-serializer


> Note: This is an unofficial community package, not affiliated with TanStack.
A REST API compliant search param serializer for TanStack Router.
Instead of using JSON.stringify / JSON.parse, this serializer uses the URLSearchParams format that conforms to REST API specifications.
TanStack Router serializes search parameters as JSON by default. While this works well for internal application state, it causes issues when integrating with external systems:
- External system compatibility: URLs like ?userCode=%22123%22 (JSON-encoded string) are difficult for external tools and APIs to generate
- Type mismatches: Backend APIs expect standard query string formats, not JSON-encoded values
- Non-standard format: Deviates from the industry-standard application/x-www-form-urlencoded format
This package provides a REST API compliant serializer that uses the standard URLSearchParams format, making URLs human-readable and compatible with any HTTP client or external system.
For more details, see the blog post (Japanese).
- Why this package?
- Features
- Installation
- Quick Start
- Differences from TanStack Router Default
- Supported Formats
- Custom Serializer
- Feature Options
- JSON Fallback
- Presets
- Zod Helpers
- commaSeparatedArray
- joinCommaArray
- Usage with Route Definition
- Limitations
- TypeScript
- Requirements
- License
- Contributing
- REST API compliant URL format (no JSON encoding)
- Supports comma-separated arrays (?ids=1,2,3)
- Supports Rails-style nested objects (?user[name]=john)
- Supports PHP-style arrays (?ids[]=1&ids[]=2)
- Automatic boolean string conversion ("true" → true)
- Customizable feature options
- TypeScript support
- Zero dependencies (except peer dependency on zod for helpers)
``bash`
npm install tanstack-router-rest-search-serializeror
pnpm add tanstack-router-rest-search-serializeror
yarn add tanstack-router-rest-search-serializer
`tsx
import {
parseSearchParams,
stringifySearchParams,
} from 'tanstack-router-rest-search-serializer'
// Set globally in createRouter
const router = createRouter({
routeTree,
parseSearch: parseSearchParams,
stringifySearch: stringifySearchParams,
})
`
By default, TanStack Router encodes search parameters as JSON.
Parse (URL → Object):
| URL | TanStack Router Default | This Serializer |
| ------------------------------ | ---------------------------- | -------------------------------------------- |
| ?foo=bar | { foo: 'bar' } | { foo: 'bar' } |?count=123
| | { count: 123 } (number) | { count: '123' } (string) |?code=%22123%22
| ("123") | { code: '123' } (3 chars) | { code: '"123"' } (5 chars, with quotes) |?active=true
| | { active: true } (boolean) | { active: true } (boolean) |?ids=%5B%221%22%2C%222%22%5D
| | { ids: ['1', '2'] } | { ids: ['["1"', '"2"]'] } (split by comma) |?ids=1,2
| | { ids: '1,2' } (string) | { ids: ['1', '2'] } |?foo=
| | { foo: '' } (empty string) | { foo: '' } (empty string) |
Stringify (Object → URL):
| Data | TanStack Router Default | This Serializer |
| ---------------------------- | --------------------------------------------------------- | ------------------------------------------ |
| { foo: 'bar' } | ?foo=bar | ?foo=bar |{ count: 123 }
| | ?count=123 | ?count=123 |{ code: '123' }
| | ?code=%22123%22 ("123") | ?code=123 |{ active: true }
| | ?active=true | ?active=true |{ ids: ['1', '2'] }
| | ?ids=%5B%221%22%2C%222%22%5D (["1","2"]) | ?ids=1%2C2 (1,2) |{ user: { name: 'john' } }
| | ?user=%7B%22name%22%3A%22john%22%7D ({"name":"john"}) | ?user%5Bname%5D=john (user[name]=john) |
Key Differences:
- Numbers: TanStack Router preserves type, this serializer converts to string
- Strings: TanStack Router JSON encodes, this serializer keeps as-is
- Arrays: TanStack Router uses JSON format, this serializer uses comma-separated
- Nested Objects: TanStack Router uses JSON format, this serializer uses Rails-style
- Empty Strings: Both preserve empty string '' as-is[]
- Comma-separated format cannot distinguish between empty array and empty string ''?ids=
- Empty values like are parsed as empty string ''
| Format | Query String | Parse Result |
| ---------------------- | ------------------------ | -------------------------------- |
| Standard | ?foo=bar | { foo: 'bar' } |?active=true
| Boolean strings | | { active: true } |?ids=1,2,3
| Comma-separated arrays | | { ids: ['1', '2', '3'] } |?ids[]=1&ids[]=2
| PHP-style arrays | | { ids: ['1', '2'] } |?ids=1&ids=2
| Duplicate key arrays | | { ids: ['1', '2'] } |?user[name]=john
| Nested objects | | { user: { name: 'john' } } |?items[0][name]=apple
| Objects in arrays | | { items: [{ name: 'apple' }] } |?items[0]=a&items[1]=b
| Numeric index arrays | | { items: ['a', 'b'] } |
You can create a custom serializer by selecting specific features.
`tsx
import {
createSerializer,
SIMPLE_FEATURES,
} from 'tanstack-router-rest-search-serializer'
// Simple features only (standard format + boolean conversion)
const simple = createSerializer(SIMPLE_FEATURES)
// Select individual features
const custom = createSerializer({
commaSeparatedArrays: true,
booleanStrings: true,
nestedObjects: false,
phpArrays: false,
duplicateKeyArrays: true,
numericIndexArrays: false,
})
`
| Option | Default | Description |
| ---------------------- | ------- | -------------------------------------------------------------- |
| commaSeparatedArrays | true | Parse comma-separated arrays |booleanStrings
| | true | Convert "true" / "false" to boolean |nestedObjects
| | true | Rails-style nested objects |phpArrays
| | true | PHP-style arrays ids[]=1 |duplicateKeyArrays
| | true | Duplicate key arrays ids=1&ids=2 |numericIndexArrays
| | true | Numeric index arrays items[0]=a |jsonFallback
| | false | Parse JSON-encoded values for backward compatibility |
Enable jsonFallback to parse JSON-encoded values from TanStack Router's default format. This is useful for backward compatibility when migrating from JSON to REST format.
`tsx
const { parseSearchParams, stringifySearchParams } = createSerializer({
jsonFallback: true,
})
// Parses TanStack Router default JSON format:
// ?ids=%5B%221%22%2C%222%22%5D (["1","2"]) → { ids: ['1', '2'] }
// ?code=%22123%22 ("123") → { code: '123' }
// ?user=%7B%22name%22%3A%22john%22%7D ({"name":"john"}) → { user: { name: 'john' } }
`
- FULL_FEATURES: All features enabled except jsonFallbackSIMPLE_FEATURES
- : Standard format + boolean conversion only
When using TanStack Router's validateSearch with Zod, these helpers solve format compatibility issues.
Why are these helpers needed?
The comma-separated format cannot distinguish between an empty array [] and an empty string ''. When parsing ?ids=, this serializer returns '' (empty string), but Zod's z.array() expects an array. The commaSeparatedArray helper handles this conversion.
Handles empty string '' → [] conversion for array fields:
`tsx
import { z } from 'zod'
import { commaSeparatedArray } from 'tanstack-router-rest-search-serializer/zod-helpers'
const searchSchema = z.object({
ids: commaSeparatedArray(z.string()).optional(),
})
// ?ids=1,2,3 → { ids: ['1', '2', '3'] }
// ?ids= → { ids: [] }
`
Joins arrays back into comma-separated strings. Use when you want certain parameters to remain as strings containing commas:
`tsx
import { z } from 'zod'
import { joinCommaArray } from 'tanstack-router-rest-search-serializer/zod-helpers'
const searchSchema = z.object({
// Keep as string containing commas
freeText: joinCommaArray(z.string()).optional(),
})
// ?freeText=a,b,c → { freeText: 'a,b,c' } (string, not array)
`
Example of using with TanStack Router's route definition and Zod validation:
`tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import {
parseSearchParams,
stringifySearchParams,
} from 'tanstack-router-rest-search-serializer'
import { commaSeparatedArray } from 'tanstack-router-rest-search-serializer/zod-helpers'
const searchSchema = z.object({
q: z.string().optional(),
page: z.coerce.number().default(1),
tags: commaSeparatedArray(z.string()).optional(),
active: z.boolean().optional(),
})
export const Route = createFileRoute('/search')({
validateSearch: searchSchema,
})
// URL: /search?q=hello&page=2&tags=react,typescript&active=true
// Result: { q: 'hello', page: 2, tags: ['react', 'typescript'], active: true }
`
- Type coercion: All values are parsed as strings. Use z.coerce.number() for numeric values.[]
- Empty arrays: Cannot distinguish between and ''. Use commaSeparatedArray helper.joinCommaArray` helper if needed.
- Commas in values: Values containing commas will be split into arrays. Use
- Deep nesting: While supported, deeply nested structures may result in long URLs.
This package is written in TypeScript and includes type definitions.
- TanStack Router v1.x
- TypeScript 5.0+ (if using TypeScript)
- TanStack Router - Type-safe routing for React
MIT
Issues and pull requests are welcome on GitHub.