Input validation and output schema
Guardrails live at the seams. The inbound seam is where the user talks to your route handler: parse the body with Zod, cap message length, reject anything suspicious. The outbound seam is where the model talks back: when you need structured data, use generateObject with a Zod schema so the model cannot ship malformed JSON.
import { chatRequestSchema } from '@/lib/schemas';
export async function POST(req: Request) {
const body = await req.json();
const parsed = chatRequestSchema.safeParse(body);
if (!parsed.success) {
return Response.json(
{ error: 'Invalid request', issues: parsed.error.issues },
{ status: 400 },
);
}
const lastUser = parsed.data.messages.at(-1);
const userText = (lastUser?.parts ?? [])
.filter((p) => p.type === 'text')
.map((p: { type: string; text?: string }) => p.text ?? '')
.join('');
if (userText.length > 2_000) {
return Response.json(
{ error: 'Message too long. Keep it under 2000 characters.' },
{ status: 413 },
);
}
// ... streamText call follows
}Two input guardrails: Zod shape validation and a length cap. Anything that does not fit bounces with a 4xx before the LLM spins up.
import { generateObject } from 'ai';
import { z } from 'zod';
const summarySchema = z.object({
title: z.string().max(80),
bullets: z.array(z.string().max(120)).min(3).max(5),
confidence: z.number().min(0).max(1),
});
export async function summarizeSearchResults(text: string) {
const { object } = await generateObject({
model: getModel(),
schema: summarySchema,
prompt: `Summarize this web page for a busy engineer:\n\n${text}`,
});
return object;
}generateObject forces the model to produce JSON that fits the Zod schema. If it cannot, the SDK retries internally. Your downstream code receives a fully typed object or a thrown error, never malformed JSON.
Quiz: Quiz
Loading practice…