Table of contents

How do I implement custom response validation with Alamofire?

Custom response validation in Alamofire allows you to define specific criteria for determining whether an HTTP response is valid or should be treated as an error. This is essential for robust iOS applications that need to handle various API response scenarios beyond basic HTTP status codes.

Understanding Alamofire's Default Validation

By default, Alamofire considers responses with status codes in the 200-299 range as successful. However, many APIs have specific requirements that go beyond simple status code validation.

import Alamofire

// Default validation - only checks status codes 200-299
AF.request("https://api.example.com/data")
    .validate() // Uses default validation
    .responseJSON { response in
        switch response.result {
        case .success(let value):
            print("Success: \(value)")
        case .failure(let error):
            print("Error: \(error)")
        }
    }

Implementing Custom Status Code Validation

You can create custom validation rules for specific status codes that your API considers valid:

import Alamofire

// Custom status code validation
AF.request("https://api.example.com/data")
    .validate(statusCode: 200..<300) // Accept 200-299
    .validate(statusCode: [404]) // Also accept 404 as valid
    .responseJSON { response in
        switch response.result {
        case .success(let value):
            print("Valid response: \(value)")
        case .failure(let error):
            print("Invalid response: \(error)")
        }
    }

For more complex status code validation:

// Custom status code ranges
let acceptableStatusCodes = Set([200, 201, 202, 204, 304, 404])

AF.request("https://api.example.com/data")
    .validate { request, response, data in
        if acceptableStatusCodes.contains(response.statusCode) {
            return .success(Void())
        } else {
            let reason = AFError.ResponseValidationFailureReason.unacceptableStatusCode(code: response.statusCode)
            return .failure(AFError.responseValidationFailed(reason: reason))
        }
    }
    .responseJSON { response in
        // Handle response
    }

Content-Type Validation

Validate that the response contains the expected content type:

import Alamofire

// Validate content type
AF.request("https://api.example.com/data")
    .validate(contentType: ["application/json", "text/json"])
    .responseJSON { response in
        // Response guaranteed to be JSON format
    }

// Custom content type validation
AF.request("https://api.example.com/file")
    .validate { request, response, data in
        guard let contentType = response.mimeType else {
            return .failure(AFError.responseValidationFailed(reason: .missingContentType))
        }

        let acceptableTypes = ["application/pdf", "image/jpeg", "image/png"]
        if acceptableTypes.contains(contentType) {
            return .success(Void())
        } else {
            let reason = AFError.ResponseValidationFailureReason.unacceptableContentType(acceptableContentTypes: acceptableTypes, responseContentType: contentType)
            return .failure(AFError.responseValidationFailed(reason: reason))
        }
    }
    .responseData { response in
        // Handle validated file data
    }

Advanced Custom Validation Logic

For complex business logic validation, you can inspect the actual response data:

import Alamofire
import Foundation

struct APIResponse: Codable {
    let success: Bool
    let data: String?
    let errorCode: Int?
    let message: String?
}

// Custom business logic validation
AF.request("https://api.example.com/data")
    .validate { request, response, data in
        // First validate HTTP status
        guard 200..<300 ~= response.statusCode else {
            let reason = AFError.ResponseValidationFailureReason.unacceptableStatusCode(code: response.statusCode)
            return .failure(AFError.responseValidationFailed(reason: reason))
        }

        // Then validate response structure
        guard let data = data else {
            return .failure(AFError.responseValidationFailed(reason: .dataFileNil))
        }

        do {
            let apiResponse = try JSONDecoder().decode(APIResponse.self, from: data)

            // Custom business logic validation
            if apiResponse.success {
                return .success(Void())
            } else {
                // Create custom error based on API response
                let userInfo = [
                    NSLocalizedDescriptionKey: apiResponse.message ?? "API request failed",
                    "errorCode": apiResponse.errorCode ?? -1
                ] as [String : Any]

                let customError = NSError(domain: "APIValidationError", code: apiResponse.errorCode ?? -1, userInfo: userInfo)
                return .failure(AFError.responseValidationFailed(reason: .customValidationFailed(error: customError)))
            }
        } catch {
            return .failure(AFError.responseValidationFailed(reason: .customValidationFailed(error: error)))
        }
    }
    .responseDecodable(of: APIResponse.self) { response in
        switch response.result {
        case .success(let apiResponse):
            print("Valid API response: \(apiResponse)")
        case .failure(let error):
            print("Validation failed: \(error)")
        }
    }

Multiple Validation Rules

You can chain multiple validation rules for comprehensive response checking:

import Alamofire

AF.request("https://api.example.com/data")
    .validate(statusCode: 200..<300)
    .validate(contentType: ["application/json"])
    .validate { request, response, data in
        // Custom validation logic
        guard let data = data, data.count > 0 else {
            return .failure(AFError.responseValidationFailed(reason: .dataFileNil))
        }

        // Check for minimum response size
        guard data.count >= 10 else {
            let error = NSError(domain: "ValidationError", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Response too small"])
            return .failure(AFError.responseValidationFailed(reason: .customValidationFailed(error: error)))
        }

        return .success(Void())
    }
    .responseJSON { response in
        // All validations passed
    }

Creating Reusable Validation Functions

For better code organization, create reusable validation functions:

import Alamofire
import Foundation

extension DataRequest {
    @discardableResult
    func validateAPIResponse() -> Self {
        return validate { request, response, data in
            // Reusable API-specific validation
            guard let data = data else {
                return .failure(AFError.responseValidationFailed(reason: .dataFileNil))
            }

            do {
                if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
                   let status = json["status"] as? String,
                   status == "success" {
                    return .success(Void())
                } else {
                    let error = NSError(domain: "APIError", code: 2001, userInfo: [NSLocalizedDescriptionKey: "API returned error status"])
                    return .failure(AFError.responseValidationFailed(reason: .customValidationFailed(error: error)))
                }
            } catch {
                return .failure(AFError.responseValidationFailed(reason: .customValidationFailed(error: error)))
            }
        }
    }

    @discardableResult
    func validateMinimumDataSize(_ minimumSize: Int) -> Self {
        return validate { request, response, data in
            guard let data = data, data.count >= minimumSize else {
                let error = NSError(domain: "ValidationError", code: 3001, userInfo: [NSLocalizedDescriptionKey: "Response data too small"])
                return .failure(AFError.responseValidationFailed(reason: .customValidationFailed(error: error)))
            }
            return .success(Void())
        }
    }
}

// Usage of custom validation extensions
AF.request("https://api.example.com/data")
    .validateAPIResponse()
    .validateMinimumDataSize(100)
    .responseJSON { response in
        // Handle validated response
    }

Error Handling with Custom Validation

Proper error handling is crucial when implementing custom validation:

import Alamofire

AF.request("https://api.example.com/data")
    .validate { request, response, data in
        // Custom validation with detailed error information
        guard 200..<300 ~= response.statusCode else {
            let reason = AFError.ResponseValidationFailureReason.unacceptableStatusCode(code: response.statusCode)
            return .failure(AFError.responseValidationFailed(reason: reason))
        }

        return .success(Void())
    }
    .responseJSON { response in
        switch response.result {
        case .success(let value):
            print("Success: \(value)")
        case .failure(let error):
            if let afError = error as? AFError {
                switch afError {
                case .responseValidationFailed(let reason):
                    switch reason {
                    case .unacceptableStatusCode(let code):
                        print("Invalid status code: \(code)")
                    case .unacceptableContentType(let acceptableTypes, let responseType):
                        print("Invalid content type. Expected: \(acceptableTypes), Got: \(responseType)")
                    case .customValidationFailed(let error):
                        print("Custom validation failed: \(error.localizedDescription)")
                    default:
                        print("Other validation failure: \(reason)")
                    }
                default:
                    print("Other AFError: \(afError)")
                }
            } else {
                print("Unknown error: \(error)")
            }
        }
    }

Validation for Web Scraping Applications

When implementing web scraping applications using Alamofire, custom validation becomes particularly important for ensuring data quality and handling edge cases:

import Alamofire
import Foundation

// Specialized validation for scraping HTML content
extension DataRequest {
    @discardableResult
    func validateHTMLContent() -> Self {
        return validate { request, response, data in
            guard let data = data,
                  let htmlString = String(data: data, encoding: .utf8) else {
                let error = NSError(domain: "ScrapingError", code: 4001, userInfo: [NSLocalizedDescriptionKey: "Could not decode HTML content"])
                return .failure(AFError.responseValidationFailed(reason: .customValidationFailed(error: error)))
            }

            // Check if response contains valid HTML structure
            if htmlString.contains("<html") && htmlString.contains("</html>") {
                return .success(Void())
            } else {
                let error = NSError(domain: "ScrapingError", code: 4002, userInfo: [NSLocalizedDescriptionKey: "Response does not contain valid HTML structure"])
                return .failure(AFError.responseValidationFailed(reason: .customValidationFailed(error: error)))
            }
        }
    }

    @discardableResult
    func validateAntiBot() -> Self {
        return validate { request, response, data in
            guard let data = data,
                  let content = String(data: data, encoding: .utf8) else {
                return .success(Void())
            }

            // Check for common anti-bot indicators
            let antiBotIndicators = [
                "captcha",
                "blocked",
                "bot detected",
                "access denied"
            ]

            let lowercaseContent = content.lowercased()
            for indicator in antiBotIndicators {
                if lowercaseContent.contains(indicator) {
                    let error = NSError(domain: "ScrapingError", code: 4003, userInfo: [NSLocalizedDescriptionKey: "Potential anti-bot mechanism detected"])
                    return .failure(AFError.responseValidationFailed(reason: .customValidationFailed(error: error)))
                }
            }

            return .success(Void())
        }
    }
}

Best Practices for Custom Validation

  1. Keep validation logic focused: Each validation function should have a single responsibility
  2. Provide meaningful error messages: Help developers understand what went wrong
  3. Consider performance: Avoid heavy processing in validation functions
  4. Test thoroughly: Validate your validation logic with various response scenarios
  5. Document your validation rules: Make it clear what constitutes a valid response
  6. Use appropriate error codes: Create a consistent error code system for different validation failures

Integration with Timeout and Retry Logic

Custom validation works seamlessly with Alamofire's timeout and retry mechanisms. When you set timeouts for HTTP requests using Alamofire, validation failures can trigger retry attempts:

import Alamofire

// Combining validation with retry logic
AF.request("https://api.example.com/data")
    .validate(statusCode: 200..<300)
    .validateHTMLContent()
    .validateAntiBot()
    .responseString { response in
        switch response.result {
        case .success(let html):
            // Process valid HTML content
            print("Successfully validated and received HTML: \(html.prefix(100))...")
        case .failure(let error):
            // Handle validation or network failure
            print("Request failed: \(error)")
        }
    }

You can also combine custom validation with retry logic for failed requests in Alamofire to create robust scraping applications that automatically recover from temporary issues.

Testing Custom Validation

Unit testing your custom validation logic is crucial for maintaining reliable applications:

import XCTest
import Alamofire
@testable import YourApp

class CustomValidationTests: XCTestCase {
    func testValidHTMLContentValidation() {
        let validHTML = "<html><head><title>Test</title></head><body>Content</body></html>"
        let data = validHTML.data(using: .utf8)!
        let response = HTTPURLResponse(url: URL(string: "https://example.com")!, 
                                     statusCode: 200, 
                                     httpVersion: nil, 
                                     headerFields: nil)!

        let request = AF.request("https://example.com")
        let result = request.validateHTMLContent()

        // Test would need custom validation result checking
        // This is a simplified example structure
    }

    func testAntiBotDetection() {
        let blockedContent = "Access denied - bot detected"
        let data = blockedContent.data(using: .utf8)!

        // Similar test structure for anti-bot validation
    }
}

Conclusion

Custom response validation in Alamofire provides powerful tools for ensuring your iOS applications receive and process only valid responses. By implementing proper validation logic, you can catch errors early, provide better user experiences, and build more reliable applications.

Whether you're building traditional iOS apps or specialized web scraping tools, custom validation helps maintain data integrity and provides clear feedback when responses don't meet your application's requirements. Remember to balance thoroughness with performance, always provide clear error messages, and test your validation logic thoroughly across different scenarios.

The combination of status code validation, content-type checking, business logic validation, and specialized scraping validations creates a robust foundation for handling diverse API and web scraping scenarios in your Swift applications.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon