7.11 Request Validation
Table of Contents
Why Request Validation Matters
Request validation is the process of checking incoming data before your backend uses it. Every time a client sends data to your API, you must assume it might be:
- Missing required fields
- In the wrong format
- Too long or too short
- Malicious on purpose
If you skip validation, you risk:
- Crashes and confusing errors
- Security issues, like injection attacks
- Corrupted data in your database
- Unclear API behavior for clients
Always validate all client input before using it or storing it. Never trust data just because it comes from your own frontend.
In REST APIs, validation usually happens:
- At the edge of your application, where requests first arrive
- Before calling your business logic
- Before talking to your database or other services
You enforce rules that define what a "valid" request looks like, and you reject anything that does not match those rules.
Where Validation Happens in REST APIs
There are several common places to validate requests:
| Location | Typical purpose | Example |
|---|---|---|
| URL / Path parameters | Validate identifiers and resource paths | /users/{user_id} is an integer |
| Query parameters | Validate filters, pagination, search options | ?page=1&limit=20 |
| Headers | Validate auth tokens, content type, custom flags | Authorization, Content-Type |
| Request body (JSON, form) | Validate main data payload | User registration data |
| Files | Validate file type, size, and count | Image uploads |
In a typical REST endpoint like:
POST /users/{user_id}/posts?page=1&limit=20
Content-Type: application/json
{
"title": "Hello",
"content": "My first post"
}You might validate:
user_idis a positive integerpageandlimitare positive, with a max limitContent-Typeisapplication/jsontitleandcontentexist and have valid length
Types of Validation
Syntactic vs Semantic Validation
Two broad categories:
| Type | Question it answers | Example rule |
|---|---|---|
| Syntactic | "Is the data structurally correct?" | age must be an integer |
| Semantic | "Does the data make sense logically?" | start_date must be before end_date |
Both are important.
- Syntactic validation: Check types, formats, required fields, lengths.
- Semantic validation: Check relationships between fields, domain rules, database lookups.
Example
Request body:
{
"email": "user@example.com",
"age": 17,
"plan": "premium"
}Possible rules:
- Syntactic:
emailis a non-empty string that looks like an emailageis an integerplanis"free","basic", or"premium"- Semantic:
agemust be at least 18 for"premium"planemailmust be unique in your system
You usually do syntactic validation first, then semantic validation.
Common Validation Rules for REST APIs
Required and Optional Fields
Decide which fields are mandatory.
Example for user registration:
{
"email": "user@example.com",
"password": "secret123",
"name": "Alice"
}Possible rules:
emailandpasswordare requirednameis optional
Requests:
{ "email": "user@example.com" } // Invalid, missing password
{ "email": "user@example.com", "password": "secret123", "name": "Alice" } // ValidDo not silently ignore missing required fields. Return a clear error describing what is wrong.
Data Types
Typical JSON types you validate:
| Field | Expected type | Invalid examples |
|---|---|---|
age | integer | "30", 30.5, true |
price | number | "9.99", null (if required) |
is_active | boolean | "true", 1 |
tags | array of str | "tag1,tag2", { "name": "tag1" } |
If you expect integers in query params:
GET /products?limit=10
limit=abc should not be accepted.
String Length and Patterns
Examples:
username: 3 to 20 characters, only letters, numbers, underscorepassword: at least 8 characterstitle: max 255 characters
You can express these as rules:
3 <= len(username) <= 20re.match("^[a-zA-Z0-9_]+$", username)
Passwords often have extra constraints, for example:
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
Numeric Ranges
Limit numeric values:
agebetween 0 and 120limitbetween 1 and 100pricegreater than or equal to 0
Example:
{ "age": -5 } // Invalid
{ "age": 25 } // ValidFor pagination:
GET /items?page=1&limit=5000You might:
- Accept
page >= 1 - Force
limit <= 100and maybe default to 20
Always validate pagination input. It protects your server and database from very expensive queries.
Enumerations
Some fields only accept a limited set of values.
Examples:
status:"pending","processing","completed","canceled"role:"user","admin"
Requests:
{ "status": "waiting" } // Invalid
{ "status": "pending" } // ValidUsing enums makes your API predictable and easier to document.
Date and Time
Common rules:
- Format: ISO 8601, for example
"2026-08-27T14:30:00Z" - Date relationships:
start_date <= end_datedue_datemust be in the future
Example:
{
"start_date": "2026-08-27",
"end_date": "2026-08-26"
}
Invalid because start_date is after end_date.
Validating Different Parts of the Request
Path Parameters
For a URL like:
GET /users/{user_id}Typical rules:
user_idis an integeruser_idis positive
Examples:
/users/123valid/users/-10invalid or not allowed/users/abcinvalid
If user_id is not found in your database, this is usually not a validation error, but a 404 Not Found.
Query Parameters
You often use query parameters for:
- Pagination:
page,limit - Filtering:
status,category - Sorting:
sort_by,order
Example:
GET /tasks?status=completed&page=0&limit=200Possible validation:
statusin["pending", "completed"]pageis integer,page >= 1limitis integer,1 <= limit <= 100
If query params are optional, you usually:
- Apply defaults when they are missing
- Only validate them when present
Example default behavior:
pagedefault 1limitdefault 20
So /tasks behaves like /tasks?page=1&limit=20.
Headers
Important headers to validate:
Content-Type: matches the body format you expectAccept: sometimes used to negotiate response formatAuthorization: has the expected scheme and token format
Examples:
- If you expect JSON in the body:
- Accept:
Content-Type: application/json - Reject:
Content-Type: text/plain - For bearer tokens:
- Accept:
Authorization: Bearer <token> - Reject:
Authorization: Token123
Invalid headers usually result in 400 Bad Request or 415 Unsupported Media Type, and auth problems in 401 Unauthorized.
Request Body: JSON Example
If you define a CreateUser payload:
{
"email": "user@example.com",
"password": "secret123",
"age": 25
}Rules might be:
email: required, format looks like an emailpassword: required, min 8 charactersage: optional, but if present must be integer >= 13
Invalid examples:
{} // Missing email and password
{
"email": "not-an-email",
"password": "short"
}
{
"email": "user@example.com",
"password": "longenough",
"age": "twenty"
}In each case you should explain which field failed and why.
Files
When handling file uploads, validate:
- Maximum file size
- Allowed file types or extensions
- Maximum number of files
Example rules for profile picture:
- Only
image/jpegorimage/png - Maximum 5 MB
Invalid cases:
- A 20 MB image
- A
.exefile disguised as an image
For files, you usually combine:
- Header-based checks:
Content-Type - Name checks: file extension
- Deeper checks: inspecting the file content when necessary
Returning Validation Errors
Choosing HTTP Status Codes
Typical status codes for validation problems:
| Status code | Meaning | When to use |
|---|---|---|
| 400 | Bad Request | General invalid input, malformed JSON, wrong types |
| 401 | Unauthorized | Missing or invalid authentication information |
| 403 | Forbidden | Authenticated, but not allowed to do this |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Conflicting state, like duplicate unique value |
| 422 | Unprocessable Entity | Input is well-formed JSON but fails validation rules |
Many APIs use 400 for all client input errors. Others use 422 specifically for detailed validation errors.
Error Response Structure
Design a consistent error format. For example:
{
"detail": [
{
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"type": "value_error.email"
},
{
"loc": ["body", "password"],
"msg": "ensure this value has at least 8 characters",
"type": "value_error.any_str.min_length"
}
]
}Here:
loctells where the problem is:body,query,path,header, etc.msgexplains the issue in human words.typeis a machine-friendly error code.
Another simpler style:
{
"errors": {
"email": "Invalid email format",
"password": "Password must be at least 8 characters"
}
}Or:
{
"message": "Validation failed",
"errors": [
{ "field": "email", "error": "Invalid email format" },
{ "field": "password", "error": "Must be at least 8 characters" }
]
}Use a consistent error response format across your entire API. Clients should be able to parse and handle validation errors automatically.
Validation in Create vs Update Operations
Create (POST)
On create endpoints, you often:
- Require most fields
- Enforce strong validation
Example POST /users:
email: required, uniquepassword: requiredname: optional
Missing required fields should cause a failure.
Full Update (PUT)
With PUT, the client often sends a full representation of the resource. You usually:
- Require all important fields
- Interpret missing fields as "clear" or use explicit semantics
Example resource:
{
"name": "Alice",
"age": 25,
"is_active": true
}PUT payload:
{
"name": "Alice Updated",
"age": 26,
"is_active": false
}Validate as if it is a complete object.
Partial Update (PATCH)
With PATCH, the client sends only the fields it wants to change. Validation is trickier:
- Fields are optional
- Only validate fields that are present
- Combined result must still be valid
Example:
Current user:
{
"email": "user@example.com",
"age": 30
}PATCH request:
{
"age": 150
}Here:
emailis not present, so you do not revalidate itageis present, so you validate it and reject if out of range
Another PATCH:
{}This might be:
- Valid but no changes, or
- Rejected as "no fields to update", depending on your API design.
Business Rule Validation
After basic structure checks, you often validate business rules that involve:
- Database lookups
- Relationships between entities
- Constraints that cannot be expressed with simple types
Examples:
- Uniqueness
emailmust be unique.
Process:
- Validate format of
email - Check database to see if email already exists
- If yes, return
409 Conflictor a validation-like error
- Resource existence
project_idin payload must refer to an existing project.
If project does not exist:
- Return
404 Not Found - Or treat as a validation error for
project_id
- State-dependent rules
- User cannot cancel an order that is already shipped.
Steps:
- Validate
order_idformat - Load order from database
- Check order state
- If state is incompatible with requested action, return an error such as:
{
"message": "Order cannot be canceled in state 'shipped'"
}Idempotency and Validation
Idempotent methods like GET, PUT, and often DELETE should behave predictably. Validation helps ensure that:
- The same invalid request always fails in the same way
- The same valid request has consistent results
Example for a payment API with an idempotency key:
- If the first call fails due to validation, you should keep that failure result for the same key.
- Do not process it differently on a retry.
This is especially important in payment, booking, or order APIs.
Practical Examples
Below are language-agnostic examples of validation scenarios to help you think clearly about how to design them.
Example 1: Creating a Task
Endpoint:
POST /tasks
Content-Type: application/jsonRequest body:
{
"title": "Buy groceries",
"description": "Milk, eggs, bread",
"priority": 3,
"due_date": "2026-09-01"
}Validation rules:
title: required, string, 1 to 100 charactersdescription: optional, string, max 1000 characterspriority: optional, integer between 1 and 5, default 3due_date: optional, valid date, must not be in the past
Invalid request 1 (missing title):
{
"description": "Milk, eggs, bread"
}Response:
400 Bad Request{
"message": "Validation failed",
"errors": {
"title": "This field is required"
}
}Invalid request 2 (priority out of range):
{
"title": "Buy groceries",
"priority": 10
}Response:
422 Unprocessable Entity{
"errors": {
"priority": "Must be between 1 and 5"
}
}Example 2: Pagination Parameters
Endpoint:
GET /products?page=0&limit=500Rules:
pageinteger, min 1, default 1limitinteger, min 1, max 100, default 20
Invalid query:
page=0violates min 1limit=500violates max 100
Response:
400 Bad Request{
"errors": {
"page": "Must be >= 1",
"limit": "Must be between 1 and 100"
}
}Valid query with defaults:
GET /products
Server treats it as page=1&limit=20.
Example 3: Enum and Business Rule Combined
Endpoint:
PATCH /orders/123/status
Content-Type: application/jsonRequest:
{ "status": "shipped" }Rules:
statusenum:"pending","paid","shipped","delivered","canceled"- Business rule: You cannot go from
"pending"directly to"shipped"; it must be"paid"first.
Possible invalid cases:
- Invalid enum:
{ "status": "sending" }Return validation error:
{
"errors": {
"status": "Invalid value, must be one of: pending, paid, shipped, delivered, canceled"
}
}- Invalid state transition:
- Order is currently
"pending"in database. - Client sends
"shipped".
Here the value is syntactically valid but breaks a business rule. Return something like:
{
"message": "Order status cannot change from 'pending' to 'shipped'"
}
You might use 400 Bad Request or 409 Conflict depending on your API style.
Designing Validation Rules Up Front
Before implementing an endpoint, define its validation rules clearly. For each field, decide:
| Field name | Location | Required? | Type | Rules / constraints |
|---|---|---|---|---|
title | body | yes | string | 1 to 100 characters |
description | body | no | string | max 1000 characters |
priority | body | no | integer | 1 to 5, default 3 |
due_date | body | no | date | must not be in the past |
page | query | no | integer | min 1, default 1 |
limit | query | no | integer | min 1, max 100, default 20 |
id | path | yes | integer | positive |
Authorization | header | yes | string, Bearer | must contain valid token and correspond to a user |
This table-like approach makes it easy to:
- Implement validation
- Document your API clearly
- Share rules with frontend developers
Later, when you use tools like OpenAPI and libraries such as Pydantic or others in real code, these rules translate directly into schemas and types.
Best Practices Summary
- Validate all inputs, even from your own frontend.
- Separate syntactic and semantic validation in your thinking.
- Keep validation close to the request handling layer.
- Use meaningful HTTP status codes, especially
400,401,404,409,422. - Return structured, consistent error responses with clear messages.
- Enforce limits on strings, numbers, pagination, and file sizes.
- Use enums for values with limited options.
- Validate both path and query parameters, not only JSON bodies.
- Apply stricter rules on create operations, and careful partial validation on PATCH.
- Document your validation rules so clients know how to call your API correctly.
With these principles, your REST APIs become safer, more predictable, and much easier for other developers to use.
Views: 10
KAHIBARO