Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Field-Level Validation

This guide covers granular validation at the field and attribute level, allowing you to implement custom validation logic for specific user attributes, custom extensions, and complex data types.

Custom Attribute Validators

The field-level validation system allows you to register validators for specific attributes:

#![allow(unused)]
fn main() {
use scim_server::validation::{FieldValidator, ValidationContext, ValidationError};
use std::collections::HashMap;
use regex::Regex;
use async_trait::async_trait;

pub struct CustomAttributeValidator {
    validators: HashMap<String, Box<dyn FieldValidator + Send + Sync>>,
}

#[async_trait]
pub trait FieldValidator {
    async fn validate(
        &self,
        field_name: &str,
        value: &serde_json::Value,
        context: &ValidationContext,
    ) -> Result<(), ValidationError>;
}

impl CustomAttributeValidator {
    pub fn new() -> Self {
        let mut validators = HashMap::new();
        
        // Phone number validator
        validators.insert(
            "phoneNumbers".to_string(),
            Box::new(PhoneNumberValidator::new()) as Box<dyn FieldValidator + Send + Sync>
        );
        
        // Social Security Number validator
        validators.insert(
            "enterpriseUser:ssn".to_string(),
            Box::new(SsnValidator::new()) as Box<dyn FieldValidator + Send + Sync>
        );
        
        // Employee ID validator
        validators.insert(
            "enterpriseUser:employeeNumber".to_string(),
            Box::new(EmployeeIdValidator::new()) as Box<dyn FieldValidator + Send + Sync>
        );
        
        // Custom business identifier validator
        validators.insert(
            "enterpriseUser:businessId".to_string(),
            Box::new(BusinessIdentifierValidator::new()) as Box<dyn FieldValidator + Send + Sync>
        );
        
        Self { validators }
    }
    
    pub async fn validate_field(
        &self,
        field_name: &str,
        value: &serde_json::Value,
        context: &ValidationContext,
    ) -> Result<(), ValidationError> {
        if let Some(validator) = self.validators.get(field_name) {
            validator.validate(field_name, value, context).await?;
        }
        
        Ok(())
    }
}
}

Phone Number Validation

Comprehensive phone number validation with international format support:

#![allow(unused)]
fn main() {
pub struct PhoneNumberValidator {
    allowed_countries: Vec<String>,
    phone_regex: Regex,
    external_validation_enabled: bool,
}

impl PhoneNumberValidator {
    pub fn new() -> Self {
        Self {
            allowed_countries: vec![
                "US".to_string(), 
                "CA".to_string(), 
                "GB".to_string(),
                "DE".to_string(),
                "FR".to_string(),
            ],
            phone_regex: Regex::new(r"^\+[1-9]\d{1,14}$").unwrap(),
            external_validation_enabled: true,
        }
    }
    
    pub fn with_allowed_countries(mut self, countries: Vec<String>) -> Self {
        self.allowed_countries = countries;
        self
    }
    
    pub fn disable_external_validation(mut self) -> Self {
        self.external_validation_enabled = false;
        self
    }
}

#[async_trait]
impl FieldValidator for PhoneNumberValidator {
    async fn validate(
        &self,
        field_name: &str,
        value: &serde_json::Value,
        context: &ValidationContext,
    ) -> Result<(), ValidationError> {
        if let Some(phone_numbers) = value.as_array() {
            for (index, phone_obj) in phone_numbers.iter().enumerate() {
                if let Some(phone_value) = phone_obj.get("value").and_then(|v| v.as_str()) {
                    self.validate_single_phone(phone_value, field_name, index).await?;
                }
            }
        } else if let Some(phone_value) = value.as_str() {
            // Handle direct string value
            self.validate_single_phone(phone_value, field_name, 0).await?;
        }
        
        Ok(())
    }
}

impl PhoneNumberValidator {
    async fn validate_single_phone(
        &self,
        phone_value: &str,
        field_name: &str,
        index: usize,
    ) -> Result<(), ValidationError> {
        let field_path = if field_name.contains("phoneNumbers") {
            format!("{}[{}].value", field_name, index)
        } else {
            field_name.to_string()
        };
        
        // Basic format validation
        if !self.phone_regex.is_match(phone_value) {
            return Err(ValidationError::new(
                "INVALID_PHONE_FORMAT",
                "Phone number must be in international format (+1234567890)",
            ).with_field(&field_path));
        }
        
        // Extract and validate country code
        let country_code = self.extract_country_code(phone_value)?;
        if !self.is_allowed_country_code(&country_code) {
            return Err(ValidationError::new(
                "INVALID_COUNTRY_CODE",
                &format!("Phone number country code '{}' is not allowed", country_code),
            ).with_field(&field_path));
        }
        
        // Validate phone number length for specific countries
        self.validate_country_specific_length(phone_value, &country_code, &field_path)?;
        
        // External validation if enabled
        if self.external_validation_enabled {
            self.validate_with_external_service(phone_value, &field_path).await?;
        }
        
        Ok(())
    }
    
    fn extract_country_code(&self, phone_value: &str) -> Result<String, ValidationError> {
        if phone_value.len() < 2 {
            return Err(ValidationError::new(
                "INVALID_PHONE_LENGTH",
                "Phone number too short",
            ));
        }
        
        // Common country codes
        for length in [1, 2, 3] {
            if phone_value.len() > length {
                let potential_code = &phone_value[1..=length];
                if self.is_valid_country_code(potential_code) {
                    return Ok(potential_code.to_string());
                }
            }
        }
        
        Err(ValidationError::new(
            "UNKNOWN_COUNTRY_CODE",
            "Unable to determine country code",
        ))
    }
    
    fn is_valid_country_code(&self, code: &str) -> bool {
        // Common country codes
        let valid_codes = [
            "1", "7", "20", "27", "30", "31", "32", "33", "34", "36", "39", "40", "41", 
            "43", "44", "45", "46", "47", "48", "49", "51", "52", "53", "54", "55", "56", 
            "57", "58", "60", "61", "62", "63", "64", "65", "66", "81", "82", "84", "86", 
            "90", "91", "92", "93", "94", "95", "98", "212", "213", "216", "218", "220", 
            "221", "222", "223", "224", "225", "226", "227", "228", "229", "230", "231", 
            "232", "233", "234", "235", "236", "237", "238", "239", "240", "241", "242", 
            "243", "244", "245", "246", "247", "248", "249", "250", "251", "252", "253", 
            "254", "255", "256", "257", "258", "260", "261", "262", "263", "264", "265", 
            "266", "267", "268", "269", "290", "291", "297", "298", "299", "350", "351", 
            "352", "353", "354", "355", "356", "357", "358", "359", "370", "371", "372", 
            "373", "374", "375", "376", "377", "378", "380", "381", "382", "383", "385", 
            "386", "387", "389", "420", "421", "423", "500", "501", "502", "503", "504", 
            "505", "506", "507", "508", "509", "590", "591", "592", "593", "594", "595", 
            "596", "597", "598", "599", "670", "672", "673", "674", "675", "676", "677", 
            "678", "679", "680", "681", "682", "683", "684", "685", "686", "687", "688", 
            "689", "690", "691", "692", "850", "852", "853", "855", "856", "880", "886", 
            "960", "961", "962", "963", "964", "965", "966", "967", "968", "970", "971", 
            "972", "973", "974", "975", "976", "977", "992", "993", "994", "995", "996", 
            "998"
        ];
        
        valid_codes.contains(&code)
    }
    
    fn is_allowed_country_code(&self, country_code: &str) -> bool {
        match country_code {
            "1" => self.allowed_countries.contains(&"US".to_string()) || 
                   self.allowed_countries.contains(&"CA".to_string()),
            "44" => self.allowed_countries.contains(&"GB".to_string()),
            "49" => self.allowed_countries.contains(&"DE".to_string()),
            "33" => self.allowed_countries.contains(&"FR".to_string()),
            _ => true, // Allow other countries by default
        }
    }
    
    fn validate_country_specific_length(
        &self,
        phone_value: &str,
        country_code: &str,
        field_path: &str,
    ) -> Result<(), ValidationError> {
        let expected_lengths = match country_code {
            "1" => vec![11], // US/Canada: +1 + 10 digits
            "44" => vec![13], // UK: +44 + 10-11 digits
            "49" => vec![12, 13], // Germany: +49 + 10-11 digits
            "33" => vec![12], // France: +33 + 9 digits
            _ => return Ok(()), // Skip validation for other countries
        };
        
        if !expected_lengths.contains(&phone_value.len()) {
            return Err(ValidationError::new(
                "INVALID_PHONE_LENGTH",
                &format!("Invalid phone number length for country code {}", country_code),
            ).with_field(field_path));
        }
        
        Ok(())
    }
    
    async fn validate_with_external_service(
        &self,
        phone_number: &str,
        field_path: &str,
    ) -> Result<(), ValidationError> {
        let client = reqwest::Client::new();
        let response = client
            .get(&format!("https://api.phonevalidation.com/validate/{}", phone_number))
            .timeout(std::time::Duration::from_secs(3))
            .send()
            .await
            .map_err(|_| ValidationError::new(
                "PHONE_VALIDATION_SERVICE_ERROR",
                "Unable to validate phone number with external service",
            ).with_field(field_path))?;
        
        if !response.status().is_success() {
            return Err(ValidationError::new(
                "INVALID_PHONE_NUMBER",
                "Phone number validation failed",
            ).with_field(field_path));
        }
        
        // Parse validation response
        let validation_result: PhoneValidationResult = response.json().await
            .map_err(|_| ValidationError::new(
                "PHONE_VALIDATION_PARSE_ERROR",
                "Failed to parse phone validation response",
            ).with_field(field_path))?;
        
        if !validation_result.is_valid {
            return Err(ValidationError::new(
                "INVALID_PHONE_NUMBER",
                &validation_result.reason.unwrap_or_else(|| "Phone number is invalid".to_string()),
            ).with_field(field_path));
        }
        
        Ok(())
    }
}

#[derive(serde::Deserialize)]
struct PhoneValidationResult {
    is_valid: bool,
    reason: Option<String>,
    carrier: Option<String>,
    line_type: Option<String>,
}
}

Social Security Number Validation

Comprehensive SSN validation with format and uniqueness checks:

#![allow(unused)]
fn main() {
pub struct SsnValidator {
    allow_itin: bool,
    check_uniqueness: bool,
}

impl SsnValidator {
    pub fn new() -> Self {
        Self {
            allow_itin: false,
            check_uniqueness: true,
        }
    }
    
    pub fn allow_itin(mut self, allow: bool) -> Self {
        self.allow_itin = allow;
        self
    }
    
    pub fn check_uniqueness(mut self, check: bool) -> Self {
        self.check_uniqueness = check;
        self
    }
    
    fn validate_ssn_format(&self, ssn: &str) -> Result<(), ValidationError> {
        // Remove hyphens and spaces
        let cleaned = ssn.replace(['-', ' '], "");
        
        // Must be exactly 9 digits
        if cleaned.len() != 9 || !cleaned.chars().all(|c| c.is_ascii_digit()) {
            return Err(ValidationError::new(
                "INVALID_SSN_FORMAT",
                "SSN must be 9 digits (XXX-XX-XXXX format)",
            ));
        }
        
        // Extract area, group, and serial numbers
        let area = &cleaned[0..3];
        let group = &cleaned[3..5];
        let serial = &cleaned[5..9];
        
        // Validate area number
        self.validate_area_number(area)?;
        
        // Validate group number
        self.validate_group_number(group)?;
        
        // Validate serial number
        self.validate_serial_number(serial)?;
        
        // Check for invalid patterns
        self.validate_patterns(&cleaned)?;
        
        Ok(())
    }
    
    fn validate_area_number(&self, area: &str) -> Result<(), ValidationError> {
        let area_num: u32 = area.parse().unwrap_or(0);
        
        // Invalid area numbers
        if area_num == 0 || area_num == 666 || area_num >= 900 {
            return Err(ValidationError::new(
                "INVALID_SSN_AREA",
                "Invalid SSN area number",
            ));
        }
        
        // Check if it's an ITIN (Individual Taxpayer Identification Number)
        if area_num >= 900 && area_num <= 999 {
            if !self.allow_itin {
                return Err(ValidationError::new(
                    "ITIN_NOT_ALLOWED",
                    "Individual Taxpayer Identification Numbers (ITIN) are not allowed",
                ));
            }
        }
        
        Ok(())
    }
    
    fn validate_group_number(&self, group: &str) -> Result<(), ValidationError> {
        let group_num: u32 = group.parse().unwrap_or(0);
        
        if group_num == 0 {
            return Err(ValidationError::new(
                "INVALID_SSN_GROUP",
                "Invalid SSN group number (cannot be 00)",
            ));
        }
        
        Ok(())
    }
    
    fn validate_serial_number(&self, serial: &str) -> Result<(), ValidationError> {
        let serial_num: u32 = serial.parse().unwrap_or(0);
        
        if serial_num == 0 {
            return Err(ValidationError::new(
                "INVALID_SSN_SERIAL",
                "Invalid SSN serial number (cannot be 0000)",
            ));
        }
        
        Ok(())
    }
    
    fn validate_patterns(&self, ssn: &str) -> Result<(), ValidationError> {
        // Invalid SSN patterns
        let invalid_patterns = [
            "000000000", "111111111", "222222222", "333333333",
            "444444444", "555555555", "666666666", "777777777",
            "888888888", "999999999", "123456789", "987654321"
        ];
        
        if invalid_patterns.contains(&ssn) {
            return Err(ValidationError::new(
                "INVALID_SSN_PATTERN",
                "SSN contains an invalid pattern",
            ));
        }
        
        // Check for consecutive digits
        if self.has_consecutive_pattern(ssn) {
            return Err(ValidationError::new(
                "INVALID_SSN_PATTERN",
                "SSN cannot contain repetitive patterns",
            ));
        }
        
        Ok(())
    }
    
    fn has_consecutive_pattern(&self, ssn: &str) -> bool {
        let chars: Vec<char> = ssn.chars().collect();
        
        // Check for all same digits
        if chars.iter().all(|&c| c == chars[0]) {
            return true;
        }
        
        // Check for ascending/descending sequences
        for window in chars.windows(3) {
            if window[0] as u8 + 1 == window[1] as u8 && window[1] as u8 + 1 == window[2] as u8 {
                return true; // Ascending sequence
            }
            if window[0] as u8 == window[1] as u8 + 1 && window[1] as u8 == window[2] as u8 + 1 {
                return true; // Descending sequence
            }
        }
        
        false
    }
}

#[async_trait]
impl FieldValidator for SsnValidator {
    async fn validate(
        &self,
        field_name: &str,
        value: &serde_json::Value,
        context: &ValidationContext,
    ) -> Result<(), ValidationError> {
        if let Some(ssn) = value.as_str() {
            // Validate format
            self.validate_ssn_format(ssn)
                .map_err(|mut e| {
                    e.field_path = Some(field_name.to_string());
                    e
                })?;
            
            // Check uniqueness if enabled
            if self.check_uniqueness {
                // Note: This would require access to the storage provider
                // In practice, you'd inject the storage provider into the validator
                if let Some(storage) = context.storage.as_ref() {
                    if storage.ssn_exists(&context.tenant_id, ssn).await.unwrap_or(false) {
                        return Err(ValidationError::new(
                            "SSN_ALREADY_EXISTS",
                            "Social Security Number is already in use",
                        ).with_field(field_name));
                    }
                }
            }
        }
        
        Ok(())
    }
}
}

Employee ID Validation

Custom business identifier validation:

#![allow(unused)]
fn main() {
pub struct EmployeeIdValidator {
    format_regex: Regex,
    department_prefixes: HashMap<String, String>,
    check_uniqueness: bool,
}

impl EmployeeIdValidator {
    pub fn new() -> Self {
        let mut department_prefixes = HashMap::new();
        department_prefixes.insert("Engineering".to_string(), "ENG".to_string());
        department_prefixes.insert("Sales".to_string(), "SAL".to_string());
        department_prefixes.insert("Marketing".to_string(), "MKT".to_string());
        department_prefixes.insert("HR".to_string(), "HRS".to_string());
        department_prefixes.insert("Finance".to_string(), "FIN".to_string());
        
        Self {
            format_regex: Regex::new(r"^[A-Z]{3}\d{5}$").unwrap(),
            department_prefixes,
            check_uniqueness: true,
        }
    }
    
    pub fn with_custom_format(mut self, regex: &str) -> Result<Self, regex::Error> {
        self.format_regex = Regex::new(regex)?;
        Ok(self)
    }
    
    fn validate_format(&self, employee_id: &str) -> Result<(), ValidationError> {
        if !self.format_regex.is_match(employee_id) {
            return Err(ValidationError::new(
                "INVALID_EMPLOYEE_ID_FORMAT",
                "Employee ID must follow format: 3 letters + 5 digits (e.g., ENG12345)",
            ));
        }
        
        Ok(())
    }
    
    fn validate_department_prefix(
        &self,
        employee_id: &str,
        user_department: Option<&str>,
    ) -> Result<(), ValidationError> {
        if let Some(department) = user_department {
            if let Some(expected_prefix) = self.department_prefixes.get(department) {
                let prefix = &employee_id[0..3];
                if prefix != expected_prefix {
                    return Err(ValidationError::new(
                        "EMPLOYEE_ID_DEPARTMENT_MISMATCH",
                        &format!(
                            "Employee ID prefix '{}' does not match department '{}' (expected '{}')",
                            prefix, department, expected_prefix
                        ),
                    ));
                }
            }
        }
        
        Ok(())
    }
    
    fn validate_sequence_number(&self, employee_id: &str) -> Result<(), ValidationError> {
        let sequence = &employee_id[3..8];
        let sequence_num: u32 = sequence.parse().unwrap_or(0);
        
        // Sequence number cannot be 00000
        if sequence_num == 0 {
            return Err(ValidationError::new(
                "INVALID_EMPLOYEE_ID_SEQUENCE",
                "Employee ID sequence number cannot be 00000",
            ));
        }
        
        // Validate reasonable range (e.g., 00001-99999)
        if sequence_num > 99999 {
            return Err(ValidationError::new(
                "INVALID_EMPLOYEE_ID_SEQUENCE",
                "Employee ID sequence number must be between 00001 and 99999",
            ));
        }
        
        Ok(())
    }
}

#[async_trait]
impl FieldValidator for EmployeeIdValidator {
    async fn validate(
        &self,
        field_name: &str,
        value: &serde_json::Value,
        context: &ValidationContext,
    ) -> Result<(), ValidationError> {
        if let Some(employee_id) = value.as_str() {
            // Validate format
            self.validate_format(employee_id)
                .map_err(|mut e| {
                    e.field_path = Some(field_name.to_string());
                    e
                })?;
            
            // Validate sequence number
            self.validate_sequence_number(employee_id)
                .map_err(|mut e| {
                    e.field_path = Some(field_name.to_string());
                    e
                })?;
            
            // Validate department prefix if user has department info
            if let Some(user_data) = context.additional_data.get("user") {
                if let Some(department) = user_data.get("department").and_then(|v| v.as_str()) {
                    self.validate_department_prefix(employee_id, Some(department))
                        .map_err(|mut e| {
                            e.field_path = Some(field_name.to_string());
                            e
                        })?;
                }
            }
            
            // Check uniqueness
            if self.check_uniqueness {
                if let Some(storage) = context.storage.as_ref() {
                    if storage.employee_id_exists(&context.tenant_id, employee_id).await.unwrap_or(false) {
                        return Err(ValidationError::new(
                            "EMPLOYEE_ID_ALREADY_EXISTS",
                            "Employee ID is already in use",
                        ).with_field(field_name));
                    }
                }
            }
        }
        
        Ok(())
    }
}
}

Credit Card Validation (for Financial Applications)

If your application handles financial data:

#![allow(unused)]
fn main() {
pub struct CreditCardValidator {
    allowed_types: Vec<CreditCardType>,
    validate_luhn: bool,
}

#[derive(Debug, Clone, PartialEq)]
pub enum CreditCardType {
    Visa,
    MasterCard,
    AmericanExpress,
    Discover,
    DinersClub,
    JCB,
}

impl CreditCardValidator {
    pub fn new() -> Self {
        Self {
            allowed_types: vec![
                CreditCardType::Visa,
                CreditCardType::MasterCard,
                CreditCardType::AmericanExpress,
            ],
            validate_luhn: true,
        }
    }
    
    pub fn with_allowed_types(mut self, types: Vec<CreditCardType>) -> Self {
        self.allowed_types = types;
        self
    }
    
    fn detect_card_type(&self, number: &str) -> Option<CreditCardType> {
        match number {
            n if n.starts_with('4') => Some(CreditCardType::Visa),
            n if n.starts_with("51") || n.starts_with("52") || n.starts_with("53") || 
                 n.starts_with("54") || n.starts_with("55") => Some(CreditCardType::MasterCard),
            n if n.starts_with("34") || n.starts_with("37") => Some(CreditCardType::AmericanExpress),
            n if n.starts_with("6011") || n.starts_with("65") => Some(CreditCardType::Discover),
            n if n.starts_with("300") || n.starts_with("301") || n.starts_with("302") ||
                 n.starts_with("303") || n.starts_with("36") || n.starts_with("38") => Some(CreditCardType::DinersClub),
            n if n.starts_with("35") => Some(CreditCardType::JCB),
            _ => None,
        }
    }
    
    fn validate_luhn_algorithm(&self, number: &str) -> bool {
        let digits: Vec<u32> = number.chars()
            .filter_map(|c| c.to_digit(10))
            .collect();
        
        if digits.is_empty() {
            return false;
        }
        
        let checksum = digits.iter()
            .rev()
            .enumerate()
            .map(|(i, &digit)| {
                if i % 2 == 1 {
                    let doubled = digit * 2;
                    if doubled > 9 { doubled - 9 } else { doubled }
                } else {
                    digit
                }
            })
            .sum::<u32>();
        
        checksum % 10 == 0
    }
    
    fn validate_length(&self, number: &str, card_type: &CreditCardType) -> bool {
        match card_type {
            CreditCardType::Visa => number.len() == 13 || number.len() == 16 || number.len() == 19,
            CreditCardType::MasterCard => number.len() == 16,
            CreditCardType::AmericanExpress => number.len() == 15,
            CreditCardType::Discover => number.len() == 16,
            CreditCardType::DinersClub => number.len() == 14,
            CreditCardType::JCB => number.len() == 15 || number.len() == 16,
        }
    }
}

#[async_trait]
impl FieldValidator for CreditCardValidator {
    async fn validate(
        &self,
        field_name: &str,
        value: &serde_json::Value,
        _context: &ValidationContext,
    ) -> Result<(), ValidationError> {
        if let Some(card_number) = value.as_str() {
            // Remove spaces and hyphens
            let cleaned_number = card_number.replace([' ', '-'], "");
            
            // Validate format (digits only)
            if !cleaned_number.chars().all(|c| c.is_ascii_digit()) {
                return Err(ValidationError::new(
                    "INVALID_CREDIT_CARD_FORMAT",
                    "Credit card number must contain only digits",
                ).with_field(field_name));
            }
            
            // Detect card type
            let card_type = self.detect_card_type(&cleaned_number)
                .ok_or_else(|| ValidationError::new(
                    "UNSUPPORTED_CREDIT_CARD_TYPE",
                    "Unsupported credit card type",
                ).with_field(field_name))?;
            
            // Check if card type is allowed
            if !self.allowed_types.contains(&card_type) {
                return Err(ValidationError::new(
                    "CREDIT_CARD_TYPE_NOT_ALLOWED",
                    &format!("Credit card type {:?} is not allowed", card_type),
                ).with_field(field_name));
            }
            
            // Validate length for card type
            if !self.validate_length(&cleaned_number, &card_type) {
                return Err(ValidationError::new(
                    "INVALID_CREDIT_CARD_LENGTH",
                    &format!("Invalid length for {:?} credit card", card_type),
                ).with_field(field_name));
            }
            
            // Validate using Luhn algorithm
            if self.validate_luhn && !self.validate_luhn_algorithm(&cleaned_number) {
                return Err(ValidationError::new(
                    "INVALID_CREDIT_CARD_CHECKSUM",
                    "Credit card number failed checksum validation",
                ).with_field(field_name));
            }
        }
        
        Ok(())
    }
}
}

Integration with Custom Validators

Use field-level validators within your main custom validators:

#![allow(unused)]
fn main() {
use scim_server::validation::{CustomValidator, ValidationContext, ValidationError};

pub struct ComprehensiveUserValidator {
    field_validator: CustomAttributeValidator,
}

impl ComprehensiveUserValidator {
    pub fn new() -> Self {
        Self {
            field_validator: CustomAttributeValidator::new(),
        }
    }
}

#[async_trait]
impl CustomValidator for ComprehensiveUserValidator {
    async fn validate_user(
        &self,
        user: &User,
        context: &ValidationContext,
    ) -> Result<(), ValidationError> {
        // Validate phone numbers
        if let Some(phone_numbers) = &user.phone_numbers {
            let phone_value = serde_json::to_value(phone_numbers).unwrap();
            self.field_validator
                .validate_field("phoneNumbers", &phone_value, context)
                .await?;
        }
        
        // Validate enterprise extensions
        if let Some(enterprise_ext) = &user.extension_attributes {
            for (key, value) in enterprise_ext {
                let field_name = format!("enterpriseUser:{}", key);
                self.field_validator
                    .validate_field(&field_name, value, context)
                    .await?;
}