ITADN

Proposal: Add a new package `@valibot/web-standards`

#1484Openysknsid25 创建于 2026-05-29
enhancement
Y
ysknsid25commented
# Motivation Valibot states the following about **Browser state** in its official documentation under [Use Cases](https://valibot.dev/guides/use-cases/): > The browser state, which is stored using cookies, search parameters or the local storage, can be accidentally or intentionally manipulated by the user. To ensure the functionality of an application, it can help to validate this data before processing. Valibot can be used for this, which also improves type safety. I completely agree with this point. Valibot's true value lies in parsing and type-checking these String types as strict literals, Branded Types, and Enums. However, in practice, the reality is that **users have to write their own code for retrieval and normalization *before* passing the data to Valibot**. This also means creating a helper every time you create a library, repository, or application. This extra step before using Valibot is left to the user, or to other third-party libraries. The other use cases listed on the Use Cases page each have their own designated users. | Use Case | Coverage | | --- | --- | | Form | Formisch | | Server Requests / Data Migration | Valibot (itself) | | Schema Builder | `@valibot/to-json-schema` | | Config File | `@valibot/config-loader` (I proposed https://github.com/open-circle/valibot/issues/1473) | | **Browser state** | **This place is still empty** | The motivation for proposing this package is to fill in the remaining **Browser state** part. I believe this will differentiate it from other schema libraries. A library officially provided by the provider of a schema library can be expected to inspire confidence and affinity. Furthermore, I can expect a synergy where "people who want to use `Formisch` will also utilize Valibot." I believe there is great value in being able to provide all use cases requiring schema as an open circle with just Valibot. Above all, I am confident that we can make the overall user experience for the use cases officially advocated by Valibot even better. Therefore, I would like to first discuss two points that I have personally experienced toil regarding the usability of browser state while developing, which I hope to address by providing `@valibot/web-standards`. ## 1. Search parameters To retrieve values ​​from `URLSearchParams` and pass them to Valibot, the user would need to write the following preparation code each time: ```ts import * as v from 'valibot'; const Direction = { Left: 'LEFT', Right: 'RIGHT', } as const; const schema = v.object({ q: v.string(), tags: v.array(v.string()), page: v.pipe(v.string(), v.transform(Number), v.number()), direction: v.enum(Direction) }); // Extraction and normalization are the user's responsibility. const params = new URLSearchParams(document.location.search); // If you want an array with keys that have the same name (?tags=a&tags=b), Object.fromEntries is not enough; // You need to retrieve them again using getAll. const obj: Record<string, string | string[]> = {}; for (const key of new Set(params.keys())) { const all = params.getAll(key); obj[key] = all.length > 1 ? all : all[0]; } const result = v.parse(schema, obj); ``` Extracting `?foo=foo&bar=bar` from `document.location`, creating `URLSearchParams`, and rearranging the keys with the same name into an array—this preprocessing is required every time as boilerplate unrelated to the schema. Furthermore, it's not limited to `document.location`; sometimes you receive something created with `new URLSearchParams`, or simply a string like `https://example.com/hoge?foo=foo&bar=bar`. ## 2. Cookie Cookies will take up even more processing power. ```ts import * as v from 'valibot'; const Theme = { Dark: 'dark', Light: 'light', } as const; const schema = v.object({ theme: v.enum(Theme), session: v.pipe(v.string(), v.brand('SessionId')), count: v.pipe(v.string(), v.transform(Number), v.number()), }); // document.cookie is a single string 'a=1; b=2' const obj: Record<string, string> = {}; for (const pair of document.cookie.split(';')) { const eq = pair.indexOf('='); // '=' divides by the first character (if the value is base64 / JWT, it includes '=') if (eq === -1) continue; const key = pair.slice(0, eq).trim(); if (key === '') continue; let value = pair.slice(eq + 1).trim(); try { value = decodeURIComponent(value); // Returns the encodeURIComponent used for writing. } catch { /* keep raw */ } if (!(key in obj)) obj[key] = value; // If the names are the same, the first one to choose wins. } const result = v.parse(schema, obj); ``` Users are being asked to write code for "interpreting cookie strings," which is not actually related to Valibot's schema, such as parsing `;` delimiters, dividing `=` by the first one, `decodeURIComponent`, and handling cookies with the same name. # Proposal Similar to `to-json-schema` and `i18n`, this will be provided as a separate package under `packages/`, or as a separate repository. The key to the design is **separation of responsibilities**. - **Normalization Layer (this package)** - Handles normalizing web standard inputs such as `Location`, `URL`, `URLSearchParams`, strings, into `Record`. It does not participate in type checking at all. - **Type Checking (User Schema)** - Narrows strings to meaningful types using `picklist`, `enum_`, `brand`, `transform`, etc. This is Valibot's strongest area. This separation significantly simplifies the process that users currently do with `parse → manual narrowing → as cast`. ## 1. Search parameters ```ts import * as v from 'valibot'; import * as vw from '@valibot/web-standards'; const Direction = { Left: 'LEFT', Right: 'RIGHT', } as const; const schema = v.object({ q: v.string(), tags: v.array(v.string()), page: v.pipe(v.string(), v.transform(Number), v.number()), /* It can be treated as an Enum, not just a simple String. There are also other cases where you might want to treat it as a Branded Type, such as userId. */ direction: v.enum(Direction) }); // The package handles all aspects of data extraction, URLSearchParams conversion, and arraying of keys with the same name. const result = vw.parseSearchParams(schema, document.location); // { q: string; tags: string[]; page: number } ``` `@valibot/web-standards` is responsible for the aforementioned preprocessing (`document.location` → extracting `?foo=foo&bar=bar` → `URLSearchParams` → creating an array of keys with the same name using `getAll` → `Record`). By accepting not only `Location` but also `URL` / string / `URLSearchParams` as input, the same functions can be used in both browsers (`document.location`) and servers (`new URL(request.url)`). In line with the synthesis philosophy, we provide not only parse sugar but also **normalized schemas/actions that can be inserted into pipes**. ```ts // Components that can be combined (placed at the entry point of the pipe) const schema = v.pipe( vw.searchParams(), // Normalize Web standard input to Record using transform v.object({ q: v.string(), tags: v.array(v.string()) }) ); v.parse(schema, document.location); // A thin sugar coating placed on top of it vw.parseSearchParams(objSchema, document.location); vw.safeParseSearchParams(objSchema, document.location); ``` ## 2. Cookie ```ts import * as v from 'valibot'; import * as vw from '@valibot/web-standards'; const Theme = { Dark: 'dark', Light: 'light', } as const; const schema = v.object({ theme: v.enum(Theme), // enum resolve = narrowing session: v.pipe(v.string(), v.brand('SessionId')), // validated branded type count: v.pipe(v.string(), v.transform(Number), v.number()), }); const state = vw.parseCookies(schema, document.cookie); // { theme: 'dark' | 'light'; session: string & Brand<'SessionId'>; count: number } ``` `@valibot/web-standards` is responsible for normalizing cookie strings. - Parsing of `;` delimiters - Dividing `=` by the first one (handles cases where the value is base64/JWT and contains `=`) - Removal of double quotes according to RFC 6265 - `decodeURIComponent` (restores wire format; this is the responsibility of the normalization process, not type interpretation. We could also control whether or not decoding is performed by adding an argument.) - Handling of cookies with the same name (first one wins) Furthermore, the fact that **the cookies being validated have no attributes** is an advantage that narrows the scope. Both `document.cookie` and the request's `Cookie` header only contain `name=value; name=value`, and attributes such as `path` and `expires` do not appear. Therefore, complex attribute parsing is unnecessary. As a result, the branded type, which was previously applied unvalidated with `as` casts, will change to **branding with runtime validation**. Both retrieval and narrowing will be consolidated into a single schema, and `as` will disappear. # Naming The title is **`@valibot/web-standards`**, but the following are also candidates: - `@valibot/web-standards` - Most accurately represents the reality that the input is a Web standard object: `URLSearchParams` / `URL`. - `@valibot/web` - Short and easy to remember, and fits the "naming by function/domain" lineage of `to-json-schema` / `i18n`. It has extensibility that won't break even if inputs such as `FormData` or `localStorage` are added in the future. - `@valibot/browser-state` - Perfectly matches the term "Browser state" on the Use Cases page, and has a strong connection to the documentation. However, since this package also works on the server, "browser" might appear narrower than it actually is. > Note: We would like to avoid using the singular form `@valibot/web-standard` as it may cause confusion with the **Standard Schema** specification that Valibot supports. # Contributing - [x] I’d love to implement this feature and help maintain this package
2 条评论