Hello! Let's continue our journey in building robust machine learning APIs.
In our last lesson, you successfully implemented the core inference logic for your prediction endpoint. Your API can now take in feature data, pass it to a scikit-learn model, and return a properly formatted prediction. This is a massive step!
However, our API currently has a blind spot. While Pydantic ensures the data types are correct (e.g., sepal_length is a float), it doesn't check if the values make sense. A user could send a petal_length of -10 cm, which is physically impossible. This could cause your model to return nonsensical predictions or even crash.
Today, we'll address this by focusing on our next learning outcome: Add custom input validation to the ML prediction endpoint using Pydantic validators.
We will enhance our IrisFeatures Pydantic model with two layers of validation:
- Simple constraints to enforce basic rules on individual fields (e.g., values must be positive).
- Custom validator functions to implement more complex, domain-specific logic that involves multiple fields.
By the end of this lesson, your API will be far more robust, capable of rejecting semantically invalid data before it ever reaches your model.
1. The First Line of Defense: Field Constraints
Pydantic's most straightforward validation method beyond simple type hints is using the Field function. It allows you to add constraints directly to your model's attributes. For our Iris dataset, a fundamental rule is that all measurements must be positive.
Let's modify your IrisFeatures schema to enforce this. You'll import Field from Pydantic and use it to declare that each feature must be greater than zero (gt=0).
Update your Pydantic model (likely in schemas.py or main.py) to look like this:
from pydantic import BaseModel, Field
# In previous lessons, this might have been named IrisFeatures
class PredictionInput(BaseModel):
sepal_length: float = Field(..., gt=0, description="Sepal length in cm")
sepal_width: float = Field(..., gt=0, description="Sepal width in cm")
petal_length: float = Field(..., gt=0, description="Petal length in cm")
petal_width: float = Field(..., gt=0, description="Petal width in cm")
A quick note on the syntax:
Field(...): The ellipsis (...) indicates that this field is required.gt=0: This is the constraint, meaning "greater than 0". Other options includelt(less than),ge(greater than or equal to), andle(less than or equal to).
After updating your code, restart your Uvicorn server. Go to the Swagger UI at http://127.0.0.1:8000/docs, try to execute a prediction with a negative value for any feature, and observe the response. FastAPI will automatically catch the validation error from Pydantic and return a 422 Unprocessable Entity error with a descriptive message.
The video below demonstrates how these automatic validation errors appear in FastAPI.
Python FastAPI Tutorial (Part 4): Pydantic Schemas - Request and Response Validation
In this clip from his FastAPI tutorial series, Corey Schafer tests an endpoint with invalid data, showcasing the automatic 422 error responses that FastAPI generates based on Pydantic's validation rules.
Starting around 18 minutes in, watch testing validation. Notice how providing an empty string for a required field or omitting a field altogether results in a detailed error message explaining exactly what went wrong.
2. Advanced Logic with Custom Validators
Field constraints are great for simple rules, but what about more complex, domain-specific logic? For example, in most iris species, the petal is shorter than the sepal. An input where petal_length > sepal_length might be a sign of a measurement error.
Pydantic's validators, implemented as methods with decorators, are the perfect tool for this. Since you are proficient with Python decorators, this pattern should feel familiar.
There are two main types of validators:
@field_validator: Operates on a single field's data.@model_validator: Operates on the entire model's data, allowing for cross-field validation.
For our use case—comparing petal_length and sepal_length—we need @model_validator.
Let's start by looking at a concise article that uses @field_validator. While its example validates the length of a list, the structure and principle are identical.
Creating a Secure Machine Learning API with FastAPI and ...
The article 'Creating a Secure Machine Learning API...' provides a clear, practical example of adding a custom validator to a Pydantic model for an ML API.
In 'Step 3: Validate the Input', read the validation introduction and review the code block that follows it. Pay close attention to the use of the @field_validator("data") decorator and how it raises a ValueError if the validation fails. This is the fundamental pattern we will be using.
Now, let's apply this concept to our IrisFeatures schema using @model_validator to enforce our cross-field rule.
Update your PredictionInput model again with the following code.
from pydantic import BaseModel, Field, model_validator
from typing import Self # Import Self for type hinting
class PredictionInput(BaseModel):
sepal_length: float = Field(..., gt=0, description="Sepal length in cm")
sepal_width: float = Field(..., gt=0, description="Sepal width in cm")
petal_length: float = Field(..., gt=0, description="Petal length in cm")
petal_width: float = Field(..., gt=0, description="Petal width in cm")
@model_validator(mode='after')
def check_petal_sepal_lengths(self) -> Self:
"""
Check that petal length is not greater than sepal length.
"""
if self.petal_length >= self.sepal_length:
raise ValueError("Domain error: petal length cannot be greater than or equal to sepal length.")
return self
Key points about this code:
@model_validator(mode='after'): This decorator registers the method as a validator.mode='after'means it runs after Pydantic has validated and populated the individual fields. This ensuresself.petal_lengthandself.sepal_lengthare already validatedfloatvalues when we access them.check_petal_sepal_lengths(self) -> Self: The method receivesself, which is an instance of the model. This gives you access to all its fields.raise ValueError(...): If your custom rule is violated, you raise aValueError. FastAPI will intercept this and, just as before, return a422error with your custom message.return self: If validation passes, you must return the model instance.
To dive deeper into the mechanics of custom validators, the following video offers a comprehensive explanation.
Python Pydantic Tutorial: Complete Data Validation Course (Used by FastAPI)
Corey Schafer's Pydantic course provides an excellent, in-depth look at creating custom validators. This section covers both @field_validator and @model_validator, explaining their purpose and syntax.
About 49 minutes in, watch the section on custom validators. This segment will solidify your understanding by demonstrating how to write validator functions, raise errors, and use different validation modes like 'before' and 'after'.
3. Testing Your Robust API
With your new validator in place, it's time to test it.
- Restart your Uvicorn server to load the updated code.
- Open your browser to the Swagger UI at
http://127.0.0.1:8000/docs. - Navigate to your
POST /predictendpoint and click "Try it out". - Test the validation logic by providing an input that should fail, for example:
{ "sepal_length": 5.0, "sepal_width": 3.0, "petal_length": 6.0, "petal_width": 2.0 } - Execute the request.
You should receive a 422 Unprocessable Entity response. In the response body, you will see your custom error message: "Domain error: petal length cannot be greater than or equal to sepal length."
You have successfully hardened your API against invalid domain-specific inputs!
Conclusion
In this lesson, you've significantly improved the reliability of your machine learning API. By adding custom validation, you ensure that only clean, sensible data reaches your model, preventing unexpected errors and improving the quality of your predictions.
Key Takeaways:
- Default type validation is often insufficient for ML models; domain-specific validation is crucial.
- Pydantic's
Fieldfunction is a simple way to add constraints like value ranges (gt,lt, etc.) to single fields. - The
@field_validatordecorator allows for custom validation logic on individual fields. - The
@model_validatordecorator is used for complex rules that require checking multiple fields at once. - Raising a
ValueErrorwithin a validator signals a failure, which FastAPI automatically translates into a user-friendly422error response.
In our next lesson, we will continue to build a more resilient API. We'll ask the question: what happens if the input data passes all our validation checks, but the model.predict() call itself fails for some reason? We will learn to implement specific error handling for exceptions that may occur during model inference.