# @isa/utils/z-safe-parse A lightweight Zod utility library for safe parsing with automatic fallback to original values on validation failures. ## Overview The Z-Safe-Parse library provides a single utility function `safeParse()` that wraps Zod's `safeParse()` method to provide graceful error handling. Unlike Zod's standard parsing which throws errors on validation failures, this utility automatically falls back to the original input value and logs a warning to the console. This makes it ideal for scenarios where you want to validate data transformations but maintain application stability even when validation fails. ## Table of Contents - [Features](#features) - [Quick Start](#quick-start) - [Core Concepts](#core-concepts) - [API Reference](#api-reference) - [Usage Examples](#usage-examples) - [Error Handling](#error-handling) - [Best Practices](#best-practices) - [Testing](#testing) - [Architecture Notes](#architecture-notes) ## Features - **Safe parsing** - Never throws errors, always returns a value - **Automatic fallback** - Returns original input when validation fails - **Console warnings** - Logs validation errors for debugging - **Type-safe** - Full TypeScript type inference from Zod schemas - **Lightweight** - Single utility function with minimal dependencies - **Zod integration** - Works seamlessly with existing Zod schemas - **Zero configuration** - Drop-in replacement for Zod's parse method ## Quick Start ### 1. Import the Utility ```typescript import { safeParse } from '@isa/utils/z-safe-parse'; import { z } from 'zod'; ``` ### 2. Define a Zod Schema ```typescript const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), }); type User = z.infer; ``` ### 3. Parse Data Safely ```typescript // Valid data - parses successfully const validUser = safeParse(UserSchema, { id: 1, name: 'John Doe', email: 'john@example.com' }); // Result: { id: 1, name: 'John Doe', email: 'john@example.com' } // Invalid data - returns original input and logs warning const invalidUser = safeParse(UserSchema, { id: '1', // Wrong type (should be number) name: 'Jane Doe', email: 'invalid' // Invalid email format }); // Result: { id: '1', name: 'Jane Doe', email: 'invalid' } (original input) // Console: Warning logged with Zod error details ``` ## Core Concepts ### Safe Parsing Philosophy The `safeParse()` function follows a non-throwing error handling philosophy: ```typescript // Traditional Zod parsing (throws on error) try { const data = UserSchema.parse(input); // Use data... } catch (error) { // Handle error... console.error('Validation failed:', error); } // Safe parsing (never throws) const data = safeParse(UserSchema, input); // Always get a value back (validated or original) // Errors automatically logged to console ``` ### Type Inference Full TypeScript type inference is maintained: ```typescript const UserSchema = z.object({ id: z.number(), name: z.string(), }); // TypeScript infers return type as: // { id: number; name: string } const user = safeParse(UserSchema, input); ``` **Important**: Due to fallback behavior, the actual runtime type might not match the inferred type if validation fails. Use this utility only when you can tolerate type mismatches or have additional type guards. ### Console Warning Output When validation fails, a warning is logged with full error details: ```typescript safeParse(UserSchema, { id: 'invalid', name: 123 }); // Console output: // Warning: Failed to parse data // ZodError: [ // { // "code": "invalid_type", // "expected": "number", // "received": "string", // "path": ["id"], // "message": "Expected number, received string" // }, // { // "code": "invalid_type", // "expected": "string", // "received": "number", // "path": ["name"], // "message": "Expected string, received number" // } // ] ``` ### Use Case Scenarios **Ideal for**: - Parsing external API responses where validation failures should not crash the app - Data migrations where you want to log issues but maintain compatibility - Form data transformations with lenient error handling - Development/debugging scenarios where you want to see invalid data **Not ideal for**: - Critical data validation where failures must halt execution - Security-sensitive parsing where invalid data is dangerous - Type-strict contexts where runtime type mismatches cause downstream errors ## API Reference ### `safeParse(schema, data): T` Safely parses data using a Zod schema, returning validated data on success or original data on failure. #### Parameters | Parameter | Type | Description | |-----------|------|-------------| | `schema` | `z.ZodSchema` | Zod schema to validate against | | `data` | `unknown` | Data to parse and validate | #### Returns `T` - Validated data if parsing succeeds, otherwise the original input cast to `T` #### Type Parameters - `T` - The TypeScript type inferred from the Zod schema #### Side Effects - Logs warning to console if parsing fails (includes full Zod error) #### Example ```typescript import { safeParse } from '@isa/utils/z-safe-parse'; import { z } from 'zod'; const ProductSchema = z.object({ id: z.number(), name: z.string(), price: z.number().positive(), inStock: z.boolean() }); type Product = z.infer; // Success case const product: Product = safeParse(ProductSchema, { id: 42, name: 'Widget', price: 19.99, inStock: true }); console.log(product.id); // 42 (type-safe) // Failure case const invalidProduct: Product = safeParse(ProductSchema, { id: '42', // Wrong type name: 'Widget', price: -10, // Negative price inStock: 'yes' // Wrong type }); // Warning logged to console // Returns: { id: '42', name: 'Widget', price: -10, inStock: 'yes' } // Note: TypeScript type is Product, but runtime type doesn't match! ``` ## Usage Examples ### API Response Parsing ```typescript import { safeParse } from '@isa/utils/z-safe-parse'; import { z } from 'zod'; import { HttpClient } from '@angular/common/http'; const ApiResponseSchema = z.object({ data: z.array(z.object({ id: z.number(), title: z.string(), createdAt: z.string().datetime() })), total: z.number(), page: z.number() }); type ApiResponse = z.infer; @Injectable() export class DataService { #http = inject(HttpClient); async fetchData(): Promise { const response = await fetch('/api/data'); const json = await response.json(); // Safe parse - won't throw even if API response is malformed return safeParse(ApiResponseSchema, json); } } ``` ### Form Data Transformation ```typescript import { safeParse } from '@isa/utils/z-safe-parse'; import { z } from 'zod'; const FormDataSchema = z.object({ name: z.string().min(1), age: z.coerce.number().int().positive(), email: z.string().email(), agreeToTerms: z.boolean() }); type FormData = z.infer; function processFormSubmission(rawFormData: unknown): FormData { // Parse form data with automatic type coercion const formData = safeParse(FormDataSchema, rawFormData); // If validation failed, formData contains original (possibly invalid) data // but function continues execution console.log('Processing form:', formData); return formData; } // Example usage const submittedData = { name: 'John', age: '25', // String, will be coerced to number email: 'john@example.com', agreeToTerms: true }; const result = processFormSubmission(submittedData); // Success: { name: 'John', age: 25, email: 'john@example.com', agreeToTerms: true } ``` ### Data Migration with Logging ```typescript import { safeParse } from '@isa/utils/z-safe-parse'; import { z } from 'zod'; // Old data format interface LegacyUser { user_id: string; user_name: string; user_email: string; } // New data format const ModernUserSchema = z.object({ id: z.coerce.number(), name: z.string(), email: z.string().email() }); type ModernUser = z.infer; function migrateLegacyUsers(legacyUsers: LegacyUser[]): ModernUser[] { return legacyUsers.map(legacy => { // Transform to new format const transformed = { id: legacy.user_id, name: legacy.user_name, email: legacy.user_email }; // Safe parse - logs warnings for invalid data but continues migration return safeParse(ModernUserSchema, transformed); }); } // Example usage const legacyData: LegacyUser[] = [ { user_id: '1', user_name: 'Alice', user_email: 'alice@example.com' }, { user_id: 'invalid', user_name: 'Bob', user_email: 'not-an-email' } // Invalid! ]; const migratedUsers = migrateLegacyUsers(legacyData); // Migrates all users, logs warnings for invalid entries // Result: [ // { id: 1, name: 'Alice', email: 'alice@example.com' }, // Valid // { id: 'invalid', name: 'Bob', email: 'not-an-email' } // Invalid, logged // ] ``` ### Configuration File Parsing ```typescript import { safeParse } from '@isa/utils/z-safe-parse'; import { z } from 'zod'; const ConfigSchema = z.object({ apiUrl: z.string().url(), timeout: z.number().int().positive().default(5000), retries: z.number().int().nonnegative().default(3), debug: z.boolean().default(false) }); type Config = z.infer; function loadConfig(configFile: unknown): Config { // Parse config with defaults and validation const config = safeParse(ConfigSchema, configFile); // Even if validation fails, app continues with original values // (might have missing required fields!) return config; } // Example usage const userConfig = { apiUrl: 'https://api.example.com', timeout: '10000', // String, will be coerced to number // retries: omitted, will use default debug: true }; const config = loadConfig(userConfig); console.log(config.timeout); // 10000 (coerced from string) console.log(config.retries); // 3 (default value) ``` ### Array Validation with Filtering ```typescript import { safeParse } from '@isa/utils/z-safe-parse'; import { z } from 'zod'; const ItemSchema = z.object({ id: z.number(), name: z.string(), price: z.number().positive() }); const ItemsArraySchema = z.array(ItemSchema); type Item = z.infer; function parseItemsArray(data: unknown): Item[] { // Parse array - if validation fails, returns original array const items = safeParse(ItemsArraySchema, data); // Additional runtime validation (since safeParse might return invalid data) return items.filter((item: any) => { return typeof item.id === 'number' && typeof item.name === 'string' && typeof item.price === 'number' && item.price > 0; }); } // Example usage const mixedData = [ { id: 1, name: 'Item 1', price: 10.00 }, // Valid { id: '2', name: 'Item 2', price: 20.00 }, // Invalid id type { id: 3, name: 'Item 3', price: -5 }, // Invalid price { id: 4, name: 'Item 4', price: 15.00 } // Valid ]; const validItems = parseItemsArray(mixedData); // Result: Only items 1 and 4 (valid items after filtering) // Console: Warning logged for validation failure ``` ### Optional Field Handling ```typescript import { safeParse } from '@isa/utils/z-safe-parse'; import { z } from 'zod'; const UserProfileSchema = z.object({ id: z.number(), name: z.string(), bio: z.string().optional(), website: z.string().url().optional(), age: z.number().int().positive().optional() }); type UserProfile = z.infer; function parseUserProfile(data: unknown): UserProfile { return safeParse(UserProfileSchema, data); } // Example usage const profile1 = parseUserProfile({ id: 1, name: 'John', bio: 'Software developer', // website omitted - valid (optional) age: 30 }); // Success: All optional fields handled correctly const profile2 = parseUserProfile({ id: '2', // Wrong type name: 'Jane', website: 'not-a-url' // Invalid URL }); // Warning logged, returns original data ``` ## Error Handling ### Console Warning Format When validation fails, `safeParse()` logs a warning with this format: ``` console.warn('Failed to parse data', zodError); ``` The `zodError` object contains: - **issues**: Array of validation errors - **path**: Path to the failing field - **message**: Human-readable error message - **code**: Error code (e.g., 'invalid_type', 'too_small') ### Detecting Parse Failures Since `safeParse()` always returns a value, you cannot rely on try/catch. To detect failures: #### Option 1: Use Zod's safeParse directly ```typescript const result = UserSchema.safeParse(data); if (!result.success) { // Handle validation failure console.error('Validation failed:', result.error); // Use original data or default value return data as User; } else { // Use validated data return result.data; } ``` #### Option 2: Add runtime type guards ```typescript const user = safeParse(UserSchema, data); // Runtime check if (typeof user.id !== 'number') { // Validation likely failed console.error('Invalid user data, using defaults'); return getDefaultUser(); } return user; ``` #### Option 3: Combine with validation flag ```typescript function parseWithValidation( schema: z.ZodSchema, data: unknown ): { data: T; isValid: boolean } { const result = schema.safeParse(data); if (result.success) { return { data: result.data, isValid: true }; } else { console.warn('Failed to parse data', result.error); return { data: data as T, isValid: false }; } } // Usage const { data, isValid } = parseWithValidation(UserSchema, input); if (!isValid) { // Handle invalid data return getDefaultUser(); } return data; ``` ### Production Considerations For production environments, consider: 1. **Custom logging** - Replace `console.warn` with proper logging service 2. **Error tracking** - Send validation errors to error tracking (Sentry, etc.) 3. **Metrics** - Track validation failure rates 4. **Alerts** - Alert on high validation failure rates ```typescript // Production-ready wrapper import { safeParse as baseSafeParse } from '@isa/utils/z-safe-parse'; import { z } from 'zod'; import { LoggingService } from '@isa/core/logging'; export function safeParse( schema: z.ZodSchema, data: unknown, context?: string ): T { const result = schema.safeParse(data); if (!result.success) { // Log to proper logging service inject(LoggingService).warn('Validation failed', { context, error: result.error, data }); // Track metrics trackValidationFailure(schema, context); } return result.success ? result.data : (data as T); } ``` ## Best Practices ### When to Use safeParse() **Use safeParse() when**: - Parsing external data that might be malformed - Development/debugging to see invalid data - Data migrations where errors should be logged but not halt execution - Lenient form validation where UX requires showing original input **Don't use safeParse() when**: - Validation failure should stop execution - Type safety is critical for downstream code - Security-sensitive data parsing - You need to differentiate between valid and invalid data programmatically ### Type Safety Considerations ```typescript // Risky - runtime type might not match TypeScript type const user: User = safeParse(UserSchema, input); user.id.toFixed(2); // Might fail if id is actually a string! // Safer - add runtime guards const user = safeParse(UserSchema, input); if (typeof user.id === 'number') { user.id.toFixed(2); // Safe } // Safest - use Zod's safeParse directly for critical paths const result = UserSchema.safeParse(input); if (result.success) { result.data.id.toFixed(2); // Type-safe } ``` ### Logging Best Practices ```typescript // Bad - Silent failures const data = safeParse(schema, input); // No way to know if parsing failed! // Good - Check console for warnings during development const data = safeParse(schema, input); // Warnings appear in console during development // Better - Use proper logging in production const result = schema.safeParse(input); if (!result.success) { logger.warn('Validation failed', { error: result.error, input }); } return result.success ? result.data : (input as T); ``` ### Schema Design Design schemas to be lenient when using safeParse(): ```typescript // Too strict - many false negatives const StrictSchema = z.object({ id: z.number(), name: z.string().min(1).max(50), email: z.string().email(), age: z.number().int().min(18).max(120) }); // More lenient - graceful degradation const LenientSchema = z.object({ id: z.coerce.number(), // Coerce from string name: z.string().default(''), // Default for missing values email: z.string().optional(), // Optional instead of required age: z.coerce.number().optional() // Coerce + optional }); ``` ### Combining with Other Validation ```typescript import { safeParse } from '@isa/utils/z-safe-parse'; // Zod for structure validation const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string() }); // Custom business logic validation function validateUserBusinessRules(user: User): boolean { // Check business rules that Zod can't express return user.name !== 'admin' && user.email.includes('@'); } // Combined validation function parseAndValidateUser(data: unknown): User | null { const user = safeParse(UserSchema, data); if (!validateUserBusinessRules(user)) { console.error('Business validation failed'); return null; } return user; } ``` ## Testing The library uses **Jest** for testing. ### Running Tests ```bash # Run tests for this library npx nx test utils-z-safe-parse --skip-nx-cache # Run tests with coverage npx nx test utils-z-safe-parse --code-coverage --skip-nx-cache # Run tests in watch mode npx nx test utils-z-safe-parse --watch ``` ### Test Examples ```typescript import { describe, it, expect, vi } from '@jest/globals'; import { safeParse } from './utils-z-safe-parse'; import { z } from 'zod'; describe('safeParse', () => { const TestSchema = z.object({ id: z.number(), name: z.string() }); it('should return parsed data for valid input', () => { const input = { id: 1, name: 'Test' }; const result = safeParse(TestSchema, input); expect(result).toEqual(input); }); it('should return original data for invalid input', () => { const input = { id: 'invalid', name: 123 }; const result = safeParse(TestSchema, input); expect(result).toEqual(input); }); it('should log warning for invalid input', () => { const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(); const input = { id: 'invalid', name: 'Test' }; safeParse(TestSchema, input); expect(consoleWarnSpy).toHaveBeenCalledWith( 'Failed to parse data', expect.any(Object) ); consoleWarnSpy.mockRestore(); }); it('should handle complex nested schemas', () => { const NestedSchema = z.object({ user: z.object({ id: z.number(), profile: z.object({ name: z.string(), age: z.number() }) }) }); const validInput = { user: { id: 1, profile: { name: 'John', age: 30 } } }; const result = safeParse(NestedSchema, validInput); expect(result).toEqual(validInput); }); it('should preserve type coercion', () => { const CoerceSchema = z.object({ id: z.coerce.number(), active: z.coerce.boolean() }); const input = { id: '42', active: 'true' }; const result = safeParse(CoerceSchema, input); expect(result.id).toBe(42); expect(result.active).toBe(true); }); }); ``` ## Architecture Notes ### Implementation Details The `safeParse()` function is a thin wrapper around Zod's `safeParse()` method: ```typescript export function safeParse(schema: z.ZodSchema, data: unknown): T { const parsed = schema.safeParse(data); if (!parsed.success) { console.warn('Failed to parse data', parsed.error); return data as T; } return parsed.data; } ``` **Key characteristics**: 1. **Single responsibility** - Only handles safe parsing with fallback 2. **Zero configuration** - Works out of the box 3. **Type preservation** - Maintains TypeScript types from schema 4. **Non-throwing** - Always returns a value 5. **Logging only** - Side effect limited to console.warn ### Design Decisions #### 1. Fallback to Original Value **Decision**: Return original input when validation fails **Rationale**: - Maintains application stability - Allows graceful degradation - Useful for development/debugging **Tradeoff**: Type safety is compromised (TypeScript type might not match runtime type) #### 2. Console Warning **Decision**: Log to console.warn instead of console.error **Rationale**: - Warning severity (not critical error) - Easy to find in browser dev tools - Doesn't trigger error tracking by default **Tradeoff**: May be missed in production without proper monitoring #### 3. No Success Flag **Decision**: Don't return a success boolean with the result **Rationale**: - Simpler API (just returns T) - Encourages graceful handling - Keeps wrapper minimal **Tradeoff**: Can't programmatically detect failures without runtime checks ### Comparison with Alternatives #### vs. Zod's parse() ```typescript // Zod's parse() - throws on error try { const data = UserSchema.parse(input); } catch (error) { // Handle error } // safeParse() - never throws const data = safeParse(UserSchema, input); // Warning logged if invalid ``` #### vs. Zod's safeParse() ```typescript // Zod's safeParse() - returns result object const result = UserSchema.safeParse(input); if (result.success) { // Use result.data } else { // Handle result.error } // safeParse() - always returns data const data = safeParse(UserSchema, input); // Original data if validation fails ``` ### Known Limitations #### 1. Type Safety Compromise (High Impact) **Limitation**: Runtime type might not match TypeScript type **Impact**: High - can cause runtime errors in downstream code **Mitigation**: - Add runtime type guards for critical paths - Use Zod's safeParse() for security-sensitive data - Document when validation might fail #### 2. No Programmatic Error Detection (Medium Impact) **Limitation**: Can't distinguish valid from invalid data programmatically **Impact**: Medium - requires runtime checks or separate validation **Mitigation**: - Check console for warnings during development - Add custom wrapper that returns success flag - Use Zod's safeParse() when you need to detect failures #### 3. Console-Only Logging (Medium Impact) **Limitation**: Errors only logged to console **Impact**: Medium - no error tracking, metrics, or alerts by default **Mitigation**: - Wrap safeParse() with custom logging in production - Integrate with error tracking services - Add metrics tracking for validation failures ## Dependencies ### Required Libraries - `zod` - Runtime schema validation ### Path Alias Import from: `@isa/utils/z-safe-parse` ### Project Configuration - **Project Name**: `utils-z-safe-parse` - **Prefix**: `util` - **Testing**: Jest - **Source Root**: `libs/utils/z-safe-parse/src` ## License Internal ISA Frontend library - not for external distribution.