- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
@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
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Error Handling
- Best Practices
- Testing
- 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
import { safeParse } from '@isa/utils/z-safe-parse';
import { z } from 'zod';
2. Define a Zod Schema
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
3. Parse Data Safely
// 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:
// 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:
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:
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<T>(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<T> |
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
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<typeof ProductSchema>;
// 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
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<typeof ApiResponseSchema>;
@Injectable()
export class DataService {
#http = inject(HttpClient);
async fetchData(): Promise<ApiResponse> {
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
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<typeof FormDataSchema>;
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
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<typeof ModernUserSchema>;
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
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<typeof ConfigSchema>;
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
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<typeof ItemSchema>;
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
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<typeof UserProfileSchema>;
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
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
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
function parseWithValidation<T>(
schema: z.ZodSchema<T>,
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:
- Custom logging - Replace
console.warnwith proper logging service - Error tracking - Send validation errors to error tracking (Sentry, etc.)
- Metrics - Track validation failure rates
- Alerts - Alert on high validation failure rates
// 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<T>(
schema: z.ZodSchema<T>,
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
// 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
// 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():
// 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
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
# 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
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:
export function safeParse<T>(schema: z.ZodSchema<T>, 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:
- Single responsibility - Only handles safe parsing with fallback
- Zero configuration - Works out of the box
- Type preservation - Maintains TypeScript types from schema
- Non-throwing - Always returns a value
- 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()
// 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()
// 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.