Skip to content

Form-Safe Parsing

Problem

Form submissions often arrive as strings, optional fields, and checkboxes. You need typed output without repeating conversion code for every field.

Solution

Use coercion and wrappers to normalize the form payload in one step, then read messages directly by path.

ts
import { SpellValidationError, s } from '@vielzeug/spell';

const SignupForm = s.object({
  age: s.coerce.number().int().min(18),
  email: s.string().email(),
  marketing: s.coerce.boolean().default(false),
  referralCode: s.string().trim().optional().nullable().required(),
});

const result = SignupForm.safeParse({
  age: '29',
  email: 'ada@example.com',
  marketing: 'true',
  referralCode: null,
});

if (result.success) {
  console.log(result.data.referralCode);
} else if (result.error instanceof SpellValidationError) {
  console.log(result.error.messagesAt('email'));
}

Pitfalls

  • .required() removes undefined but keeps null. That matters after .optional().nullable() chains.
  • s.coerce.boolean() is for loosely typed input at the boundary. Keep your app state typed after parsing.
  • Call safeParseAsync() if any nested field uses checkAsync().