用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/dev-hann/song --skill zod-validation命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | zod-validation |
| description | Create Zod schemas for runtime validation with basic and advanced features |
| license | MIT |
| compatibility | opencode |
| metadata | {"category":"development","complexity":"intermediate"} |
Use this when you need to:
I'll ask clarifying questions if:
import { z } from 'zod';
export const VideoSchema = z.object({
id: z.string().min(1, 'ID is required'),
type: z.literal('video'),
title: z.string().min(1, 'Title is required'),
description: z.string().default(''),
duration: z.number().default(0),
viewCount: z.number().default(0),
thumbnail: z.string().url('Invalid thumbnail URL')
});
// Infer type from schema
export type Video = z.infer<typeof VideoSchema>;
const NonEmptyStringSchema = z.string()
.min(1, 'Cannot be empty')
.max(100, 'Must be 100 characters or less');
const EmailSchema = z.string().email('Invalid email address');
const UrlSchema = z.string().url('Invalid URL');
const RegexSchema = z.string()
.regex(/^[A-Z]/, 'Must start with uppercase');
const YouTubeUrlSchema = z.string()
.url('Invalid URL')
.refine(
(url) => {
return url.includes('youtube.com') || url.includes('youtu.be');
},
'Must be a YouTube URL'
);
const PositiveNumberSchema = z.number()
.positive('Must be positive');
const BoundedNumberSchema = z.number()
.min(0, 'Cannot be negative')
.max(100, 'Cannot exceed 100');
const IntegerSchema = z.number()
.int('Must be an integer');
// Coerce string to number
const CoercedNumberSchema = z.coerce.number();
const BooleanSchema = z.boolean();
// Coerce string to boolean
const CoercedBooleanSchema = z.coerce.boolean();
const VideoTypeSchema = z.enum(['video', 'channel', 'playlist']);
const VideoLiteralSchema = z.literal('video');
const QualitySchema = z.enum(['360p', '720p', '1080p']);
export const VideoSchema = z.object({
id: z.string(),
title: z.string(),
// Optional without default
description: z.string().optional(),
published: z.string().optional(),
isLive: z.boolean().optional(),
// Optional with default
duration: z.number().default(0),
viewCount: z.number().default(0)
});
// Array validation
const StringArraySchema = z.array(z.string())
.min(1, 'At least one item required')
.max(100, 'Cannot have more than 100 items');
const VideoArraySchema = z.array(VideoSchema);
// Nested objects
const ChannelInfoSchema = z.object({
id: z.string(),
name: z.string(),
thumbnail: z.union([
z.string().url(),
z.object({
url: z.string().url(),
width: z.number().positive(),
height: z.number().positive()
})
]),
subscribers: z.number().default(0)
});
const VideoSchema = z.object({
id: z.string(),
title: z.string(),
channel: ChannelInfoSchema
});
const ThumbnailSchema = z.union([
z.string().url('Invalid thumbnail URL'),
z.object({
url: z.string().url('Invalid thumbnail URL'),
width: z.number().positive('Width must be positive'),
height: z.number().positive('Height must be positive')
})
]);
const StringOrNumberSchema = z.union([
z.string(),
z.number()
]);
export const SearchParamsSchema = z.object({
q: z.string().min(1, 'Query is required'),
filter: z.enum(['video', 'channel', 'playlist']).optional(),
limit: z.coerce.number().positive().optional()
});
export type SearchParams = z.infer<typeof SearchParamsSchema>;
export const SearchResponseSchema = z.object({
query: z.string(),
results: z.array(VideoSchema),
has_continuation: z.boolean().default(false)
});
export type SearchResponse = z.infer<typeof SearchResponseSchema>;
export const SearchResultSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('video'),
id: z.string(),
title: z.string()
}),
z.object({
type: z.literal('channel'),
id: z.string(),
name: z.string()
}),
z.object({
type: z.literal('playlist'),
id: z.string(),
title: z.string()
})
]);
// Capitalize first letter
const CapitalizedNameSchema = z.string()
.transform((val) => {
return val.charAt(0).toUpperCase() + val.slice(1);
});
// Handle YouTube.js TextRun objects
const StringOrTextRunSchema = z.union([
z.string(),
z.object({
text: z.string(),
runs: z.array(z.any())
})
]).transform((val) => {
if (typeof val === 'string') return val;
if ('text' in val && typeof val.text === 'string') {
return val.text;
}
return String(val);
});
// Trim strings before validation
const TrimmedStringSchema = z.preprocess(
(val) => {
if (typeof val === 'string') {
return val.trim();
}
return val;
},
z.string().min(1, 'Cannot be empty')
);
// Normalize email
const NormalizedEmailSchema = z.preprocess(
(val) => {
if (typeof val === 'string') {
return val.toLowerCase().trim();
}
return val;
},
z.string().email('Invalid email')
);
// Convert strings to numbers
const NumberStringSchema = z.preprocess(
(val) => {
if (typeof val === 'string') {
return parseInt(val, 10);
}
return val;
},
z.number().int('Must be a number')
);
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
id: z.string(),
name: z.string(),
subcategories: z.array(CategorySchema).optional()
})
);
export type Category = z.infer<typeof CategorySchema>;
// Intersection
const BaseSchema = z.object({
id: z.string(),
createdAt: z.date()
});
const VideoSchema = BaseSchema.extend({
type: z.literal('video'),
title: z.string()
});
// Record (Dictionary)
const MetadataSchema = z.record(
z.string().min(1),
z.string()
);
const VideoTagsSchema = z.record(z.string(), z.string());
// Tuple
const CoordinatesSchema = z.tuple([
z.number(), // x
z.number() // y
]);
const RangeSchema = z.tuple([
z.number().min(0), // start
z.number().min(0) // end
]);
// Passthrough: Allow extra fields
const PassthroughSchema = z.object({
id: z.string(),
name: z.string()
}).passthrough();
// Strict: Reject extra fields
const StrictSchema = z.object({
id: z.string(),
name: z.string()
}).strict();
// Strip: Remove extra fields (default)
const StripSchema = z.object({
id: z.string(),
name: z.string()
});
const PasswordSchema = z.string()
.min(8, 'Must be at least 8 characters')
.refine(
(val) => /[A-Z]/.test(val),
'Must contain at least one uppercase letter'
)
.refine(
(val) => /[a-z]/.test(val),
'Must contain at least one lowercase letter'
);
const UsernameSchema = z.string()
.refine(
(val) => /^[a-zA-Z0-9]+$/,
'Must be alphanumeric with no special characters'
);
const UniqueEmailSchema = z.string()
.email('Invalid email')
.refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
'Email already exists'
);
const AsyncVideoIdSchema = z.string()
.min(11, 'Invalid video ID')
.refine(
async (id) => {
const exists = await checkVideoExists(id);
return exists;
},
'Video does not exist'
);
const FullVideoSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string(),
duration: z.number()
});
// Make all fields optional
const PartialVideoSchema = FullVideoSchema.partial();
// Make specific fields optional
const SomeOptionalSchema = FullVideoSchema.partial({
description: true,
duration: true
});
// Make all fields required
const RequiredVideoSchema = PartialVideoSchema.required();
// Pick specific fields
const VideoBasicSchema = FullVideoSchema.pick({
id: true,
title: true
});
// Omit specific fields
const VideoMetaSchema = FullVideoSchema.omit({
description: true
});
import { VideoSchema } from '@/schemas/video';
export function parseVideo(data: unknown): Video | null {
const result = VideoSchema.safeParse(data);
if (!result.success) {
console.error('Validation errors:', result.error.errors);
return null;
}
return result.data;
}
export function processVideo(data: unknown): Video {
try {
return VideoSchema.parse(data);
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Validation failed: ${error.errors[0].message}`);
}
throw error;
}
}
Before committing schema code:
| Basic Method | Use For |
|---|---|
z.string() | String type |
z.number() | Number type |
z.boolean() | Boolean type |
z.enum() | Fixed set of values |
z.literal() | Specific value |
z.object() | Object shape |
z.array() | Array type |
z.union() | Multiple possible types |
| Advanced Method | Use For |
|---|---|
z.discriminatedUnion() | Type field discrimination |
.refine() | Custom validation |
.transform() | Data transformation |
.preprocess() | Input normalization |
z.lazy() | Recursive types |
.passthrough() | Allow extra fields |
.strict() | Reject extra fields |
.partial() | Make fields optional |
.required() | Make fields required |
.pick() | Select specific fields |
.omit() | Remove specific fields |
| Modifier | Purpose |
|---|---|
.min() | Minimum value/length |
.max() | Maximum value/length |
.optional() | Optional field |
.default() | Default value |
.safeParse() | Non-blocking validation |
.parse() | Blocking validation |
Related SKILLS: code-standards.md, parser-patterns.md, api-route-development.md