Skip to content
Runtime
Framework

Defaults and absence

A parameter declared with .default(v) treats an absent key and the value v as the same state. That equivalence runs in both directions:

  • Decoding: an absent key produces v.
  • Encoding: a value equal to v produces no key.
const products = defineQueryModel({ page: param.integer({ min: 1 }).default(1) });
const absent = products.decode("");
const explicit = products.decode("?page=1");
if (absent.ok) absent.value.page; // 1
if (explicit.ok) explicit.value.page; // 1
products.encode({ page: 1 }); // []
products.encode({ page: 2 }); // [["page", "2"]]

Without the encoding half, ?page=1 and an empty query would decode identically but serialize differently. That means:

  • two links to the same view are not equal strings, so caches and analytics see two views,
  • a “copy link” button produces noise proportional to the number of parameters,
  • and comparing “did the query change?” requires a semantic comparison instead of a string one.

Omission is not a size optimization. It is what makes the canonical form canonical.

Change the status filter to Active and back to All. The key appears, then disappears — because “All” is the declared default, not because the value was cleared.

Products

6 matching

  • Edge runtime handbookactive$59
  • Node.js request toolkitactive$39
  • Nuxt deployment guidearchived$19
  • history.pushState
  • history.replaceState
  • popstate

The adapter writes through the History API and re-reads on popstate.

Type a query, press Enter.

Typed state

{
  "page": 1,
  "sort": "created_at",
  "status": "all"
}

Canonical URL

Valid
/products

    QueryWeave distinguishes states that a Record<string, string> would flatten together.

    State Query Decoded value Encoded again
    Not set, has a default (absent) the default (absent)
    Not set, optional (absent) undefined (absent)
    Set to nothing, nullable ?q= null ?q=
    Set to nothing, plain ?q= recovered (absent)
    Required and absent (absent) decode fails

    The third row is the one that is easy to miss. If your application needs “the user explicitly cleared this field” to be different from “the user never touched it”, .nullable() is what expresses it:

    const filters = defineQueryModel({
    category: param.text().nullable().optional(),
    });
    const untouched = filters.decode("");
    const cleared = filters.decode("?category=");
    if (untouched.ok) untouched.value.category; // undefined — never set
    if (cleared.ok) cleared.value.category; // null — explicitly cleared

    Because undefined encodes to nothing, assigning it is how you remove a key:

    // current URL: ?search=vue&page=3
    await runtime.transaction((draft) => {
    draft.search = undefined;
    draft.page = 1;
    });
    // written URL: /products

    The runtime also has an explicit operation, which is clearer when you are removing several keys and do not care about their current values:

    // current URL: ?search=vue&page=3
    await runtime.remove(["search", "page"]);
    // written URL: /products
    // from the same initial URL, reset every managed key to its default
    await runtime.reset();
    // written URL: /products

    remove omits the keys from the written output. reset sets them to their defaults, which — for a parameter that has one — produces the same URL. The difference appears for optional parameters: resetting one sets it to undefined, which is also its default.

    A required parameter cannot be defaulted, by construction: .default() changes presence to default. This is why defaults() returns only the parameters that have one, and why its type excludes required keys.

    const model = defineQueryModel({
    id: param.text(),
    page: param.integer().default(1),
    });
    model.defaults(); // { page: 1 } — `id` is not present, in the type or at runtime
    • A default that cannot be encoded to itself breaks omission. If a custom codec encodes 1 as "01", then decoding "01" must produce 1 again, or the comparison in encode will not match. .default() runs that round trip when you declare it and throws when it fails.
    • List defaults are compared by their encoded form, so .default([]) is dropped correctly without any deep-equality helper. An empty list that is not the default is written as one empty value, tags=, which reads back as [].
    • A default is validated against the parameter’s own constraints when you declare it. param.integer({ min: 5 }).default(1) throws, because it would produce a state that decoding could never yield. Refinements are not run on defaults.
    • Defaults are frozen copies. Every decode of an absent key hands out the same frozen array or object, so a transaction that tries to mutate it in place throws instead of changing the next request’s state. Replace nested values; do not mutate them.

    Issues and recovery covers what happens when the input is wrong.