Payload validation
Parse untrusted payload at open with validate, Standard Schema, or a sync function.
A bad payload at open should reject before a component ever mounts. Pass validate on layerOptions or at bag-form open to parse untrusted input synchronously; failure rejects open with PayloadValidationError and mounts nothing.
Wired handle (ValidatedLayerHandle)
When validate is on layerOptions, createLayer / useLayer return a ValidatedLayerHandle — open/upsert take schema input; current and update use parsed output:
import { useLayer } from "@stainless-code/react-layers";
const prompt = layerOptions<{ label: string }, string>({
stack: "prompt",
key: ["prompt", "rename"],
component: Prompt,
validate: (input: unknown) => {
const label =
typeof input === "object" && input !== null && "label" in input
? (input as { label: unknown }).label
: undefined;
if (typeof label !== "string" || !label.trim()) {
throw new Error("Label required");
}
return { label: label.trim() };
},
});
function RenameButton() {
const c = useLayer(prompt);
// open's arg is validator INPUT; the layer stores OUTPUT
return (
<button type="button" onClick={() => void c.open({ label: " hello " })}>
Rename
</button>
);
}
// component payload: { label: "hello" }
Headless hosts use createLayer(prompt, client) with the same input/output split.
Standard Schema
Any library implementing Standard Schema v1 works — Zod 4, Valibot, ArkType, etc.:
import * as z from "zod";
import { createLayer } from "@stainless-code/layers";
const schema = z.object({ email: z.string().email() });
const invite = layerOptions<{ email: string }, void>({
stack: "modal",
key: ["invite"],
component: Invite,
validate: schema,
});
const c = createLayer(invite, client);
await c.open({ email: "user@example.com" });
Core ships the StandardSchemaV1 type interface — no schema-library dependency.
PayloadValidationError
When validation fails, open rejects with PayloadValidationError:
import { createLayer, isPayloadValidationError } from "@stainless-code/layers";
const c = createLayer(prompt, client);
try {
await c.open(untrusted);
} catch (err) {
if (isPayloadValidationError(err)) {
for (const issue of err.issues) {
console.log(issue.message, issue.path);
}
}
}
Bag-form (escape hatch)
Pass validate at the call site when the options bag does not carry it:
await client.open({
...invite,
payload: { email: "user@example.com" },
validate: schema,
});
Rules
- Validation runs synchronously at
open— async schemas throw at open time (Async payload validation is not supported). open’spayloadis schema input; the component sees schema output.upsertre-validates — invalid input rejects and leaves the existing layer unchanged.
Type helpers: InferValidatorInput, InferValidatorOutput, ValidationIssue. See Error handling for narrowing rejections.