Skip to main content
Version: 0.7.0

API Error Handling & Validation Implementation Guide

Overview

The OpenAPI Generator for OpenEdge ABL provides comprehensive error handling and validation capabilities with both traditional and optimized validation patterns. This guide covers custom validation hooks, the complete exception-based error handling system, and the optimised ValidationContext pattern.

Architecture

The error handling system follows a layered approach:

  1. System Validation (WebHandler) - Handles parsing errors, type conversion, and OpenAPI schema validation
  2. Custom Validation (Business Logic) - Handles business rules and domain-specific validation
  3. Exception Factory Methods (ApiResultBase) - Provides streamlined access to RFC 7807 compliant error responses
  4. Custom Error Models - Support for OpenAPI-defined error schemas
  5. Business Logic - Core application logic with proper error propagation

Validation Patterns

Strict Validation Behavior

When config strictValidation: true, abstract BusinessLogic adds checks (e.g., null/empty requireds → BadRequest). See business_logic_abstract for conditionals. Errors follow RFC 7807, override for custom. When false, logs warnings instead of adding errors.

ValidationContext Pattern

The ValidationContext pattern provides optimized validation with lazy object creation:

method public ValidationContext ValidateOperationContext(input poPayload as RequestType):
var ValidationContext oContext = new ValidationContext("operationId").

// Schema validation - only creates objects if errors found
var ValidationErrorDetails oSchemaErrors = ValidateSchemaOperation(poPayload).
if valid-object(oSchemaErrors) and oSchemaErrors:HasErrors()
then do:
oContext:MergeErrors(oSchemaErrors).
oContext:SetSchemaValidationFailed().
end.

// Business rules validation - skipped if schema validation failed
if oContext:IsValid()
then do:
var ValidationErrorDetails oBusinessErrors = ValidateBusinessRulesOperation(poPayload).
if valid-object(oBusinessErrors) and oBusinessErrors:HasErrors()
then do:
oContext:MergeErrors(oBusinessErrors).
oContext:SetBusinessRuleValidationFailed().
end.
end.

return oContext.
end method.

Benefits: Better optimizations with lazy object creation and conditional validation execution.

Implementation Examples

1. Default Behavior (No Custom Validation)

class PetBusinessLogic inherits AbstractPetBusinessLogic:
// No validation overrides needed - uses default empty validation
method public override AddPetResult AddPet(input poPayload as AddPetRequest):
// Business logic here
return oResult.
end method.
end class.

2. Schema Validation Override

Override only OpenAPI schema validation rules:

class PetBusinessLogic inherits AbstractPetBusinessLogic:

method public override ValidationErrorDetails ValidateSchemaAddPet(input poPayload as AddPetRequest):
var ValidationErrorDetails oErrors = new ValidationErrorDetails().

// Custom schema validation (beyond OpenAPI spec)
if poPayload:Pet:PhotoUrls:Count = 0 then
oErrors:AddFieldError("pet.photoUrls", "required", "At least one photo URL required").

// Custom format validation
if poPayload:Pet:Name matches "*~~.*" then
oErrors:AddFieldError("pet.name", "invalid_chars", "Pet name cannot contain periods (.)").

return oErrors.
end method.
end class.

3. Business Rules Validation Override

Override only business logic validation:

class PetBusinessLogic inherits AbstractPetBusinessLogic:

method public override ValidationErrorDetails ValidateBusinessRulesAddPet(input poPayload as AddPetRequest):
var ValidationErrorDetails oErrors = new ValidationErrorDetails().

// Business rules validation
if poPayload:Pet:Name = "Fluffy" then
oErrors:AddFieldError("pet.name", "business_rule", "Sorry, no pets named Fluffy allowed").

if poPayload:Pet:Age > 20 then
oErrors:AddFieldError("pet.age", "business_rule", "Pet age seems unrealistic").

// Cross-field validation
if poPayload:Pet:Age < 1 and poPayload:Pet:Status = "adult" then
oErrors:AddFieldError("pet.status", "inconsistent", "Adult pets must be at least 1 year old").

return oErrors.
end method.
end class.

4. Combined Validation Override

Override the complete validation chain:

class PetBusinessLogic inherits AbstractPetBusinessLogic:

method public override ValidationErrorDetails ValidateAddPet(input poPayload as AddPetRequest):
var ValidationErrorDetails oErrors = new ValidationErrorDetails().

// Custom validation chain - complete control
// Run schema validation first
var ValidationErrorDetails oSchemaErrors = this-object:ValidateSchemaAddPet(poPayload).
if oSchemaErrors:HasErrors() then
this-object:MergeValidationErrors(oSchemaErrors, oErrors).

// Run business rules only if schema validation passed
if not oErrors:HasErrors()
then do:
var ValidationErrorDetails oBusinessErrors = this-object:ValidateBusinessRulesAddPet(poPayload).
if oBusinessErrors:HasErrors() then
this-object:MergeValidationErrors(oBusinessErrors, oErrors).
end.

return oErrors.
end method.
end class.

5. Optimized ValidationContext Override

Use the high-performance ValidationContext pattern:

class PetBusinessLogic inherits AbstractPetBusinessLogic:

method public override ValidationContext ValidateAddPetContext(input poPayload as AddPetRequest):
var ValidationContext oContext = new ValidationContext("AddPet").

// Schema validation - lazy object creation
var ValidationErrorDetails oSchemaErrors = this-object:ValidateSchemaAddPet(poPayload).
if valid-object(oSchemaErrors) and oSchemaErrors:HasErrors()
then do:
oContext:MergeErrors(oSchemaErrors).
oContext:SetSchemaValidationFailed().
end.

// Business rules validation - only if schema passed
if oContext:IsValid()
then do:
var ValidationErrorDetails oBusinessErrors = this-object:ValidateBusinessRulesAddPet(poPayload).
if valid-object(oBusinessErrors) and oBusinessErrors:HasErrors()
then do:
oContext:MergeErrors(oBusinessErrors).
oContext:SetBusinessRuleValidationFailed().
end.
end.

return oContext.
end method.
end class.

6. Conditional Validation

Use ValidationContext for complex conditional validation:

class OrderBusinessLogic inherits AbstractOrderBusinessLogic:

method public override ValidationContext ValidateCreateOrderContext(input poPayload as CreateOrderRequest):
var ValidationContext oContext = new ValidationContext("CreateOrder").

// Basic validation first
if poPayload:Items:Count = 0
then do:
oContext:AddFieldError("items", "required", "Order must contain at least one item").
return oContext. // Early exit - no point validating further
end.

// Payment validation - only if items exist
if poPayload:PaymentMethod = "credit_card"
then do:
if poPayload:CreditCardNumber = ? or poPayload:CreditCardNumber = "" then
oContext:AddFieldError("creditCardNumber", "required", "Credit card number required").
end.

// Inventory validation - only if payment is valid
if oContext:IsValid()
then do:
var ValidationErrorDetails oInventoryErrors = ValidateInventoryAvailability(poPayload).
if valid-object(oInventoryErrors) and oInventoryErrors:HasErrors()
then do:
oContext:MergeErrors(oInventoryErrors).
oContext:SetBusinessRuleValidationFailed().
end.
end.

return oContext.
end method.
end class.

WebHandler Integration

Traditional Pattern (Default)

// WebHandler automatically calls traditional validation
define variable oValidationErrors as ValidationErrorDetails no-undo.
oValidationErrors = oPetBusinessLogic:ValidateAddPet(oPayload).
if oValidationErrors:HasErrors() then
undo, throw new ApiException(400, oValidationErrors).

Optimized Pattern (New)

// WebHandler calls optimized validation
var ValidationContext oValidationContext = oPetBusinessLogic:ValidateAddPetContext(oPayload).
if oValidationContext:HasErrors() then
undo, throw new ApiException(400, oValidationContext:GetValidationErrors()).

Validation Flow

The system supports multiple validation flows:

Complete Validation Chain

  1. Parameter Validation (WebHandler) - Path, query, header, body parameters
  2. Schema Validation (Abstract Business Logic) - OpenAPI schema rules
  3. Business Rules Validation (Concrete Business Logic) - Custom business logic
  4. Main Business Logic (Concrete Business Logic) - Core application logic

Optimized Validation Chain

  1. Parameter Validation (WebHandler) - Path, query, header, body parameters
  2. Schema Validation (Conditional) - Only if needed
  3. Business Rules Validation (Conditional) - Only if schema validation passed
  4. Main Business Logic (Concrete Business Logic) - Core application logic

Exception Factory Methods

The ApiResultBase class provides streamlined exception factory methods for all standard HTTP error responses. All methods return ApiException objects and can be used directly:

4xx Client Error Responses

// Bad Request (400) - General validation errors
throw oResult:BadRequestException("Invalid request data").
throw oResult:BadRequestException(oValidationErrors).

// Unauthorized (401)
throw oResult:UnauthorizedException("Authentication required").

// Forbidden (403)
throw oResult:ForbiddenException("Access denied to this resource").

// Not Found (404)
throw oResult:NotFoundException("Pet with ID 123 not found").

// Method Not Allowed (405)
throw oResult:MethodNotAllowedException("POST method not supported for this endpoint").

// Not Acceptable (406)
throw oResult:NotAcceptableException("Requested content type not acceptable").

// Conflict (409)
throw oResult:ConflictException("Pet with this name already exists").

// Gone (410)
throw oResult:GoneException("This resource is no longer available").

// Unprocessable Entity (422)
throw oResult:UnprocessableEntityException("Validation failed for the provided data").

// Too Many Requests (429)
throw oResult:TooManyRequestsException("Rate limit exceeded, please try again later").

5xx Server Error Responses

// Internal Server Error (500)
throw oResult:InternalServerErrorException("An unexpected error occurred").

// Not Implemented (501)
throw oResult:NotImplementedException("This feature is not yet implemented").

// Bad Gateway (502)
throw oResult:BadGatewayException("Upstream service returned an invalid response").

// Service Unavailable (503)
throw oResult:ServiceUnavailableException("Service is temporarily unavailable").

// Gateway Timeout (504)
throw oResult:GatewayTimeoutException("Upstream service timed out").

Custom Error Models

When your OpenAPI specification defines custom error models, the generator creates specific exception classes:

Custom Error Model Example

// OpenAPI spec defines:
components:
schemas:
CustomError:
type: object
properties:
errorCode:
type: string
message:
type: string
details:
type: object

// Generated classes:
class CustomError implements IModelBase:
// Properties and methods for CustomError
end class.

class CustomErrorException inherits Progress.Lang.AppError:
// Constructor and methods for throwing CustomError
end class.

Using Custom Error Models

method public override UpdatePetResult UpdatePet(input poPayload as UpdatePetRequest):
var UpdatePetResult oResult = new UpdatePetResult().

// Business logic validation
if poPayload:Pet:Status = "sold" and poPayload:Pet:OwnerId = ?
then do:
undo, throw new CustomErrorException(new CustomErrorBuilder()
:SetErrorCode("PET_SOLD_WITHOUT_OWNER")
:SetMessage("Cannot mark pet as sold without an owner")
:SetDetails(/* additional data */)
:Build()
).
end.

// Continue with normal processing...
return oResult.
end method.

Error Response Format

All errors follow RFC 7807 (Problem Details) format:

Standard Error Response

{
"type": "https://api.example.com/errors/bad-request",
"title": "Bad Request",
"status": 400,
"detail": "The request contains invalid data",
"instance": "/operations/addPet"
}

Validation Error Response

{
"type": "/errors/validation",
"title": "One or more validation errors occurred",
"status": 400,
"errors": [
{
"field": "pet.name",
"code": "business_rule",
"message": "Sorry, no pets named Fluffy allowed",
"value": null
},
{
"field": "pet.age",
"code": "business_rule",
"message": "Pet age seems unrealistic",
"value": null
}
]
}

Custom Error Model Response

{
"type": "/errors/custom",
"title": "Custom Error",
"status": 422,
"errorCode": "PET_SOLD_WITHOUT_OWNER",
"message": "Cannot mark pet as sold without an owner",
"details": {
"petId": 123,
"currentStatus": "available"
}
}

Best Practices

1. Validation Method Naming

  • Methods follow the pattern Validate{OperationId}
  • Each operation gets its own validation method

2. Error Codes

  • Use descriptive error codes like "business_rule", "required", "invalid_format"
  • Keep codes consistent across your application
  • Codes should be machine-readable

3. Field Paths

  • Use dot notation for nested fields: "pet.name", "address.street"
  • Use array notation for array elements: "tags[0]", "items[].name"
  • Keep field paths consistent with your data model

4. Exception Factory Method Usage

  • Always use the streamlined factory methods from ApiResultBase
  • They provide consistent RFC 7807 compliance
  • They handle proper error detail enrichment automatically

5. Custom Error Models

  • Use custom error models when you need structured error data beyond simple messages
  • Define them in your OpenAPI specification for automatic generation
  • Follow consistent naming patterns

6. Performance Considerations

  • Keep validation logic lightweight
  • Avoid database calls in validation (use business logic for that)
  • Consider caching expensive validation operations

7. Testing

  • Unit test validation methods independently
  • Test both positive and negative validation scenarios
  • Verify error messages are user-friendly
  • Test custom error model serialization

Advanced Usage

Conditional Validation

method protected override ValidationErrorDetails ValidateUpdatePet(input poPayload as UpdatePetRequest):
var ValidationErrorDetails oErrors = new ValidationErrorDetails().

// Only validate certain fields if they are provided
if poPayload:Pet:Name <> ? and poPayload:Pet:Name <> ""
then do:
if length(poPayload:Pet:Name) < 2
then oErrors:AddFieldError("pet.name", "min_length", "Pet name must be at least 2 characters").
end.

return oErrors.
end method.

Cross-Field Validation

method protected override ValidationErrorDetails ValidateCreateOrder(input poPayload as CreateOrderRequest):
var ValidationErrorDetails oErrors = new ValidationErrorDetails().

// Cross-field validation
if poPayload:Order:PaymentMethod = "credit_card"
then do:
if poPayload:Order:CreditCardNumber = ? or poPayload:Order:CreditCardNumber = ""
then oErrors:AddFieldError("order.creditCardNumber", "required", "Credit card number required for credit card payments").
end.

return oErrors.
end method.

Validation Chains

method protected override ValidationErrorDetails ValidateUpdateOrder(input poPayload as UpdateOrderRequest):
var ValidationErrorDetails oErrors = new ValidationErrorDetails().

// Basic validation - required fields
if poPayload:OrderId = ? or poPayload:OrderId = 0
then oErrors:AddFieldError("orderId", "required", "Order ID is required").

if poPayload:Status = ? or poPayload:Status = ""
then oErrors:AddFieldError("status", "required", "Order status is required").

// Only run expensive validation if basic validation passed
if not oErrors:HasErrors()
then do:
// Check if order exists and can be updated
if not this:OrderExists(poPayload:OrderId)
then oErrors:AddFieldError("orderId", "not_found", "Order not found").

if not this:CanUpdateOrderStatus(poPayload:OrderId, poPayload:Status)
then oErrors:AddFieldError("status", "invalid_transition", "Invalid status transition for this order").
end.

return oErrors.
end method.

Error Response Mapping

method public override DeletePetResult DeletePet(input piPetId as integer):
var DeletePetResult oResult = new DeletePetResult().

// Check if pet exists
if not PetExists(piPetId)
then throw oResult:NotFoundException(substitute("Pet with ID &1 not found", piPetId)).

// Check if pet can be deleted
if IsPetSold(piPetId)
then throw oResult:ConflictException("Cannot delete a pet that has been sold").

// Check permissions
if not UserCanDeletePet(piPetId)
then throw oResult:ForbiddenException("You don't have permission to delete this pet").

// Perform deletion
this:DeletePetFromDatabase(piPetId).
oResult:SetNoContent(). // or whichever response status code is defined for this operation

return oResult.
end method.

Migration Guide

Existing Code

No changes required for existing generated code. The validation hooks are optional and have default implementations that return no errors.

Adding Custom Validation

  1. Locate your concrete business logic implementation
  2. Override the appropriate Validate{OperationId} method
  3. Implement your business validation logic
  4. Return ValidationErrorDetails with any errors found

Upgrading to Exception Factory Methods

  1. Replace manual ApiException creation with factory methods
  2. Use oResult:ExceptionMethod() instead of new ApiException()
  3. Remove manual error detail creation where possible

Troubleshooting

Common Issues

  1. Validation method not called

    • Ensure the method name matches exactly: Validate{OperationId}
    • Check that the operation has parameters (in the OpenAPI spec)
  2. Validation errors not returned

    • Verify ValidationErrorDetails:HasErrors() returns true
    • Check that the appropriate exception is thrown
  3. Type mismatch errors

    • Ensure parameter types match the operation's input type
    • Check that the method signature matches the abstract declaration
  4. Custom error model not working

    • Verify the error model is defined in your OpenAPI specification
    • Check that the generated exception class exists
    • Ensure proper serialization of custom properties
  5. Factory/Variant Configuration Errors

    • Invalid factory class: Check businessLogicFactories paths in config (dynamic-new errors logged)
    • Variant header ignored: Verify "variantSelection": {"enabled": true} and header name in config
    • No mock factory: Enable mock_data: true in yaml or register custom mock factory in config
    • Config not loading: Ensure OrdersPing-api-config.json exists in PROPATH
    • Invalid variant header: Fallback to default BusinessLogic with log, check header name matches config
    • No BusinessLogic factory: ApiException in webhandler if registry has no valid factory for key
    • Strict validation warnings: Check logs for warnings when strictValidation=false but issues detected

Debugging

Enable logging in your business logic class to debug validation:

logger:Debug(substitute("Validating operation &1", "AddPet")).
logger:Debug(substitute("Validation errors found: &1", oErrors:GetErrorCount())).

RFC 7807 Compliance

All error responses follow RFC 7807 (Problem Details for HTTP APIs):

Required Properties

  • type: URI reference identifying the problem type
  • title: Short, human-readable summary
  • status: HTTP status code

Optional Properties

  • detail: Human-readable explanation
  • instance: URI reference identifying the specific occurrence
  • extensions: Additional problem-specific information

Benefits

  • Standardized error format across all APIs
  • Machine-readable error information
  • Consistent client error handling
  • Extensible for custom error data

Conclusion

This comprehensive error handling and validation system provides a powerful and flexible way to implement business-specific validation rules and proper error responses. The system integrates seamlessly with the existing infrastructure and provides consistent, RFC 7807 compliant error responses across all generated OpenEdge ABL code.

The streamlined exception factory methods eliminate boilerplate code while maintaining full control over error details. Custom error models allow for rich, structured error information when needed, and the validation hooks provide clean separation between system and business validation logic.