← スキル一覧に戻る

schema-validator
by deancochran
⭐ 1🍴 0📅 2026年1月22日
SKILL.md
name: schema-validator description: Creates and validates Zod schemas for data structures
Schema Validator Skill
When to Use
- Adding new data structures
- Creating form validation
- Setting up tRPC input validation
- Validating external API data
What This Skill Does
- Analyzes data structure requirements
- Creates Zod schema with proper types:
- Primitives: string, number, boolean
- Objects: nested structures
- Arrays: with item validation
- Unions: for variants
- Refinements: for complex validations
- Adds helpful error messages
- Generates TypeScript types from schema
- Adds to core package schemas if shared
Basic Schema Patterns
Primitive Types
import { z } from 'zod';
// String with validation
const nameSchema = z.string()
.min(1, 'Name is required')
.max(100, 'Name must be less than 100 characters');
// Number with validation
const ageSchema = z.number()
.int('Must be a whole number')
.positive('Must be positive')
.max(120, 'Must be less than 120');
// Boolean
const isActiveSchema = z.boolean();
// Date
const createdAtSchema = z.date();
// UUID
const idSchema = z.string().uuid();
// Email
const emailSchema = z.string().email('Invalid email address');
// URL
const websiteSchema = z.string().url('Invalid URL');
Object Schemas
const activitySchema = z.object({
id: z.string().uuid(),
name: z.string().min(1, 'Name required'),
type: z.enum(['run', 'bike', 'swim', 'other']),
distance: z.number().positive().optional(),
duration: z.number().int().positive(),
startTime: z.date(),
endTime: z.date(),
});
// Infer TypeScript type
type Activity = z.infer<typeof activitySchema>;
Nested Objects
const profileSchema = z.object({
id: z.string().uuid(),
name: z.string(),
settings: z.object({
notifications: z.boolean(),
theme: z.enum(['light', 'dark', 'auto']),
units: z.object({
distance: z.enum(['km', 'mi']),
elevation: z.enum(['m', 'ft']),
}),
}),
});
Array Schemas
// Array of primitives
const tagsSchema = z.array(z.string()).min(1, 'At least one tag required');
// Array of objects
const stepsSchema = z.array(
z.object({
id: z.string(),
name: z.string(),
duration: z.number(),
})
).min(1, 'At least one step required');
// Validate array length
const top5Schema = z.array(z.string()).max(5, 'Maximum 5 items');
Enum and Union Types
// Enum
const activityTypeSchema = z.enum(['run', 'bike', 'swim', 'other']);
// Union
const resultSchema = z.union([
z.object({ success: true, data: z.any() }),
z.object({ success: false, error: z.string() }),
]);
// Discriminated union (better type inference)
const resultSchema = z.discriminatedUnion('success', [
z.object({ success: z.literal(true), data: z.any() }),
z.object({ success: z.literal(false), error: z.string() }),
]);
Optional and Nullable
// Optional (can be undefined)
const descriptionSchema = z.string().optional();
// Nullable (can be null)
const middleNameSchema = z.string().nullable();
// Optional and nullable
const nicknameSchema = z.string().optional().nullable();
// With default value
const statusSchema = z.string().default('active');
Advanced Patterns
Refinements (Custom Validation)
const passwordSchema = z.string()
.min(8, 'Password must be at least 8 characters')
.refine(
(val) => /[A-Z]/.test(val),
'Password must contain uppercase letter'
)
.refine(
(val) => /[0-9]/.test(val),
'Password must contain a number'
);
// Refine on object
const dateRangeSchema = z.object({
startDate: z.date(),
endDate: z.date(),
}).refine(
(data) => data.endDate > data.startDate,
{
message: 'End date must be after start date',
path: ['endDate'], // Show error on endDate field
}
);
Transformations
// Transform string to number
const ageSchema = z.string()
.regex(/^\d+$/, 'Must be a number')
.transform((val) => parseInt(val, 10));
// Transform string to date
const dateSchema = z.string()
.transform((val) => new Date(val));
// Trim and lowercase
const emailSchema = z.string()
.email()
.transform((val) => val.trim().toLowerCase());
Conditional Validation
const formSchema = z.object({
type: z.enum(['personal', 'business']),
email: z.string().email(),
companyName: z.string().optional(),
}).refine(
(data) => {
// If business type, companyName is required
if (data.type === 'business') {
return !!data.companyName;
}
return true;
},
{
message: 'Company name required for business accounts',
path: ['companyName'],
}
);
Extending Schemas
// Base schema
const baseActivitySchema = z.object({
id: z.string().uuid(),
name: z.string(),
type: z.enum(['run', 'bike', 'swim']),
});
// Extend with additional fields
const detailedActivitySchema = baseActivitySchema.extend({
distance: z.number(),
duration: z.number(),
streams: z.array(z.object({
type: z.string(),
data: z.array(z.number()),
})),
});
// Or use merge
const activityWithProfileSchema = baseActivitySchema.merge(
z.object({
profile: profileSchema,
})
);
Partial and Pick
// All fields optional
const partialActivitySchema = activitySchema.partial();
// Specific fields optional
const updateActivitySchema = activitySchema.partial({
name: true,
description: true,
});
// Pick specific fields
const activityPreviewSchema = activitySchema.pick({
id: true,
name: true,
type: true,
});
// Omit specific fields
const createActivitySchema = activitySchema.omit({
id: true,
createdAt: true,
});
Form Validation
React Hook Form Integration
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const formSchema = z.object({
name: z.string().min(1, 'Name required'),
email: z.string().email('Invalid email'),
age: z.number().int().positive().max(120),
});
type FormData = z.infer<typeof formSchema>;
export function MyForm() {
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
email: '',
age: 0,
},
});
const onSubmit = (data: FormData) => {
console.log(data); // Validated data
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Form fields */}
</form>
);
}
tRPC Input Validation
Query Input
export const activityRouter = router({
list: protectedProcedure
.input(
z.object({
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
type: z.enum(['run', 'bike', 'swim', 'other']).optional(),
search: z.string().optional(),
})
)
.query(async ({ input, ctx }) => {
// Input is validated automatically
const { limit, offset, type, search } = input;
// ...
}),
});
Mutation Input
const createActivitySchema = z.object({
name: z.string().min(1, 'Name required'),
type: z.enum(['run', 'bike', 'swim', 'other']),
distance: z.number().positive().optional(),
duration: z.number().int().positive(),
});
export const activityRouter = router({
create: protectedProcedure
.input(createActivitySchema)
.mutation(async ({ input, ctx }) => {
// Input is validated
// Zod errors automatically converted to TRPC errors
// ...
}),
});
Validation at Runtime
Parse (throw on error)
try {
const activity = activitySchema.parse(unknownData);
// activity is typed as Activity
console.log(activity.name);
} catch (error) {
if (error instanceof z.ZodError) {
console.log(error.errors);
}
}
Safe Parse (no throw)
const result = activitySchema.safeParse(unknownData);
if (result.success) {
// result.data is typed as Activity
console.log(result.data.name);
} else {
// result.error is ZodError
console.log(result.error.errors);
}
Partial Validation
// Validate only some fields
const partialResult = activitySchema
.pick({ name: true, type: true })
.safeParse(unknownData);
Error Handling
Custom Error Messages
const schema = z.object({
email: z.string().email('Please enter a valid email address'),
age: z.number({
required_error: 'Age is required',
invalid_type_error: 'Age must be a number',
}).positive('Age must be positive'),
});
Error Formatting
const result = schema.safeParse(data);
if (!result.success) {
// Get field errors
const fieldErrors = result.error.flatten().fieldErrors;
// { email: ['Please enter a valid email'], age: ['Age must be positive'] }
// Map to form errors
Object.entries(fieldErrors).forEach(([field, messages]) => {
form.setError(field, { message: messages?.[0] });
});
}
Schema Location
Core Package (Shared)
packages/core/schemas/
├── activity.ts # Activity schemas
├── profile.ts # Profile schemas
├── activity_plan_v2.ts # Activity plan schemas
└── form-schemas.ts # Form-specific schemas
App-Specific
apps/mobile/lib/schemas/ # Mobile-only schemas
apps/web/lib/schemas/ # Web-only schemas
Critical Patterns
- ✅ Use meaningful error messages
- ✅ Validate at boundaries (user input, external APIs)
- ✅ Infer types from schemas (single source of truth)
- ✅ Use
.safeParse()when errors are expected - ✅ Use
.parse()when validation failure is exceptional - ✅ Place shared schemas in core package
- ✅ Test validation logic
- ✅ Document complex refinements
Example: Complete Activity Schema
// packages/core/schemas/activity.ts
import { z } from 'zod';
export const activitySchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
name: z.string().min(1, 'Activity name is required').max(200),
description: z.string().max(1000).optional(),
type: z.enum(['run', 'bike', 'swim', 'other'], {
errorMap: () => ({ message: 'Invalid activity type' }),
}),
distance: z.number().nonnegative().optional(),
duration: z.number().int().positive('Duration must be positive'),
elevationGain: z.number().nonnegative().optional(),
startTime: z.date(),
endTime: z.date(),
averageHeartRate: z.number().int().positive().max(220).optional(),
maxHeartRate: z.number().int().positive().max(220).optional(),
averagePower: z.number().nonnegative().optional(),
maxPower: z.number().nonnegative().optional(),
tss: z.number().nonnegative().optional(),
createdAt: z.date(),
updatedAt: z.date(),
}).refine(
(data) => data.endTime > data.startTime,
{
message: 'End time must be after start time',
path: ['endTime'],
}
).refine(
(data) => !data.maxHeartRate || !data.averageHeartRate || data.maxHeartRate >= data.averageHeartRate,
{
message: 'Max heart rate must be greater than or equal to average',
path: ['maxHeartRate'],
}
);
export type Activity = z.infer<typeof activitySchema>;
// Create variant for input (omit generated fields)
export const createActivitySchema = activitySchema.omit({
id: true,
userId: true,
createdAt: true,
updatedAt: true,
});
export type CreateActivityInput = z.infer<typeof createActivitySchema>;
// Update variant (all fields optional except id)
export const updateActivitySchema = activitySchema
.partial()
.required({ id: true });
export type UpdateActivityInput = z.infer<typeof updateActivitySchema>;
スコア
総合スコア
50/100
リポジトリの品質指標に基づく評価
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
レビュー
💬
レビュー機能は近日公開予定です