Standard Schema
QueryWeave does not bundle a validation library, and it never will. Validators join through Standard Schema, a specification several libraries implement, so the choice stays yours and stays replaceable.
Where validation fits
Section titled “Where validation fits”Codecs already decide shape. param.integer({ min: 1 }) rejects abc and rejects 0 without any
validator at all. Validation is the layer above that: rules a codec should not know about, such as
“this slug must exist” or “this range must be ordered”.
The path a query takes through the model, stage by stage.
- Raw values — ["2"]
- Codec
- Typed value — 2
- Refinement
- Validated value — 2
Raw values → Codec. Codec → Typed value. Typed value → Refinement. Refinement → Validated value.
The contract
Section titled “The contract”QueryWeave’s own hook is QueryRefinement, which is deliberately tiny:
interface QueryRefinement<TInput, TOutput = TInput> { readonly name?: string | undefined; refine( value: TInput, context: QueryRefineContext, ): QueryRefinementResult<TOutput> | Promise<QueryRefinementResult<TOutput>>;}@queryweave/standard-schema adapts any Standard Schema validator into that shape. It is the only
vendor-facing function in the entire project:
import { fromStandardSchema } from "@queryweave/standard-schema";Install
Section titled “Install”pnpm add @queryweave/core @queryweave/standard-schemaThe package depends on @standard-schema/spec — a types-only package — and on nothing else. No
validator is a dependency of any published QueryWeave package, and a repository check fails the
build if one ever becomes one.
Validating one parameter
Section titled “Validating one parameter”import { defineQueryModel, param } from "@queryweave/core";import { fromStandardSchema } from "@queryweave/standard-schema";import { z } from "zod";
const products = defineQueryModel({ search: param .text() .refine(fromStandardSchema(z.string().min(2))) .optional(),});
const accepted = products.decode("?search=vue");if (accepted.ok) accepted.value.search; // "vue"
const rejected = products.decode("?search=v");rejected.issues[0]?.code; // "validation_failed"rejected.ok && rejected.value.search; // undefined — recovered by presenceThe failure became a QueryWeave issue. Zod’s error type never reaches your code, which is what lets you change validators later without touching anything that reads issues.
Validating the whole model
Section titled “Validating the whole model”Rules that involve more than one key belong to the model:
const priceFilters = defineQueryModel( { minPrice: param.number({ min: 0 }), maxPrice: param.number({ min: 0 }), }, { refine: [fromStandardSchema(orderedPriceRangeSchema)], },);
const result = priceFilters.decode("?minPrice=200&maxPrice=100");result.issues[0]?.key; // "$" — this rule involves both valuesModel refinements run only after every parameter decoded, and their issues are reported under the
key $ (modelIssueKey).
Transforming values
Section titled “Transforming values”A Standard Schema that transforms changes the parameter’s value type, and QueryWeave follows it:
const model = defineQueryModel({ search: param.text().refine(fromStandardSchema(z.string().transform((value) => value.length))),});
// search is now numberAsynchronous schemas
Section titled “Asynchronous schemas”const result = await products.decodeAsync("?search=wireless");
result.ok; // true after the asynchronous schema resolvesresult.issues; // any rejection is normalized to QueryWeave issuesA refinement may return a promise. Synchronous decode cannot wait, so it reports
validation_failed with a message naming decodeAsync and recovers by presence — it does not
block and does not silently pass.
Naming a refinement
Section titled “Naming a refinement”const productSearch = fromStandardSchema(schema, { name: "product-search" });
const products = defineQueryModel({ search: param.text().refine(productSearch).optional(),});The name defaults to the validator’s own vendor string and is used for diagnostics only.
Which validators work
Section titled “Which validators work”Any library implementing Standard Schema v1. The repository verifies three of them against one shared contract: see Zod, Valibot, ArkType.
Edge cases
Section titled “Edge cases”- Refinements are a pipeline. Each receives the previous one’s output, in declaration order.
- A failed refinement recovers by presence, exactly like a codec failure: a default, or
undefined, or a failed result for a required parameter. - Issue paths are preserved. A validator’s path is appended to the parameter’s path, so a nested failure still points at the right place.
- A validator error is never thrown. It becomes an issue.
How it is tested
Section titled “How it is tested”tests/standard-schema/compatibility.test.ts runs one contract across Zod, Valibot, and ArkType,
asserting normalized issues and inferred output types — never a vendor’s own error shape. The
standard-schema consumer fixture repeats it against the packed archive.