Skip to content

Validation & Field Paths ​

The haskoning_business_logic_utils package provides a validation system that allows you to add remarks, warnings, and errors to fields dynamically during calculations. This is essential for providing feedback to users about system-calculated values and validation issues.

Key Concepts ​

Value Types ​

  • User-defined value: Value set by the user in the frontend; can be overridden by the system
  • System value: Value calculated by the system based on other values
  • Design value: Value used in calculations; either the user-defined or system value
  • Default value: Static value defined in the API; displayed on initial page load and automatically acts as the user-defined value

Field Paths with FieldPathMixin ​

The FieldPathMixin enables you to get the full path to a field in your data model. This is crucial for targeting specific fields with validation messages.

Setting Up Models ​

python
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from haskoning_bussines_logic_utils.validation_context.field_path_mixin import FieldPathMixin

class InputBaseModel(FieldPathMixin, BaseModel):
    model_config = ConfigDict(
        validate_assignment=True,
        validate_default=True
    )

class DesignLoads(InputBaseModel):
    load_factor: Annotated[float, Field(
        description="Load factor for the design (-)", 
        default=1, 
        ge=0
    )]
    cod_load: Annotated[float, Field(
        description="COD design load [kg/s]", 
        ge=0, 
        default=47000 / 24 / 3600
    )]
    bod_load: Annotated[float, Field(
        description="BOD design load [kg/s]", 
        ge=0, 
        default=18500 / 24 / 3600
    )]

class OperatingWindow(InputBaseModel):
    design_loads: DesignLoads = Field(default_factory=DesignLoads)

class InputSchema(InputBaseModel):
    operating_window: OperatingWindow = Field(default_factory=OperatingWindow)

Getting Field Paths ​

Use the get_field_path method to retrieve the full path to a field:

python
def test(input_schema: InputSchema):
    # All three examples return: 'operating_window.design_loads.load_factor'
    
    # From top level
    input_schema.operating_window.design_loads.get_field_path('load_factor')
    
    # From intermediate level
    operating_window = input_schema.operating_window
    operating_window.design_loads.get_field_path('load_factor')
    
    # From the nested level
    design_loads = operating_window.design_loads
    design_loads.get_field_path('load_factor')

Invalid Field Names ​

python
# This will raise an error
input_schema.operating_window.design_loads.get_field_path('invalid_field')

Adding Validation Messages ​

Import the validation functions:

python
from haskoning_bussines_logic_utils.validation_context.main import (
    add_remark, 
    add_warning, 
    add_error,
    add_visibility
)

Add Remark (with System Value) ​

Remarks notify users that the system has overridden their input with a calculated value. The design value becomes the system value.

python
from haskoning_bussines_logic_utils.validation_context.main import add_remark

def test(input_schema: InputSchema):
    if input_schema.operating_window.design_loads.load_factor != 2:
        add_remark(
            message=f"Load factor must be 2 for this configuration",
            field_path=input_schema.operating_window.design_loads.get_field_path('load_factor')
        )
        
        # After adding remark, set the system value
        input_schema.operating_window.design_loads.load_factor = 2

Set Warning ​

Warnings alert users to recommended values while still allowing their input to be used:

python
from haskoning_bussines_logic_utils.validation_context.main import set_warning

def check_BOD_TKN_ratio(design_loads: DesignLoads):
    """Check if: BOD/TKN Ratio out of range"""

    BOD_TKN = BOD_TKN_division(
        BOD_load=design_loads.bod_load,
        TKN_load=design_loads.tkn_load)

    if BOD_TKN < 3.5:
        set_warning(
            field_path=design_loads.get_field_path("bod_load"),
            message="BOD/TKN Ratio is out of range (lower than 3.5) - Confirm BOD & TKN loads. Consider increasing the nitrification rate with BOD TKN Correction settings",
        )
        set_warning(
            field_path=design_loads.get_field_path("tkn_load"),
            message="BOD/TKN Ratio is out of range (lower than 3.5) -  Confirm BOD & TKN loads. Consider increasing the nitrification rate with BOD TKN Correction settings",
        )
    if BOD_TKN > 6.5:
        set_warning(
            field_path=design_loads.get_field_path("bod_load"),
            message="BOD/TKN Ratio is out of range (higher than 6.5) -  Confirm BOD & TKN loads. Consider decreasing the nitrification rate with BOD TKN Correction settings",
        )
        set_warning(
            field_path=design_loads.get_field_path("tkn_load"),
            message="BOD/TKN Ratio is out of range (higher than 6.5) -  Confirm BOD & TKN loads. Consider decreasing the nitrification rate with BOD TKN Correction settings",
        )

Warnings are displayed like this:

Validation warning

Add Error ​

Errors indicate validation failures that must be corrected:

python
from haskoning_bussines_logic_utils.validation_context.main import add_error

def from_periodic_scenario(design_loads: DesignLoads):
    if design_loads.bod_load > 0.2:
        add_error(
            message=f"BOD load must be ≤ 0.2 kg/s. Current value is too high.",
            field_path=design_loads.get_field_path('bod_load')
        )
        
        # Set the corrected system value
        design_loads.bod_load = 0.2

Add Visibility Control ​

Control whether fields are visible or enabled in the frontend:

python
from haskoning_bussines_logic_utils.validation_context.main import add_visibility

def from_periodic_scenario(design_loads: DesignLoads):
    if design_loads.bod_load > 0.2:
        add_visibility(
            field_path=design_loads.get_field_path('bod_load'),
            disabled=True,  # Disable the field
            hidden=False    # Keep it visible
        )

Using Validation in Endpoints ​

Helper Functions ​

python
from haskoning_bussines_logic_utils.validation_context.context import (
    get_validation_context, 
    finalize_validation_context,
    reset_validation_context
)

Finalizing Validation Context ​

After adding validation and updating values, call finalize_validation_context in your endpoint. This handles system value overrides automatically:

python
from haskoning_bussines_logic_utils.validation_context.context import (
    get_validation_context, 
    finalize_validation_context
)
from copy import deepcopy

def my_endpoint(body: InputSchema):
    # Keep original data for comparison
    original_body = deepcopy(body)
    
    # Run your calculations (which may add validation)
    output = perform_calculations(body)
    
    # Finalize validation context
    finalize_validation_context(original_body, body)
    
    # Attach validation to output
    output.validation = get_validation_context()
    
    return output

Note: The system value will always reflect the latest value, overwriting the previous field value.

Resetting Validation Context ​

To prevent validation data from previous requests from being carried over, reset the context at the start of each request:

python
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from haskoning_bussines_logic_utils.validation_context.context import reset_validation_context

class ValidationContextMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        reset_validation_context()
        response = await call_next(request)
        return response

Add this middleware to your FastAPI application:

python
from fastapi import FastAPI

app = FastAPI()
app.add_middleware(ValidationContextMiddleware)

Complete Example ​

Here's a complete example showing validation in action:

python
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field
from copy import deepcopy

from haskoning_bussines_logic_utils.validation_context.field_path_mixin import FieldPathMixin
from haskoning_bussines_logic_utils.validation_context.main import (
    add_remark, 
    add_warning, 
    add_error
)
from haskoning_bussines_logic_utils.validation_context.context import (
    get_validation_context,
    finalize_validation_context
)

# Models
class InputBaseModel(FieldPathMixin, BaseModel):
    model_config = ConfigDict(validate_assignment=True, validate_default=True)

class CalculationInput(InputBaseModel):
    flow_rate: Annotated[float, Field(
        description="Flow rate [m³/h]",
        default=100,
        ge=0
    )]
    temperature: Annotated[float, Field(
        description="Temperature [°C]",
        default=20,
        ge=-10,
        le=50
    )]

class CalculationOutput(BaseModel):
    result: float
    validation: dict | None = None

# Business logic
def perform_calculation(input_data: CalculationInput) -> float:
    # Check flow rate
    if input_data.flow_rate < 50:
        add_warning(
            message="Flow rate is below recommended minimum of 50 m³/h",
            field_path=input_data.get_field_path('flow_rate')
        )
    
    # Check temperature
    if input_data.temperature > 30:
        add_remark(
            message="Temperature exceeds 30°C, using maximum design value",
            field_path=input_data.get_field_path('temperature')
        )
        input_data.temperature = 30
    
    if input_data.temperature < 0:
        add_error(
            message="Temperature cannot be below 0°C for this calculation",
            field_path=input_data.get_field_path('temperature')
        )
        input_data.temperature = 0
    
    # Perform calculation
    return input_data.flow_rate * input_data.temperature * 0.42

# API endpoint
def calculate_endpoint(body: CalculationInput) -> CalculationOutput:
    original_body = deepcopy(body)
    
    # Run calculations
    result = perform_calculation(body)
    
    # Finalize validation
    finalize_validation_context(original_body, body)
    
    # Return output with validation
    return CalculationOutput(
        result=result,
        validation=get_validation_context()
    )

Best Practices ​

  • Always use deepcopy to preserve original input values
  • Call finalize_validation_context after all calculations
  • Use middleware to reset validation context for each request
  • Add remarks when the system overrides user input
  • Add warnings for recommended but not required changes
  • Add errors for validation failures that must be corrected
  • Update field values after adding validation messages
  • Use descriptive messages that help users understand what happened

Next Steps ​