Below are the solutions for the exercises found here.
Solutions
Hello Zod
Goal: Validate an object with required name (string) and age (non-negative number).
import { z } from 'zod';
// 1. Define the schema
const basicUserSchema = z.object({
name: z.string(),
age: z.number().min(0, { message: "Age can't be negative." }),
});
// 2. Test data
const validData = { name: 'Ada', age: 36 };
const missingAge = { name: 'Charles' };
const negativeAge = { name: 'Bobby Tables', age: -1 };
// 3. Validate
try {
const result = basicUserSchema.parse(validData);
console.log('validData passed:', result);
} catch (err) {
console.error('validData failed:', err);
}
try {
const result = basicUserSchema.parse(missingAge);
console.log('missingAge passed:', result);
} catch (err) {
console.error('missingAge failed:', err);
}
try {
const result = basicUserSchema.parse(negativeAge);
console.log('negativeAge passed:', result);
} catch (err) {
console.error('negativeAge failed:', err);
}
Note: This should illustrate how .parse()
will throw an error for the invalid objects.
Solutions
All About Options
Goal: Make age optional. If not provided, either default it to 0 or show an error.
import { z } from 'zod';
const optionalAgeSchema = z.object({
name: z.string(),
// Option A: Make age optional, and if not provided, transform it to 0
age: z
.number()
.min(0)
.optional()
.transform((val) => val ?? 0),
});
const dataWithAge = { name: 'Ada', age: 36 };
const dataWithoutAge = { name: 'Ada' };
try {
// With age
const result1 = optionalAgeSchema.parse(dataWithAge);
console.log('dataWithAge passed:', result1);
// { name: "Ada", age: 36 }
// Without age (defaults to 0)
const result2 = optionalAgeSchema.parse(dataWithoutAge);
console.log('dataWithoutAge passed:', result2);
// { name: "Ada", age: 0 }
} catch (err) {
console.error('Failed:', err);
}
Note: If you wanted to throw an error instead of defaulting, omit the .transform(…) or throw in .refine().
Solutions
On the Street Where You Live
Goal: Nested objects, arrays of addresses, at least one address required.
import { z } from 'zod';
const addressSchema = z.object({
street: z.string(),
city: z.string(),
zip: z.string(),
apartmentNumber: z.string().optional(),
});
const userProfileSchema = z.object({
name: z.string(),
addresses: z.array(addressSchema).min(1, { message: 'At least one address required.' }),
});
const validProfile = {
name: 'Grace Hopper',
addresses: [
{
street: '123 Naval Dr',
city: 'Arlington',
zip: '76010',
},
{
street: '456 Code Ave',
city: 'Boston',
zip: '02108',
apartmentNumber: 'Apt 101',
},
],
};
const invalidProfile = {
name: 'No Addresses Provided',
addresses: [],
};
try {
const parsedValid = userProfileSchema.parse(validProfile);
console.log('Valid profile:', parsedValid);
} catch (err) {
console.error('Valid profile failed:', err);
}
try {
const parsedInvalid = userProfileSchema.parse(invalidProfile);
console.log("Invalid profile passed (which shouldn't happen):", parsedInvalid);
} catch (err) {
console.error('Invalid profile failed as expected:', err);
}
Solutions
Now for Something Completely Different: Unions
Goal: Accept either the string “anonymous” or an object with id (number) and name (string).
import { z } from 'zod';
// Create a union of two schemas:
const userIdentitySchema = z.union([
z.literal('anonymous'),
z.object({
id: z.number(),
name: z.string(),
}),
]);
// Examples
const anonymousVal = 'anonymous';
const validObject = { id: 1, name: 'Alan' };
const invalidObject = { id: 'wrong', name: 'Marvin' };
try {
console.log('anonymousVal passes:', userIdentitySchema.parse(anonymousVal));
console.log('validObject passes:', userIdentitySchema.parse(validObject));
// This will fail:
console.log('invalidObject passes:', userIdentitySchema.parse(invalidObject));
} catch (err) {
console.error('invalidObject failed as expected:', err);
}
Solutions
Refining Your Tastes
Goal: Use .refine()
to check if a number is prime.
import { z } from 'zod';
// A simple prime checker (not optimized, but fine for demonstration)
function isPrime(num: number): boolean {
if (num < 2) return false;
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) return false;
}
return true;
}
const primeNumberSchema = z.number().refine(isPrime, {
message: 'Quantity must be prime!',
});
try {
console.log('5 is prime:', primeNumberSchema.parse(5)); // Passes
console.log('10 is prime:', primeNumberSchema.parse(10)); // Fails
} catch (err) {
console.error('Failed:', err);
}
Solutions
Transform-ers: Validation in Disguise
Goal: Accept a YYYY-MM-DD string, transform it to a Date.
import { z } from 'zod';
const dateStringSchema = z
.string()
.refine(
(val) => {
// Basic check: must be parseable as a date
return !isNaN(new Date(val).valueOf());
},
{ message: 'Invalid date string' },
)
.transform((val) => new Date(val));
try {
const date = dateStringSchema.parse('2025-03-20');
console.log('Parsed date:', date); // Should be a valid Date object
} catch (err) {
console.error('Failed date parse:', err);
}
try {
dateStringSchema.parse('not-a-date'); // Will throw
} catch (err) {
console.error('Invalid date string:', err);
}
Solutions
Adding a Little Brand to Your Life
Goal: A UserId branded type that’s a valid UUID string.
import { z } from 'zod';
const userIdSchema = z.string().uuid().brand<'UserId'>();
type UserId = z.infer<typeof userIdSchema>;
// -> string & { __brand: "UserId" }
const validUuid = '7c45ae8a-cf6e-4f72-b12f-6fbb21ce3ab9';
const invalidUuid = 'this-is-not-a-uuid';
try {
const userId = userIdSchema.parse(validUuid);
console.log('Branded UserId:', userId);
// If you try to pass invalidUuid, it will throw:
userIdSchema.parse(invalidUuid);
} catch (err) {
console.error('Failed to parse userId:', err);
}
Test Branding: If you have a function that expects a UserId type, passing a normal string should fail type-check (in TypeScript). This won’t fail at runtime, but at compile time.
Solutions
Making “Partial,” “Pick,” or “Omit” Your Best Friends
Goal: Use an existing large schema, then create partial or subset schemas using .partial(), .pick(), .omit().
import { z } from 'zod';
const fullUserSchema = z.object({
name: z.string(),
email: z.string().email(),
phoneNumber: z.string().optional(),
addresses: z
.array(
z.object({
street: z.string(),
city: z.string(),
zip: z.string(),
}),
)
.optional(),
});
// PART 1: Partial user update schema (all fields become optional)
const partialUserUpdateSchema = fullUserSchema.partial();
// PART 2: Public profile: pick only certain fields to reveal publicly
const publicProfileSchema = fullUserSchema.pick({
name: true,
addresses: true,
});
// PART 3: Omit sensitive data—e.g., remove `email` from the user object
const userWithoutEmailSchema = fullUserSchema.omit({
email: true,
});
// Test them out
const sampleData = {
name: 'Test User',
email: 'test@example.com',
phoneNumber: '123-456-7890',
addresses: [{ street: '100 Test Ln', city: 'Nowhere', zip: '99999' }],
};
// 1. Partial update
const partialUpdate = { phoneNumber: '987-654-3210' };
try {
console.log('Partial update passes:', partialUserUpdateSchema.parse(partialUpdate));
} catch (err) {
console.error('Partial update error:', err);
}
// 2. Public profile
try {
console.log('Public profile:', publicProfileSchema.parse(sampleData));
} catch (err) {
console.error('Public profile error:', err);
}
// 3. Omit email
try {
console.log('User without email:', userWithoutEmailSchema.parse(sampleData));
} catch (err) {
console.error('User without email error:', err);
}
Solutions
Custom Schemas with z.custom()
Goal: Validate a hex color string: must start with # and be 3 or 6 hex digits.
import { z } from 'zod';
const hexColorSchema = z.custom<string>(
(val) => {
if (typeof val !== 'string') return false;
// Simple regex check: must start with '#' and have 3 or 6 hex digits
return /^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(val);
},
{
message: 'Invalid hex color',
},
);
// Test
const validColors = ['#FFF', '#FFFFFF', '#abc', '#aBcDeF'];
const invalidColors = ['FFF', '#FFFFF', '#GGG', '#1234567', '#abcd'];
// Validate
for (const color of validColors) {
try {
console.log(`"${color}" passes:`, hexColorSchema.parse(color));
} catch (err) {
console.error(`"${color}" failed (unexpected):`, err);
}
}
for (const color of invalidColors) {
try {
console.log(`"${color}" passes (shouldn't):`, hexColorSchema.parse(color));
} catch (err) {
console.error(`"${color}" failed (expected):`, err);
}
}
Solutions
Put It All Together: Build a Form Validator
Goal: A registration form requiring username, password, email, optional birthDate as a valid date, and each field with its own constraints.
import { z } from 'zod';
// We'll create each field's schema and then combine.
const usernameSchema = z
.string()
.min(4, { message: 'Username must be at least 4 characters.' })
.max(16, { message: 'Username can be at most 16 characters.' });
const passwordSchema = z
.string()
.min(8, { message: 'Password must be at least 8 characters long.' })
// refine to check for a digit
.refine((val) => /d/.test(val), {
message: 'Password must contain at least one digit.',
})
// optional brand if you want to differentiate it:
.brand<'SecureString'>();
const emailSchema = z
.string()
.email({ message: 'Must be a valid email address.' })
// optional brand
.brand<'EmailAddress'>();
// Optional birthDate, but if provided, must be a valid date
const birthDateSchema = z
.string()
.optional()
.refine(
(val) => {
if (!val) return true; // optional
return !isNaN(new Date(val).valueOf());
},
{ message: 'Invalid birth date format.' },
)
.transform((val) => {
return val ? new Date(val) : undefined;
});
// Combine into a single schema
const registrationFormSchema = z.object({
username: usernameSchema,
password: passwordSchema,
email: emailSchema,
birthDate: birthDateSchema,
});
// Now test it
const validFormData = {
username: 'myuser',
password: 'secret123',
email: 'test@example.com',
birthDate: '1985-01-01',
};
const invalidFormData = {
username: 'me', // too short
password: 'nopass', // not enough chars, no digit
email: 'not-an-email', // not a valid email
birthDate: 'not-a-date', // can't parse
};
try {
const parsedValid = registrationFormSchema.parse(validFormData);
console.log('Valid form data parsed:', parsedValid);
// birthDate becomes a Date object, if you transform it.
} catch (err) {
console.error('Valid form data error (unexpected):', err);
}
try {
registrationFormSchema.parse(invalidFormData);
console.log("Invalid form data passed (which shouldn't happen)");
} catch (err) {
console.error('Invalid form data failed as expected:', err);
}
Key Takeaways:
- Use separate schemas per field or concept.
- Compose them with
z.object()
. - Lean on
.refine()
,.transform()
, branding, or any other Zod technique you find handy.