Skip to content

Custom Param Parsers ​

Experimental

This feature is part of the Experimental Router. API and ergonomics may change. Make sure you've set it up first.

Param parsers transform raw URL strings into rich JS values (and back) for path, query, and hash params, with end-to-end TypeScript types.

The problem (current way) ​

In the stable router, params and query come in as string | string[] | null. You either:

  • pin a regex inline: path: '/users/:id(\\d+)'. Still typed as string, no parsing.
  • coerce by hand inside the component: const id = Number(route.params.id).
  • write a beforeEach guard to validate or redirect: cannot let other routes match.

This works but the type system can't help you, every consumer has to know the convention, and query params are even worse.

Setup ​

Enable experimental.paramParsers in the Vue Router Vite plugin. This tells the plugin where to scan for custom parsers and registers them both at runtime and in the generated typed-router.d.ts.

vite.config.ts
ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueRouter from 'vue-router/vite'

export default defineConfig({
  plugins: [
    VueRouter({
      experimental: {
        paramParsers: {
          dir: 'src/params',
        },
      },
    }),
    vue(),
  ],
})

src/params is the default directory when setting paramParsers to true, but you can point dir to any project-relative folder, or to an array of folders.

Built-in parsers ​

NamePathQueryHashType
int✅✅✅number
bool✅✅✅boolean
string✅✅✅string

string is the default param parser and does nothing. It's equivalent to not setting the parser.

vue
<!-- src/pages/users/[id=int].vue -->
<script setup lang="ts">
const route = useRoute('/users/[id=int]')
route.params.id // number
</script>

Defining custom parsers ​

You define param parsers as modules exporting a parser in the configured param parser directory. The file name is the parser name you use in routes. For example, src/params/uuid.ts exports a parser that validates UUIDs and can be used as [id=uuid] in route files.

A parser is just an object with a getter and a setter. Vue Router provides three helpers: defineParamParser(), defineParamParserRaw(), and defineHashParamParser().

Reach for defineParamParser first, it's the most common use case for simple one-to-one transforms. Use defineParamParserRaw when you need to collapse multiple input shapes into one output type or you want to reject nullish or array values outright. Use defineHashParamParser for hash params.

defineParamParser ​

defineParamParser defines a single-value transform. The router wraps it for optional/repeatable usage and handles null/arrays for you.

ts
// src/params/number.ts
import { defineParamParser, miss } from 'vue-router/experimental'

// pass the final type as a generic to enforce the return type of `get`
// and the input type of `set`
export const parser = defineParamParser<number>({
  get: value => {
    const n = Number(value)
    if (Number.isNaN(n)) miss(`"${value}" is not a number`)
    return n
  },
  set: value => String(value),
})

TIP

Write validation logic in get. For a named location, the router runs get after set to normalize params, and a validation error makes resolution fail. When matching a path, a validation error skips that route so another route, such as a catch-all, can match.

This gives us the possibility to transform a param to a number (including floats), while preserving the shape of the original params:

  • /products/[productId=number].vue one single param:
    • /products/42 → route.params.productId: 42
  • /products/[productIds=number]+.vue repeatable parameter:
    • /products/42 → route.params.productIds: [42]
    • /products/42/24 → route.params.productIds: [42, 24]
  • /products/[[productId=number]].vue one single optional param:
    • /products/42 → route.params.productId: 42
    • /products → route.params.productId: null

The logic of the param parser is simple because defineParamParser() handles the underlying transformation between single/array/nullish values. You just define how to get from a single string to your desired type and back.

To remove an optional path param, pass null when navigating.

defineParamParserRaw ​

defineParamParserRaw gives full control over the transformation. You must handle every shape (null, undefined, single, array) yourself, but in exchange you can collapse them all into one output type (e.g. always return a Set<string>, whether the input was missing, a single value, or an array). Below, the result is always a Set<string>, regardless of whether the URL provided nothing, one value, or many.

ts
// src/params/test-set.ts
import { defineParamParserRaw } from 'vue-router/experimental'

// pass the final type as a generic so `route.params.<name>` is typed
export const parser = defineParamParserRaw<Set<string>>({
  get: value => {
    if (value == null) return new Set()
    return new Set(
      Array.isArray(value) ? value.filter(v => v != null) : [value]
    )
  },
  set: value => [...value],
})

TIP

While you can also return null, undefined, or a simple string from set, returning an array is usually the best choice: an empty array [] is treated the same as null (the param is omitted), so a single [...value] covers every case without branching. After navigation, get runs again to validate the value, so any invalid combination still goes through your own check.

ts
export const parser = defineParamParserRaw<Set<string>>({
  get: value => {
    if (value == null) return new Set()
    return new Set(
      Array.isArray(value) ? value.filter(v => v != null) : [value]
    )
  },
  // empty Set → [] → param omitted, single → ['one'], many → ['a', 'b']
  set: value => [...value],
})

Here is a table of the different meaningful combinations of return values from set and how the router treats them for path and query params:

set returnsPath paramQuery param
null / undefinedparam is omittedparam is omitted (undefined) or rendered without a value (null, ?k)
stringsingle segment (/value)single entry (?k=value)
string[]repeatable (/a/b/c)repeated entries (?k=a&k=b)

defineHashParamParser ​

defineHashParamParser defines a parser for the hash. get receives the hash contents without the leading # and set returns them. Unlike defineParamParser, it does not wrap arrays or nullish values.

A route has only one hash param. To read several values from the hash, return an object:

ts
// src/params/section.ts
import { defineHashParamParser, miss } from 'vue-router/experimental'

export const parser = defineHashParamParser<{ heading: string; tab: string }>({
  get: hash => {
    const [heading, tab] = hash.split('/')
    if (!heading || !tab) miss()
    return { heading, tab }
  },
  set: ({ heading, tab }) => `${heading}/${tab}`,
})

Errors ​

Throw any error from get to mark the value as not matching. The router skips the route (treat it like a 404 candidate). miss(reason?) is just sugar for throwing a typed error.

Standard Schema (Zod / Valibot) ​

Any Standard Schema compatible schema can be used directly as a parser:

ts
// src/params/month-zod.ts
import { z } from 'zod'
export const parser = z.coerce.number().int().min(1).max(12)

WARNING

Standard Schema is one-way: it parses input but cannot serialize back. The router stringifies the value with String(value) when navigating, so this only works when String(parsed) === original. See standard-schema#14. For anything more complex, use defineParamParser with an explicit set.

Using parsers in routes ​

Path params ​

You can either rename your file to include =parser within a param segment: [productId] -> [productId=uuid], or you can declare the parser through definePage without renaming the file:

vue
<!-- src/pages/users/[id].vue -->
<script setup lang="ts">
definePage({
  params: {
    path: {
      id: 'number',
    },
  },
})
</script>

Use the object form to also restrict the values the param matches with a custom regexp:

vue
<!-- src/pages/[org].vue -->
<script setup lang="ts">
definePage({
  params: {
    path: {
      // only matches `/@vuejs`, not `/vuejs`
      org: { re: /@\w+/ },
      // or combine it with a parser
      // org: { parser: 'org', re: /@\w+/ },
    },
  },
})
</script>

The regexp is inserted as a group inside the regexp of the whole path, so it must only describe the value of the param:

  • It must not contain capturing groups, use (?:...) instead.
  • It must not match an empty value (e.g. /a*/).
  • Do not use anchors like ^ or $: they apply to the whole path, not to the param value.
  • Its flags are ignored: paths are always matched case-insensitively.

For repeatable params, each value must match the regexp, so the regexp should not match / (e.g. use [^/]+ instead of .+). Set re to null to use the default regexp.

Query params ​

Declared inside definePage():

vue
<script setup lang="ts">
definePage({
  params: {
    query: {
      // single value (last one wins if multiple are provided)
      page: { parser: 'int', format: 'value', default: 1 },
      // array form: ?tag=a&tag=b → ['a','b']
      tag: { parser: 'string', format: 'array' },
    },
  },
})
</script>

Options per query field:

  • parser: parser name (from src/params/*). Omit for raw string.
  • format: 'value' (single, takes the last value if the URL has several) or 'array'.
  • default: value or () => value used when the param is missing or parsing fails and it's not required.
  • required: rejects a match if the param is absent (instead of using default).

Hash params ​

Declare one named hash param in definePage(). Without a parser, the param is the hash contents without #:

vue
<script setup lang="ts">
definePage({
  params: {
    hash: { heading: {} },
  },
})
// no hash → undefined, `#` → '', `#setup` → 'setup'
</script>

Use a native parser like int, or a custom one like the section parser above:

vue
<script setup lang="ts">
definePage({
  params: {
    hash: {
      // `#2` → 2, no hash or `#abc` → 1
      page: { parser: 'int', default: 1 },
    },
  },
})
</script>
ts
// in another page: hash: { section: 'section' }
router.push({
  name: '/guide',
  params: { section: { heading: 'setup', tab: 'vue' } },
})
// → /guide#setup/vue

Options:

  • parser: parser name. Omit it to use string.
  • default: value or () => value used when the hash is absent, the parser throws, or it returns undefined.
  • required: reject the match in these same cases instead. You cannot use it with default.

Hash params are optional by default: their type includes undefined unless you set a default. An absent hash never calls the getter. Other parsed values, such as null, 0, or '', are kept as they are.

Without a parser, the default must be a string or null. For other types, set a parser: { default: 0 } is invalid, { parser: 'int', default: 0 } is valid. If code generation cannot infer the type of a default without a parser, it uses unknown.

When you navigate, you can omit an optional hash param or one with a default. Set it to undefined (or null if its type allows it) to remove the hash. The router adds the # to the result of set, then runs get on it to validate and normalize the value.

Released under the MIT License.