Schema Package (@schwab/schema)
Overview
The @schwab/schema package provides comprehensive TypeScript schema definitions and runtime validation using Zod for the entire Charles Schwab monorepo. It serves as the single source of truth for all data structures, API responses, UI component props, and transformation schemas, ensuring type safety and runtime validation across all applications.
Architecture
Package Configuration
Module Structure
{
"imports": {
"#schemas/ui/*": "./src/ui/*.ts",
"#schemas/enums/*": "./src/enums/*.ts",
"#schemas/native-enums/*": "./src/native-enums/*.ts",
"#schemas/factories/*": "./src/factories/*.ts",
"#schemas/fetched/*": "./src/fetched/*.ts",
"#schemas/global/data-layer/*": "./src/global/data-layer/*.ts",
"#schemas/global/feature-flag/*": "./src/global/feature-flag/*.ts"
},
"exports": {
"./ui/*": "./src/ui/*.ts",
"./native-enums/*": "./src/native-enums/*.ts",
"./fetched/*": "./src/fetched/*.ts",
"./TResponse": "./src/fetch-types/TResponse.ts"
}
}
Core Schema Categories
1. Response Type Definitions
Standardized API Response Pattern:
// src/fetch-types/TResponse.ts
import { z } from 'zod';
export const RRbusApiErrorSchema = z.object({
status: z.number(),
source: z.string(),
title: z.string(),
detail: z.string(),
});
export const RRbusApiErrorResponseSchema = z.object({
errors: z.array(RRbusApiErrorSchema),
});
const ResponseCreateSchema = z.object({
isOk: z.boolean(),
error: z.string().or(z.number()).or(RRbusApiErrorResponseSchema).nullable(),
cacheTags: z.array(z.string()).nullish(),
});
export type TResponse<T> = z.infer<typeof ResponseCreateSchema> & { data: T };
Usage Pattern:
// Standardized response across all API calls
const response: TResponse<UserData> = await fetchUserData(userId);
if (response.isOk) {
// TypeScript knows response.data is UserData
console.log(response.data.name);
} else {
// Handle error with type safety
console.error(response.error);
}
2. UI Component Schemas
Component Schema Structure:
// src/ui/Person/PersonSchema.ts
import { z } from 'zod';
import { DataLayerComponentSchema } from '#schemas/global/data-layer/DataLayerComponentSchema';
import { FeatureFlagSchema } from '#schemas/global/feature-flag/FeatureFlagSchema';
import { ERole } from '#schemas/native-enums/ERole';
import { MarqueeSchema } from '#schemas/ui/Marquee/MarqueeSchema';
import { LockupSchema } from '#schemas/ui/Lockup/LockupSchema';
export const PersonSchema = z.object({
analytics: FetchedApiMetadataAnalyticsSchema.nullish(),
body: z.string().nullish(),
children: z.custom<React.ReactNode>().optional(),
components: z.array(
z.discriminatedUnion('role', [
LockupSchema.pick({
role: true,
body: true,
heading: true,
headingStyle: true,
margin: true,
}),
MarqueeSchema.omit({ heading: true, subheading: true, headingLevel: true }),
]),
),
complianceCode: z.string().nullable(),
dataLayer: z.object({
[EDataLayer.Component]: DataLayerComponentSchema.optional(),
}).optional(),
role: z.literal(ERole.Person),
});
export type TPerson = z.infer<typeof PersonSchema>;
Component Categories:
| Category | Examples | Description |
|---|---|---|
| Layout Components | Marquee, Container, Deck, Mosaic | Page layout and structure |
| Content Components | RichText, Heading, Image, Video | Content presentation |
| Interactive Components | Button, Cta, FormBuilder, Modal | User interaction elements |
| Navigation Components | Meganav, Nav, Menu, Pagination | Navigation and menu systems |
| Data Components | Table, HighChart, SearchBar | Data visualization and input |
3. Native Enums
Comprehensive Enum Definitions:
// ERole.ts - Component role definitions
export enum ERole {
Person = 'person',
Story = 'story',
Topic = 'topic',
Collection = 'collection',
Marquee = 'marquee',
Button = 'button',
Image = 'image'
}
// EDrupalSource.ts - Content source identification
export enum EDrupalSource {
Education = 'education',
Marketing = 'marketing',
ClientCenter = 'client-center'
}
// EDataLayer.ts - Analytics data layer types
export enum EDataLayer {
Component = 'component',
Page = 'page',
Event = 'event'
}
Available Enum Categories:
| Enum Category | Count | Examples |
|---|---|---|
| UI Enums | 15+ | ERole, EAlignment, EHeadingLevel |
| Data Enums | 10+ | EDrupalSource, EDataLayer, EChannel |
| Interaction Enums | 8+ | EActionType, EEvent, ENavMenu |
| Configuration Enums | 12+ | EPageMode, ECacheTags, EVercelWebhookEvents |
4. Feature Flag Schemas
Feature Flag Type Definitions:
// src/global/feature-flag/FeatureFlagSchema.ts
import { z } from 'zod';
export const FeatureFlagSchema = z.object({
key: z.string(),
enabled: z.boolean(),
value: z.union([z.string(), z.number(), z.boolean(), z.object({})]).optional(),
context: z.object({
userId: z.string().optional(),
environment: z.string().optional(),
segment: z.string().optional(),
}).optional(),
});
export const FeatureFlagCollectionSchema = z.record(z.string(), FeatureFlagSchema);
export type TFeatureFlag = z.infer<typeof FeatureFlagSchema>;
export type TFeatureFlagCollection = z.infer<typeof FeatureFlagCollectionSchema>;
5. Data Layer Schemas
Analytics and Tracking Schemas:
// src/global/data-layer/DataLayerComponentSchema.ts
export const DataLayerComponentSchema = z.object({
componentType: z.string(),
componentName: z.string().optional(),
componentId: z.string().optional(),
componentVersion: z.string().optional(),
content: z.object({
title: z.string().optional(),
category: z.string().optional(),
tags: z.array(z.string()).optional(),
}).optional(),
interaction: z.object({
type: z.string(),
target: z.string().optional(),
value: z.string().optional(),
}).optional(),
});
export type TDataLayerComponent = z.infer<typeof DataLayerComponentSchema>;
Factory Functions
1. Schema Factories
Dynamic Schema Creation:
// src/factories/ui/createComponentSchema.ts
export function createComponentSchema<T extends z.ZodTypeAny>(
role: ERole,
baseSchema: T,
additionalProps?: z.ZodRawShape
) {
return baseSchema.extend({
role: z.literal(role),
...additionalProps,
});
}
// Usage
const CustomButtonSchema = createComponentSchema(
ERole.Button,
z.object({
text: z.string(),
variant: z.enum(['primary', 'secondary']),
}),
{
onClick: z.function().optional(),
}
);
2. Validation Factories
// src/factories/validation/createValidator.ts
export function createValidator<T>(schema: z.ZodSchema<T>) {
return {
validate: (data: unknown): data is T => {
return schema.safeParse(data).success;
},
parse: (data: unknown): T => {
const result = schema.safeParse(data);
if (!result.success) {
throw new Error(`Validation failed: ${result.error.message}`);
}
return result.data;
},
safeparse: (data: unknown) => {
return schema.safeParse(data);
}
};
}
Advanced Schema Patterns
1. Discriminated Unions
// Polymorphic component schemas
export const ComponentSchema = z.discriminatedUnion('role', [
PersonSchema,
StorySchema,
TopicSchema,
CollectionSchema,
MarqueeSchema,
ButtonSchema,
ImageSchema,
]);
export type TComponent = z.infer<typeof ComponentSchema>;
// TypeScript automatically narrows types based on 'role' property
function renderComponent(component: TComponent) {
switch (component.role) {
case ERole.Person:
// TypeScript knows this is TPerson
return <PersonComponent {...component} />;
case ERole.Story:
// TypeScript knows this is TStory
return <StoryComponent {...component} />;
}
}
2. Conditional Schemas
// Dynamic schema based on component type
export const ConditionalComponentSchema = z.object({
role: z.nativeEnum(ERole),
data: z.any(),
}).superRefine((val, ctx) => {
switch (val.role) {
case ERole.Person:
const personResult = PersonSchema.safeParse(val.data);
if (!personResult.success) {
personResult.error.issues.forEach(issue =>
ctx.addIssue({ ...issue, path: ['data', ...issue.path] })
);
}
break;
// Additional cases...
}
});
3. Schema Composition
// Base component schema
const BaseComponentSchema = z.object({
id: z.string().optional(),
className: z.string().optional(),
dataAttributes: z.record(z.string()).optional(),
});
// Extend base schema for specific components
export const InteractiveComponentSchema = BaseComponentSchema.extend({
onClick: z.function().optional(),
disabled: z.boolean().optional(),
ariaLabel: z.string().optional(),
});
// Compose multiple schemas
export const AccessibleInteractiveSchema = InteractiveComponentSchema
.merge(AccessibilitySchema)
.merge(AnalyticsSchema);
Runtime Validation Patterns
1. API Response Validation
import { getDrupalAsset } from '@schwab/fetch/getDrupalAsset';
import { DrupalAssetSchema } from '@schwab/schema/ui/DrupalAsset/DrupalAssetSchema';
export async function getValidatedDrupalAsset(nodeId: string) {
const response = await getDrupalAsset(nodeId);
if (!response.isOk) {
throw new Error(`Failed to fetch asset: ${response.error}`);
}
// Runtime validation with Zod
const validatedData = DrupalAssetSchema.parse(response.data);
return validatedData;
}
2. Form Validation
export const ContactFormSchema = z.object({
firstName: z.string().min(1, 'First name is required'),
lastName: z.string().min(1, 'Last name is required'),
email: z.string().email('Invalid email format'),
phone: z.string().regex(/^\(\d{3}\) \d{3}-\d{4}$/, 'Invalid phone format'),
investmentAmount: z.number().min(1000, 'Minimum investment is $1,000'),
investmentGoals: z.array(z.enum(['retirement', 'education', 'general'])).min(1),
});
export type TContactForm = z.infer<typeof ContactFormSchema>;
// Usage in React Hook Form
const form = useForm<TContactForm>({
resolver: zodResolver(ContactFormSchema),
});
Performance Optimization
1. Lazy Schema Loading
// Conditionally load complex schemas
export const getLazySchema = async (componentType: ERole) => {
switch (componentType) {
case ERole.HighChart:
const { HighChartSchema } = await import('./HighChart/HighChartSchema');
return HighChartSchema;
case ERole.FormBuilder:
const { FormBuilderSchema } = await import('./FormBuilder/FormBuilderSchema');
return FormBuilderSchema;
default:
return BaseComponentSchema;
}
};
2. Schema Caching
const schemaCache = new Map<string, z.ZodSchema>();
export function getCachedSchema(key: string, schemaFactory: () => z.ZodSchema) {
if (!schemaCache.has(key)) {
schemaCache.set(key, schemaFactory());
}
return schemaCache.get(key)!;
}
Testing Integration
1. Schema Testing Utilities
// src/test/schemaTestHelpers.ts
export function createSchemaTest<T>(
schema: z.ZodSchema<T>,
validExamples: unknown[],
invalidExamples: unknown[]
) {
return {
testValid: () => {
validExamples.forEach((example, index) => {
expect(() => schema.parse(example))
.not.toThrow(`Valid example ${index} should parse`);
});
},
testInvalid: () => {
invalidExamples.forEach((example, index) => {
expect(() => schema.parse(example))
.toThrow(`Invalid example ${index} should not parse`);
});
}
};
}
// Usage
const personSchemaTest = createSchemaTest(
PersonSchema,
[validPersonData1, validPersonData2],
[invalidPersonData1, invalidPersonData2]
);
2. Mock Data Schema Validation
import mockPersonData from '@schwab/mock-data/person-all-components.json';
import { PersonSchema } from '@schwab/schema/ui/Person/PersonSchema';
describe('Mock Data Schema Validation', () => {
it('validates person mock data against schema', () => {
expect(() => PersonSchema.parse(mockPersonData)).not.toThrow();
});
});
Error Handling and Debugging
1. Enhanced Error Messages
export function createDetailedZodError(error: z.ZodError, context?: string) {
const errorMessage = error.issues.map(issue => {
const path = issue.path.join('.');
return `${context ? `${context}: ` : ''}Field '${path}' ${issue.message}`;
}).join('\n');
return new Error(`Schema validation failed:\n${errorMessage}`);
}
// Usage
try {
PersonSchema.parse(invalidData);
} catch (error) {
if (error instanceof z.ZodError) {
throw createDetailedZodError(error, 'Person Component');
}
throw error;
}
2. Development-Time Validation
export function createDevValidator<T>(schema: z.ZodSchema<T>, componentName: string) {
return (data: unknown): T => {
if (process.env.NODE_ENV === 'development') {
const result = schema.safeParse(data);
if (!result.success) {
console.error(`${componentName} validation failed:`, result.error);
// Continue in development with warning
return data as T;
}
return result.data;
}
// Production: parse with errors
return schema.parse(data);
};
}
Migration and Versioning
1. Schema Evolution
// Version 1 schema
export const PersonSchemaV1 = z.object({
name: z.string(),
email: z.string().email(),
});
// Version 2 schema (with migration)
export const PersonSchemaV2 = z.object({
firstName: z.string(),
lastName: z.string(),
email: z.string().email(),
phone: z.string().optional(),
});
// Migration function
export function migratePersonV1toV2(v1Data: z.infer<typeof PersonSchemaV1>) {
const [firstName, ...lastNameParts] = v1Data.name.split(' ');
return {
firstName,
lastName: lastNameParts.join(' '),
email: v1Data.email,
};
}
Future Enhancements
Planned Improvements
- Auto-generated Documentation: Schema-driven component documentation
- Runtime Performance: Optimized validation for production environments
- Schema Registry: Centralized schema versioning and management
- GraphQL Integration: Automatic GraphQL schema generation from Zod schemas
- Advanced Validation: Custom validation rules for business logic
Integration Roadmap
- OpenAPI Integration: Generate OpenAPI specs from Zod schemas
- Database Schemas: Sync database schemas with Zod definitions
- Real-time Validation: WebSocket message validation
- Multi-language Support: Schema generation for other languages
The Schema package provides the foundational type system and runtime validation layer that ensures data integrity, type safety, and consistent API contracts across the entire Charles Schwab monorepo. Its comprehensive coverage and Zod-based validation enable confident development with strong guarantees about data structure and validity.