Skip to content
Layers
Esc
navigateopen⌘Jpreview
On this page

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 ValidatedLayerHandleopen/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’s payload is schema input; the component sees schema output.
  • upsert re-validates — invalid input rejects and leaves the existing layer unchanged.

Type helpers: InferValidatorInput, InferValidatorOutput, ValidationIssue. See Error handling for narrowing rejections.

Last updated on July 18, 2026

Was this page helpful?