Building boundaries¶
Up to this point, you’ve been working with single‑value pipelines — validating one field at a time. That’s the foundation. But real applications rarely validate just one value. They validate objects: user profiles, API payloads, form submissions, configuration files, database records.
That’s where boundaries come in.
A boundary is Jane’s way of validating a whole object by running multiple pipelines — one per field — and combining their results into a single, coherent decision. Boundaries give you structure, clarity, and a predictable way to validate complex data.
This chapter shows you how boundaries work, how to build them, and how they fit into the larger Jane ecosystem.
What Is a Boundary?¶
A boundary is a function that:
- Takes an object.
- Runs a pipeline for each field.
- Collects all results.
- Applies boundary‑level policy.
- Returns a single
JaneResultfor the entire .
Think of it as a structured, multi‑pipeline validator for real-world data.
Boundaries are explicit. Boundaries are predictable. Boundaries are the unit of meaning for real systems.
Why Boundaries Matter¶
Boundaries solve several real problems:
- Validating multiple fields consistently.
- Shaping the final output object.
- Applying shared policy across fields.
- Producing a single decision for the entire structure.
- Generating structured events for each field.
- Integrating with telemetry and analysis features.
They also make your code cleaner:
// Without boundaries
const name = await jane.value(input.name).string().run();
if (!name.ok) {
...
}
const age = await jane.value(input.age).parse("numeric").positive().run();
if (!age.ok) {
...
}
const email = await jane.value(input.email).isEmail().run();
if (!email.ok) {
...
}
// With a boundary
const result = await jane.boundary({
name: jane.value(input.name).string(),
age: jane.value(input.age).parse("numeric").positive(),
email: jane.value(input.email).isEmail()
});
if (!result.ok) {
...
}
Boundaries turn scattered validation into a single, coherent system.
Creating Your First Boundary¶
A boundary is created by calling:
jane.boundary({...})
Each field is a pipeline:
const result = await jane.boundary({
name: jane.value(input.name).string().nonEmpty(),
age: jane.value(input.age).parse('numeric').positive(),
email: jane.value(input.email).nonEmpty().isEmail(),
});
The result is a single JaneResult representing the entire object.
Inspecting Boundary Results¶
A boundary result looks like this:
{
ok: true,
issues: undefined,
events: [
{
phase: 'scan',
kind: 'info',
code: 'scan.is.disabled',
path: {
segments: [],
inputName: undefined,
toString: [Function: toString]
},
message: 'Scan stage was skipped because policy.scan is false.',
userMessage: undefined,
metadata: {
scan: false,
mode: 'moderate',
runId: undefined,
createdAt: 1768630949771
}
},
],
values: { name: 'John Doe', age: 37, email: 'john.doe@example.com' },
fields: {...},
metadata: {...}
}
If any field fails, the entire boundary fails:
const result = await UserBoundary({
name: jane.value("").string().nonEmpty(),
age: jane.value("not-a-number").parse('numeric').positive(),
email: jane.value("alice@example.com").nonEmpty().isEmail(),
});
You get:
{
ok: false,
issues: [
{ path: "$.name", code: "validate.nonEmpty.failed", ... },
{ path: "$.age", code: "parse.numericString.invalid", ... }
],
...
}
Boundaries give you:
- Field‑level paths.
- Structured events.
- A single decision.
- A shaped final object.
Shaping the Output¶
Boundaries automatically shape the final object based on the pipelines you define.
If a pipeline parses or transforms a value, the boundary output reflects that:
const result = await UserBoundary({
name: " Alice ",
age: " 42 ",
email: "alice@example.com"
});
console.log(result.value);
// {
// name: "Alice",
// age: 42,
// email: "alice@example.com"
// }
Normalization and parsing happen inside each field’s pipeline.
Boundary‑Level Policy¶
Every boundary has its own policy layer, separate from the individual pipelines inside it. This lets you control strictness, analysis features, naming, and severity transforms at the boundary level, without having to repeat configuration for each field.
You apply boundary‑level policy by calling the relevant boundary:
await jane.strictBoundary({...}) // Strict mode - all pipelines
await jane.boundary({...}) // Moderate mode - all pipelines
await jane.laxBoundary({...}) // Lax mode - all pipelines
Boundary policy affects:
- Mode (strict, moderate, lax).
- Analysis features (diff, explain, replay, telemetry).
- Severity transforms.
- Reject and review patterns.
- Boundary and pipeline naming.
This policy applies to every field pipeline inside the boundary unless a field explicitly overrides it.
Boundary policy is the right place to enforce consistent behavior across an entire object.
Using Boundaries in Real Applications¶
Boundaries are designed for real‑world usage. Here are the most common patterns.
API Validation¶
app.post("/register", async (req, res) => {
const result = await UserBoundary({
name: jane.value("").string().nonEmpty(),
age: jane.value("not-a-number").parse('numeric').positive(),
email: jane.value("alice@example.com").nonEmpty().isEmail(),
});
if (!result.ok) {
return res.status(400).json({ errors: result.issues });
}
createUser(result.value);
res.json({ success: true });
});
Boundaries give you:
- A single decision
- Shaped, parsed, validated data
- Structured issues for error responses
Perfect for backend request validation.
Form Validation¶
const result = await UserBoundary({
name: jane.value("").string().nonEmpty(),
age: jane.value("not-a-number").parse('numeric').positive(),
email: jane.value("alice@example.com").nonEmpty().isEmail(),
});
if (!result.ok) {
showErrors(result.issues);
} else {
submit(result.value);
}
Boundaries make client‑side validation predictable and consistent.
Configuration Validation¶
const config = await UserBoundary({
port: jane.value("80").parse('numeric').safeInteger().isPort(),
email_host: jane.value('localhost').nonEmpty().isEmailStrict()
email_port: jane.value("587").parse('numeric').safeInteger().isPort(),
});
if (!config.ok) {
throw new Error("Invalid configuration:\n" + JSON.stringify(config.issues, null, 2));
}
Boundaries are ideal for validating environment variables, JSON config files, and CLI input.
What You’ve Learned¶
From this point forward, you understand:
- How boundaries combine multiple pipelines.
- How boundary‑level policy shapes the entire object.
- How naming improves debugging and observability.
- How boundaries fit naturally into APIs, forms, and config validation.
- How boundaries produce a single, coherent
JaneResult.
Boundaries are where Jane becomes a full data boundary framework — not just a validator. Let's keep it moving.