Appearance
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 = 2Set 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:

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.2Add 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 outputNote: 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 responseAdd 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
deepcopyto preserve original input values - Call
finalize_validation_contextafter 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
- Integrate validated models into Generate Endpoints
- Add visual styles for affected fields in UI Styling
- Combine validation with unit-aware calculations in Units System