# @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<typeof UserSchema>;
```

### 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<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

```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<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

```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<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

```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<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

```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<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

```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<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

```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<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

```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<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

```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<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

```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<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

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

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