| lang | en | ||
|---|---|---|---|
| title | Schemas Design | ||
| title-en | Schemas Design | ||
| title-ja | スキーマ設計思想 | ||
| related |
|
||
| instructions-for-ais |
|
This document explains the design decisions behind the lib/schemas module and its relationship with lib/types.
- Architecture Overview
- Why Separate Types and Schemas?
- Design Decisions
- Code Value Restrictions
- Schema Evolution
- Performance Considerations
┌─────────────────────────────────────────────────────────────┐
│ Compile-Time Layer (lib/types) │
│ - TypeScript type definitions │
│ - Zero runtime cost │
│ - IDE autocomplete & type checking │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Runtime Layer (lib/schemas) │
│ - Zod schema validation │
│ - External data validation │
│ - Runtime type safety │
└─────────────────────────────────────────────────────────────┘
// External data (API, file, network)
const externalData: unknown = await fetchData();
// Runtime validation (lib/schemas)
const result = normalizedPrototypeSchema.safeParse(externalData);
if (result.success) {
// Compile-time type (lib/types)
const validated: NormalizedPrototype = result.data;
// Now both TypeScript and runtime guarantee type safety
}Problem: Zod adds ~15KB (minified + gzipped) to bundle size.
Solution:
lib/types: Zero-cost TypeScript types (removed at runtime)lib/schemas: Only imported when validation is needed
Use Cases:
- Internal code: Use types only → No Zod in bundle
- External data: Import schemas → Validate before use
| Concern | lib/types | lib/schemas |
|---|---|---|
| When | Compile-time | Runtime |
| Where | TypeScript compiler | Browser/Node.js |
| What | Type annotations | Data validation |
| Who | Developers | External data sources |
Developers can choose:
- Type-only imports for internal code (lightweight)
- Schema imports for external data validation (comprehensive)
- Both for maximum safety
Decision: Use z.union([z.literal(1), z.literal(2), ...]) instead of z.number()
Rationale:
// ❌ Generic number - accepts any integer
status: z.number().int();
// ✅ Literal union - only accepts valid codes
status: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]);Benefits:
- Early detection of invalid data
- Alignment with TypeScript types (
StatusCode = 1 | 2 | 3 | 4) - Self-documenting schema
- Prevents accidental data corruption
Trade-offs:
- More verbose schema definition
- Cannot accept "unknown" future values
- Breaking change if API adds new codes
Mitigation: For forward compatibility, use a separate "lenient" schema for imports if needed.
Decision: Use .optional() matching exactOptionalPropertyTypes: true
Example:
// TypeScript type (lib/types/normalized-prototype.ts)
type NormalizedPrototype = {
licenseType?: undefined | LicenseTypeCode;
thanksFlg?: undefined | ThanksFlagCode;
};
// Zod schema (lib/schemas/normalized-prototype.ts)
const normalizedPrototypeSchema = z.object({
licenseType: z.union([z.literal(0), z.literal(1)]).optional(),
thanksFlg: z.union([z.literal(0), z.literal(1)]).optional(),
});Rationale:
- Matches TypeScript's strict optional property handling
- Prevents
nullvalues (onlyundefinedallowed) - Consistent with
exactOptionalPropertyTypes: true
Decision: Create lib/schemas as a shared module instead of per-module schemas
Benefits:
- Single source of truth for validation
- Reusable across fetcher, repository, store
- Consistent validation logic
- Easier maintenance
Structure:
// Shared schema
lib / schemas / normalized - prototype.ts;
// Used by:
lib / repository / schemas / serializable - snapshot.ts; // Imports from lib/schemas
lib / utils / validation / validators.ts; // Imports from lib/schemasSource: lib/types/codes.ts → StatusCode
Validation:
status: z.union([
z.literal(1), // アイデア (Idea)
z.literal(2), // 開発中 (In Development)
z.literal(3), // 完成 (Completed)
z.literal(4), // 供養 (Retired/Memorial)
]);Rationale: All four values appear in public API responses (verified from data analysis).
Source: lib/types/codes.ts → ReleaseFlagCode
Validation:
releaseFlg: z.union([
z.literal(1), // 下書き保存 (Draft)
z.literal(2), // 一般公開 (Public)
z.literal(3), // 限定共有 (Limited Sharing)
]);Rationale: Public API only returns releaseFlg=2, but schema allows all three for completeness.
Source: lib/types/codes.ts → LicenseTypeCode
Validation:
licenseType: z.union([
z.literal(0), // なし (None)
z.literal(1), // 表示(CC:BY)
]).optional();Rationale: API responses show only licenseType=1, but 0 is theoretically possible.
Source: lib/types/codes.ts → ThanksFlagCode
Validation:
thanksFlg: z.union([
z.literal(0), // Message not yet shown
z.literal(1), // Message shown
]).optional();Rationale: Historical data (~3.26%) may not have this field; undefined is valid.
Current: v1.0.0 - Initial strict validation
Future Considerations:
- Backward compatibility: If new status codes are added, create
v2schema - Lenient mode: Provide alternate schema for unknown code values
- Migration: Offer migration utilities for old snapshots
Pattern:
// lib/schemas/new-schema.ts
export const newSchema = z.object({
// ...
});
// lib/schemas/index.ts
export { newSchema } from './new-schema.js';Guidelines:
- One schema per file
- Export through
index.ts - Add corresponding type in
lib/types - Document in README
Benchmark (rough estimates):
- Simple field validation: ~0.001ms per field
- Array validation: ~0.01ms per item
- Full NormalizedPrototype: ~0.1ms per object
Impact:
- 1000 prototypes: ~100ms validation time
- Acceptable for snapshot import (one-time cost)
- May impact real-time API parsing
Optimization:
- Use
.parse()only when needed - Cache validation results
- Consider lazy validation for large datasets
Zod dependency: ~15KB minified + gzipped
Mitigation:
- Tree-shakeable: Only import needed schemas
- Separate entry point:
promidas/schemas - Optional: Apps not validating external data can skip this module
Coverage:
- Valid data passes
- Invalid field types rejected
- Out-of-range code values rejected
- Optional fields handled correctly
- Missing required fields rejected
Example:
it('rejects invalid status code', () => {
const invalid = { ...validPrototype, status: 99 };
const result = normalizedPrototypeSchema.safeParse(invalid);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual(['status']);
}
});Verify schema works with:
- Repository snapshot import
- Validation utilities
- Error handling flows
- Custom error messages: Provide user-friendly validation errors
- Partial schemas: Allow validating subsets of fields
- Transform schemas: Auto-convert data types on validation
- Strict mode toggle: Runtime flag for strict vs lenient validation
Commitment:
- Major version bump for breaking schema changes
- Deprecation warnings for 1 minor version
- Migration guides for schema updates