Validation

Pass any Standard Schema validator (Zod ≥3.24, Valibot, ArkType…) as the second argument to readSignals() to validate at runtime and infer the type from the schema. Astro already ships Zod as astro/zod, so this needs no extra dependency. Failures throw SignalsValidationError, which carries the issues and a .response() shortcut for a 422:

import { z } from 'astro/zod';
import { readSignals, SignalsValidationError } from '@wrux/astro-datastar/server';

const signals = z.object({ q: z.string().max(100).default('') });

export const GET: APIRoute = async ({ request }) => {
  try {
    const { q } = await readSignals(request, signals); // q: string
    // …
  } catch (err) {
    if (err instanceof SignalsValidationError) return err.response(); // 422
    throw err;
  }
};

Rendering failures into the UI

A bare 422 leaves the page silently stale. For user-correctable input, catch the error and render it into the fragment you’d normally return — see active search:

let q = '';
let error: string | undefined;
try {
  ({ q } = await readSignals(request, signals));
} catch (err) {
  if (!(err instanceof SignalsValidationError)) throw err;
  error = err.issues.map((i) => i.message).join('; ');
}
// render the error into the fragment the page already shows

Per-field form errors

For forms, read raw and use safeParse so you can re-render the form with the submitted values and field-level messages — see form validation:

const contact = z.object({
  name: z.string().trim().min(2, 'Please give your name (at least 2 characters).'),
  email: z.string().trim().email('That email address doesn’t look right.'),
});

const raw = await readSignals(request); // raw, so values survive
const result = contact.safeParse(raw);
if (!result.success) {
  return html(await container.renderToString(ContactForm, { props: {
    values: raw,
    errors: result.error.flatten().fieldErrors,
  } }));
}