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