fortify-schema
Version:
A modern TypeScript validation library designed around familiar interface syntax and powerful conditional validation. Experience schema validation that feels natural to TypeScript developers while unlocking advanced runtime validation capabilities.
638 lines (496 loc) • 17.4 kB
Markdown
# Conditional Validation Guide
Complete guide to Fortify Schema's conditional validation - from basic V1 syntax to advanced V2 runtime methods.
## 📚 Table of Contents
- [Introduction](#introduction)
- [V1 Conditional Validation (Legacy)](#v1-conditional-validation-legacy)
- [V2 Conditional Validation (Current)](#v2-conditional-validation-current)
- [V2 Runtime Methods](#v2-runtime-methods)
- [Advanced Patterns](#advanced-patterns)
- [Migration from V1 to V2](#migration-from-v1-to-v2)
- [Best Practices](#best-practices)
## Introduction
Conditional validation allows you to create dynamic validation rules based on the values of other fields or runtime properties. Fortify Schema supports both V1 (legacy) and V2 (current) conditional validation syntax.
### Basic Concept
```typescript
const Schema = Interface({
role: "admin|user|guest",
// Conditional validation: "when condition *? then : else"
permissions: "when role=admin *? string[] : string[]?",
// ^^^^ ^^ ^
// keyword operator separator
});
```
## V1 Conditional Validation (Legacy)
V1 syntax is still supported for backward compatibility but V2 is recommended for new projects. But the same logic can be applied to V2, only the methods call syntax are different.
### Basic V1 Syntax
```typescript
const V1Schema = Interface({
role: "admin|user|guest",
accountType: "free|premium|enterprise",
age: "number(13,120)",
// Basic equality
adminAccess: "when role=admin *? string[] : string[]?",
// Inequality
nonGuestAccess: "when role!=guest *? boolean : boolean?",
// Numeric comparisons
adultContent: "when age>=18 *? boolean : boolean?",
seniorDiscount: "when age>65 *? number(0,50) : number(0,0)",
youthProgram: "when age<25 *? boolean : boolean?",
// Value inclusion
premiumFeatures:
"when accountType.$in(premium,enterprise) *? string[] : string[]?",
});
```
### V1 Operators
| Operator | Description | Example |
| -------- | --------------------- | ------------------------------- |
| `=` | Equals | `when role=admin` |
| `!=` | Not equals | `when role!=guest` |
| `>` | Greater than | `when age>18` |
| `>=` | Greater than or equal | `when age>=21` |
| `<` | Less than | `when age<65` |
| `<=` | Less than or equal | `when age<=25` |
| `.in()` | Value in list | `when role.in(admin,moderator)` |
### V1 Limitations
- Will be deprecated in future versions
- Limited to simple property comparisons
- No runtime property existence checking
- No complex method calls
- Limited string operations
## V2 Conditional Validation (Current)
V2 introduces powerful runtime property checking with the new `property.$method()` syntax.
### Basic V2 Syntax
```typescript
const V2Schema = Interface({
// Runtime data objects
config: "any?",
user: "any?",
features: "any?",
// Basic user data
id: "uuid",
email: "email",
role: "admin|user|guest",
// V2 Runtime Methods - Enhanced property checking
hasPermissions: "when config.permissions.$exists() *? boolean : =false",
hasProfile: "when user.profile.$exists() *? boolean : =false",
isListEmpty: "when config.items.$empty() *? boolean : =true",
hasAdminRole: "when user.roles.$contains(admin) *? boolean : =false",
});
```
### V2 Advantages
- **Runtime property checking** - Check if properties exist at runtime
- **Deep property access** - Navigate nested objects safely
- **String operations** - Advanced string checking methods
- **Numeric operations** - Range and value checking
- **Complex defaults** - Object and array default values
- **Special character support** - Handle properties with special characters
## V2 Runtime Methods
### Property Existence Methods
#### `$exists()` - Check Property Existence
```typescript
const ExistsSchema = Interface({
config: "any?",
user: "any?",
// Basic existence checking
hasConfig: "when config.$exists() *? boolean : =false",
hasUserProfile: "when user.profile.$exists() *? boolean : =false",
// Deep nested checking
hasAdvancedSettings:
"when user.profile.settings.advanced.$exists() *? boolean : =false",
// Special characters
hasSpecialFeature:
'when config["admin-override"].$exists() *? boolean : =false',
// Unicode support
hasUnicodeFeature: "when features.feature_🚀.$exists() *? boolean : =false",
});
```
### Empty/Null Checking Methods
#### `$empty()` - Check if Empty
```typescript
const EmptySchema = Interface({
data: "any?",
// String empty checking
hasContent: "when data.description.$empty() *? =no_content : =has_content",
// Array empty checking
hasItems: "when data.items.$empty() *? =no_items : =has_items",
// Object empty checking
hasMetadata: "when data.metadata.$empty() *? =no_metadata : =has_metadata",
});
```
#### `$null()` - Check if Null
```typescript
const NullSchema = Interface({
data: "any?",
// Null checking
isDataNull: "when data.value.$null() *? =is_null : =not_null",
// Combined with existence
hasValidData:
"when data.value.$exists() && !data.value.$null() *? boolean : =false",
});
```
### String Methods
#### `$contains(value)` - Check String Contains
```typescript
const ContainsSchema = Interface({
data: "any?",
// Basic contains
hasImportantInfo:
"when data.description.$contains(important) *? boolean : =none",
// Multiple contains checks
hasKeywords:
"when data.content.$contains(urgent) || data.content.$contains(priority) *? boolean : =no_keywords",
// Case-sensitive checking
hasExactMatch: "when data.title.$contains(URGENT) *? boolean : =no_match",
});
const data = {
title: "Big title ..... ",
content: "something more beautifull....urgent",
description: `Lorem ipsum dolor sit amet consectetur, adipisicing elit. Harum neque
architecto commodi ipsam excepturi repellat consequatur, provident
asperiores. Excepturi earum vero amet enim consequatur quasi, fugit
cupiditate! Corrupti, incidunt exercitationem. something ..... `,
};
const result = ContainsSchema.safeParse({
data,
hasExactMatch: "no_match",
hasImportantInfo: "none",
hasKeywords: "no_keywords",
});
if (result.success) {
console.log("✅ Expected success:", result.data);
} else {
console.log("❌ Expected errors:", result.errors);
}
```
#### `$startsWith(prefix)` - Check String Prefix
```typescript
const StartsWithSchema = Interface({
data: "any?",
// Prefix checking
isSystemMessage: "when data.message.$startsWith(SYSTEM:) *? boolean : =false",
isErrorCode: "when data.code.$startsWith(ERR_) *? boolean : =false",
// URL checking
isSecureUrl: "when data.url.$startsWith(https://) *? boolean : =false",
});
```
#### `$endsWith(suffix)` - Check String Suffix
```typescript
const EndsWithSchema = Interface({
data: "any?",
// File extension checking
isPdfFile: "when data.filename.$endsWith(.pdf) *? boolean : =no_pdf_file",
isImageFile:
"when data.filename.$endsWith(.jpg) || data.filename.$endsWith(.png) *? boolean : =no_img",
// Domain checking
isCorporateEmail:
"when data.email.$endsWith(@company.com) *? boolean : =no_itsnot_corporated",
});
const data = {
filename: "super_file.ehezezpaozdj.pdf", //is pdf
email: "mail@somethingelse.com", //false so should fail
};
const result = EndsWithSchema.safeParse({
data,
isPdfFile: true,
isCorporateEmail: true,
isImageFile: "no_img",
});
if (result.success) {
console.log("✅ Expected success:", result.data);
} else {
console.log("❌ Expected errors:", result.errors);
}
```
### Numeric Methods
#### `$between(min,max)` - Check Numeric Range
```typescript
const BetweenSchema = Interface({
data: "any?",
// Age range checking
isAdult: "when data.age.$between(18,65) *? boolean : =isnt_adult",
// Score validation
isPassingGrade:
"when data.score.$between(60,100) *? boolean : =itsnt_passgrade",
// Price range
isAffordable: "when data.price.$between(0,100) *? boolean : =yes_itis",
});
const data = {
age: 17,
score: 59,
price: 40,
};
const result = BetweenSchema.safeParse({
data,
isAdult: true, //should fail because of age >= 18
isPassingGrade: true, //should also faild
isAffordable: true,
});
if (result.success) {
console.log("✅ Expected success:", result.data);
} else {
console.log("❌ Expected errors:", result.errors);
}
```
#### `$in(val1,val2,...)` - Check Value Inclusion
```typescript
const InSchema = Interface({
data: "any?",
// Role checking
hasElevatedAccess:
"when data.role.$in(admin,moderator,super_admin) *? boolean : =not_allowed",
// Status checking
isActiveStatus:
"when data.status.$in(active,pending,processing) *? boolean : =not_active",
// Category checking
isPremiumCategory:
"when data.category.$in(premium,enterprise,vip) *? boolean : =itsnot_premium",
});
const data = {
age: 17,
role: "admin",
status: "active",
category: "premium",
};
const result = InSchema.safeParse({
data,
hasElevatedAccess: true,
isActiveStatus: "not_active", //should faild because "status" contains one of the requirements
isPremiumCategory: "itsnot_premium", // should faild also
});
if (result.success) {
console.log("✅ Expected success:", result.data);
} else {
console.log("❌ Expected errors:", result.errors);
}
```
## Advanced Patterns
### Complex Default Values
```typescript
const ComplexDefaultsSchema = Interface({
config: "any?",
// Object defaults
// @fortify-ignore
defaultSettings:
'when config.settings.$exists() *? any : ={"theme":"dark","lang":"en"}',
// Array defaults
// @fortify-ignore
defaultTags: 'when config.tags.$exists() *? string[] : =["default","user"]',
// Nested object defaults
defaultProfile:
'when config.profile.$exists() *? any : ={"name":"Anonymous","avatar":null}',
// Complex conditional defaults
advancedConfig:
'when config.advanced.$exists() *? any : ={"features":[],"permissions":{}}',
});
const config = {
// settings: "yes", //not exist
// advancedConfig: "yes", //not exist
defaultProfile: "yes",
defaultTags: "yes"
};
const result = ComplexDefaultsSchema.safeParse({
config,
defaultSettings: "",
defaultTags: true,
defaultProfile: true,
advancedConfig: ""
});
if (result.success) {
console.log("✅ Expected success:", result.data);
} else {
console.log("❌ Expected errors:", result.errors);
}
```
### Method Combinations
```typescript
const CombinedMethodsSchema = Interface({
user: "any?",
config: "any?",
// Logical AND
isValidUser:
"when user.email.$exists() && user.verified.$exists() *? boolean : =false",
// Logical OR
hasContact:
"when user.email.$exists() || user.phone.$exists() *? boolean : =false",
// Complex combinations
canAccessFeature:
"when user.role.$in(admin,premium) && config.features.$contains(advanced) *? boolean : =false",
// Nested conditions
accessLevel:
"when user.role=admin *? when config.superAdmin.$exists() *? =super : =admin : =user",
});
```
### Deep Property Access
```typescript
const DeepAccessSchema = Interface({
data: "any?",
// Multi-level property access
hasDeepFeature:
"when data.user.profile.settings.advanced.features.$exists() *? boolean : =false",
// Array property access
hasFirstItem: "when data.items[0].$exists() *? boolean : =false",
// Mixed access patterns
hasNestedValue:
"when data.config.nested.deep.value.$exists() *? boolean : =false",
});
```
### Special Characters and Unicode
```typescript
const SpecialCharsSchema = Interface({
config: "any?",
// Properties with hyphens
hasAdminOverride:
'when config["admin-override"].$exists() *? boolean : =not_allowed',
// Properties with spaces
hasSpecialConfig:
'when config["special config"].$exists() *? boolean : =no_sp_config',
});
const config = {
"admin-override": true,
"special config": true
}
const result = SpecialCharsSchema.safeParse({
config,
hasAdminOverride: "not_allowed", // should throw err
hasSpecialConfig: "no_sp_config", // should also throw err
});
if (result.success) {
console.log("✅ Expected success:", result.data);
} else {
console.log("❌ Expected errors:", result.errors);
}
```
## Migration from V1 to V2
### Migration Examples
#### Basic Property Checking
```typescript
const Schema1 = Interface({
role: "admin|user|guest",
permissions: "when role=admin *? string[] : string[]?",
});
const Schema2 = Interface({
role: "admin|user|guest",
config: "any?",
permissions: "when config.hasPermissions.$exists() *? string[] : =[]",
});
```
#### Complex Business Logic
```typescript
const V1BusinessSchema = Interface({
accountType: "free|premium|enterprise",
userLevel: "basic|advanced|expert",
maxProjects: "when accountType=free *? number(1,3) : number(1,100)",
advancedFeatures:
"when userLevel.in(advanced,expert) *? string[] : string[]?",
});
const V2BusinessSchema = Interface({
accountType: "free|premium|enterprise",
userLevel: "basic|advanced|expert",
config: "any?",
features: "any?",
maxProjects:
"when config.account.$in(free,trial) *? number(1,3) : number(1,100)",
advancedFeatures: "when features.advanced.$exists() *? string[] : =[]",
// New capabilities not possible in V1
dynamicLimits: "when config.limits.$exists() *? any : ={}",
customFeatures: "when features.custom.$exists() *? string[] : =[]",
});
```
### Migration Strategy
1. **Identify V1 patterns** in your existing schemas
2. **Add runtime data objects** (`config: "any?"`, `features: "any?"`)
3. **Replace simple comparisons** with runtime method calls
4. **Enhance with new capabilities** available in V2
5. **Test thoroughly** to ensure behavior matches expectations
## Best Practices
### 1. Use Descriptive Property Names
```typescript
// ✅ Good
const Schema = Interface({
config: "any?",
hasPermissions: "when config.permissions.$exists() *? boolean : =false",
canEditContent: "when config.editRights.$exists() *? boolean : =false",
});
// ❌ Avoid
const Schema = Interface({
config: "any?",
p: "when config.permissions.$exists() *? boolean : =false",
e: "when config.editRights.$exists() *? boolean : =false",
});
```
### 2. Provide Meaningful Defaults
```typescript
// ✅ Good - Clear default values
const Schema = Interface({
config: "any?",
userRole: "when config.role.$exists() *? string : =guest",
permissions: "when config.permissions.$exists() *? string[] : =[]",
settings: 'when config.settings.$exists() *? any : ={"theme":"light"}',
});
// ❌ Avoid - Unclear defaults
const Schema = Interface({
config: "any?",
userRole: "when config.role.$exists() *? string : =unknown",
permissions: "when config.permissions.$exists() *? string[] : =null",
});
```
### 3. Use Appropriate Methods
```typescript
// ✅ Good - Use specific methods for specific checks
const Schema = Interface({
data: "any?",
hasContent:
"when data.description.$exists() && !data.description.$empty() *? boolean : =false",
isValidEmail:
"when data.email.$exists() && data.email.$contains(@) *? boolean : =false",
isAdminUser: "when data.role.$in(admin,super_admin) *? boolean : =false",
});
// ❌ Avoid - Generic existence checking when specific methods exist
const Schema = Interface({
data: "any?",
hasContent: "when data.description.$exists() *? boolean : =false", // Doesn't check if empty
isValidEmail: "when data.email.$exists() *? boolean : =false", // Doesn't validate format
isAdminUser: "when data.role.$exists() *? boolean : =false", // Doesn't check specific values
});
```
### 4. Handle Edge Cases
```typescript
const RobustSchema = Interface({
user: "any?",
config: "any?",
// Handle missing nested properties safely
hasValidProfile:
"when user.$exists() && user.profile.$exists() && !user.profile.$empty() *? boolean : =false",
// Provide fallbacks for missing configuration
maxRetries: "when config.retries.$exists() *? number : =3",
timeout: "when config.timeout.$exists() *? number : =5000",
// Handle array edge cases
hasItems:
"when config.items.$exists() && !config.items.$empty() *? boolean : =false",
});
```
### 5. Performance Considerations
```typescript
// ✅ Good - Efficient condition ordering
const Schema = Interface({
config: "any?",
// Check existence first (fastest)
hasFeature:
"when config.$exists() && config.features.$exists() *? boolean : =false",
// Simple checks before complex ones
isEnabled:
"when config.enabled.$exists() && config.features.$contains(advanced) *? boolean : =false",
});
// ❌ Avoid - Inefficient ordering
const Schema = Interface({
config: "any?",
// Complex check before existence check
hasFeature:
"when config.features.$contains(advanced) && config.$exists() *? boolean : =false",
});
```
## 🔗 Related Documentation
- **[Getting Started](./GETTING-STARTED.md)** - Basic Fortify Schema usage
- **[Field Types Reference](./FIELD-TYPES.md)** - Complete type reference
- **[Examples Collection](./EXAMPLES.md)** - Real-world usage patterns
- **[Quick Reference](./QUICK-REFERENCE.md)** - Syntax cheat sheet