Js-schema
Introduction
I don’t enjoy writing raw JSON Schema, and I also didn’t want to pull in a pile of npm packages just to solve a problem I understood well enough myself.
Js-schema exists mostly because I like the ergonomics of tools like zod and convex, and
I wanted something in that direction opinionated, lightweight, and tailored to how I actually describe APIs.
The result is a small DSL that looks familiar if you’ve used schema builders before, but intentionally cuts corners where strict JSON Schema compliance becomes more painful than useful. You can think of it as JSON Schema with extra steps, except those steps are there to make authoring, reading, and maintaining Rest APIs less annoying.
Some parts are spec-compliant, some are not. That trade-off is deliberate.
Methods
Below are the core schema builders exposed by v.
Not all of them map 1:1 to JSON Schema keywords, and that’s fine.
The goal here is expressiveness and clarity, not strict adherence to a spec.
| Type | Description | Example | Status |
|---|---|---|---|
string | Defines a string value. Supports additional constraints like enums, defaults, etc. | v.string() | done |
number | Defines a numeric value. Intended for integers and floats unless further constrained. | v.number() | done |
null | Explicitly allows a null value. Useful when modeling nullable fields without unions. | v.null() | done |
object | Defines an object with a fixed set of properties and their schemas. | v.object({ key: v.string() }) | done |
oneOf | Defines a union type where the value must match exactly one of the provided schemas. | v.oneOf([v.string(), v.number()]) | done |
array | Defines a homogeneous array where all items share the same schema. | v.array(v.string()) | done |
array (mixed) | Non-homogeneous arrays are also supported when order and variance matter. | v.array([v.string(), v.number()]) | done |
response body | Describes a response payload, usually keyed by status or outcome. | v.reply({ ok: v.json({ ... }) }) | done |
text | Raw text payloads (non-JSON). Typically used for plain text responses. | v.text("plain text") | todo |
xml | XML payloads. Mostly useful for legacy or external APIs. | v.xml("<xml />") | todo |
json | JSON payload shorthand, usually wrapping an object schema. | v.json({ key: v.string() }) | complete |
binary | Binary or file-based payloads (uploads/downloads). | v.file("example.png") | wip |
Auth methods
Authentication methods are modeled as first-class exports. Each method return an auth definition that can be inherited by requests in the folder.
// HTTP Basic authentication using a username and password pair.
export const basicAuth = v.basicAuth();// Bearer token authentication, commonly used with JWT-based APIs.
export const bearerAuth = v.bearerAuth({ format: "JWT" });// API key passed via a custom HTTP header (for example: x-api-key).
export const apiKeyHeader = v.apiKeyHeaderAuth({ name: "x-api-key" });// API key passed as a query parameter on the request URL.
export const apiKeyQuery = v.apiKeyQueryAuth({ name: "api-key" });These are intentionally simple and declarative.
Server
Server definitions describe where your API is hosted. You can define a single server, multiple servers, or even parameterized servers using variables.
export const servers = v.server("https://getbruno.com");Multiple servers are supported when your API is deployed to more than one region or environment:
export const servers = v.array([
v.server("https://eu.getbruno.com"),
v.server("https://us.getbruno.com"),
]);For more advanced setups, server URLs can be parameterized using variables with constraints and defaults:
export const servers = v
.server("https://{{location}}.vercel.com")
.vars({
location: v.string().enum(["eu", "us"]).default("us"),
});This allows the same schema to adapt cleanly across environments without duplicating definitions or hardcoding URLs.
Validation
This is not a validation library and I don't intend to or have any plans to make it one.