Files
ISA-Frontend/libs/utils/z-safe-parse/README.md
Lorenz Hilpert 2b5da00249 feat(checkout): add reward order confirmation feature with schema migrations
- 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
2025-10-21 14:28:52 +02:00

23 KiB

@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

  • 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:

  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
// 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:

  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()

// 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.