Schema Validation for Requests — Complete Guide
In this tutorial, you'll learn about Schema Validation. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Schema validation defines the structure, types, and constraints for request data. It ensures every field matches expectations before processing.
What You'll Learn
By the end of this lesson, you will define validation schemas, validate nested objects and arrays, and implement custom validation rules.
Why It Matters
Schema validation is the most common validation approach. It provides a declarative, reusable, and testable way to define valid input shapes.
Real-World Use
Joi and Zod are schema validation libraries used in thousands of production APIs. A typical user registration schema validates name (string, 3-50 chars), email (email format), and password (min 8 chars).
Schema Validation Flow
flowchart TD
Data[Input Data] --> Schema[Schema Definition]
Schema --> Compare[Compare Data to Schema]
Compare -->|Matches| Valid[Valid]
Compare -->|Mismatch| Invalid[Invalid]
Invalid --> Errors[Error Details]
Basic Schema Validator
# schema_validator.py
from typing import Any, Dict, List, Optional, Union
class FieldSchema:
def __init__(self, field_type: type, required: bool = False,
min_length: Optional[int] = None, max_length: Optional[int] = None,
min_value: Optional[Union[int, float]] = None,
max_value: Optional[Union[int, float]] = None,
pattern: Optional[str] = None):
self.field_type = field_type
self.required = required
self.min_length = min_length
self.max_length = max_length
self.min_value = min_value
self.max_value = max_value
self.pattern = pattern
class Schema:
def __init__(self):
self.fields: Dict[str, FieldSchema] = {}
def add(self, name: str, **kwargs):
self.fields[name] = FieldSchema(**kwargs)
def validate(self, data: Dict) -> List[str]:
errors = []
for name, schema in self.fields.items():
value = data.get(name)
if schema.required and value is None:
errors.append(f"{name}: required")
continue
if value is None:
continue
if not isinstance(value, schema.field_type):
errors.append(f"{name}: expected {schema.field_type.__name__}, got {type(value).__name__}")
continue
if schema.field_type == str:
if schema.min_length and len(value) < schema.min_length:
errors.append(f"{name}: min length {schema.min_length}")
if schema.max_length and len(value) > schema.max_length:
errors.append(f"{name}: max length {schema.max_length}")
if schema.field_type in (int, float):
if schema.min_value is not None and value < schema.min_value:
errors.append(f"{name}: min value {schema.min_value}")
if schema.max_value is not None and value > schema.max_value:
errors.append(f"{name}: max value {schema.max_value}")
return errors
user_schema = Schema()
user_schema.add("username", field_type=str, required=True, min_length=3, max_length=50)
user_schema.add("age", field_type=int, required=True, min_value=13, max_value=150)
user_schema.add("email", field_type=str, required=True)
user_schema.add("bio", field_type=str, max_length=500)
valid_data = {"username": "alice", "age": 30, "email": "a@x.com", "bio": "Hello"}
invalid_data = {"username": "ab", "age": 12, "email": 123}
print(f"Valid: {user_schema.validate(valid_data)}")
print(f"Invalid: {user_schema.validate(invalid_data)}")
Expected output:
Valid: []
Invalid: ['username: min length 3', 'age: min value 13', 'email: expected str, got int']
Nested Object Validation
# nested_schema.py
from typing import Any, Dict, List
class NestedSchema:
def __init__(self):
self.rules: Dict[str, Any] = {}
def add(self, field: str, field_type: type, required: bool = False,
nested: 'NestedSchema' = None):
self.rules[field] = {
"type": field_type,
"required": required,
"nested": nested,
}
def validate(self, data: Dict, prefix: str = "") -> List[str]:
errors = []
for field, rule in self.rules.items():
full_path = f"{prefix}.{field}" if prefix else field
value = data.get(field)
if rule["required"] and value is None:
errors.append(f"{full_path}: required")
continue
if value is None:
continue
if rule["nested"] and isinstance(value, dict):
errors.extend(rule["nested"].validate(value, full_path))
continue
if not isinstance(value, rule["type"]):
errors.append(f"{full_path}: expected {rule['type'].__name__}")
return errors
address_schema = NestedSchema()
address_schema.add("street", str, required=True)
address_schema.add("city", str, required=True)
address_schema.add("zip", str, required=True)
user_schema = NestedSchema()
user_schema.add("name", str, required=True)
user_schema.add("address", dict, required=True, nested=address_schema)
valid = {"name": "Alice", "address": {"street": "123 St", "city": "NYC", "zip": "10001"}}
invalid = {"name": "Alice", "address": {"street": "123 St"}}
print(f"Valid: {user_schema.validate(valid)}")
print(f"Invalid: {user_schema.validate(invalid)}")
Expected output:
Valid: []
Invalid: ['address.city: required', 'address.zip: required']
Array Validation
# array_validation.py
from typing import Any, Dict, List, Optional
class ArraySchema:
def __init__(self, item_type: type, min_items: int = 0, max_items: Optional[int] = None):
self.item_type = item_type
self.min_items = min_items
self.max_items = max_items
def validate(self, items: List, path: str = "items") -> List[str]:
errors = []
if not isinstance(items, list):
return [f"{path}: expected array"]
if len(items) < self.min_items:
errors.append(f"{path}: min {self.min_items} items")
if self.max_items and len(items) > self.max_items:
errors.append(f"{path}: max {self.max_items} items")
for i, item in enumerate(items):
if not isinstance(item, self.item_type):
errors.append(f"{path}[{i}]: expected {self.item_type.__name__}")
return errors
tags_schema = ArraySchema(str, min_items=1, max_items=5)
scores_schema = ArraySchema(int, min_items=1)
print(f"Tags valid: {tags_schema.validate(['api', 'graphql'])}")
print(f"Tags empty: {tags_schema.validate([])}")
print(f"Tags too many: {tags_schema.validate(['a', 'b', 'c', 'd', 'e', 'f'])}")
print(f"Tags wrong type: {tags_schema.validate([1, 2, 3])}")
Expected output:
Tags valid: []
Tags empty: ['items: min 1 items']
Tags too many: ['items: max 5 items']
Tags wrong type: ['items[0]: expected str', 'items[1]: expected str', 'items[2]: expected str']
Common Mistakes
1. Not Validating Types
Accepting strings where numbers are expected causes crashes later. Validate types early.
2. Ignoring Empty Strings
An empty string is not None. Validate both null and empty string for required fields.
3. No Boundary Validation
Accepting age=999 or username of 10,000 characters. Always validate min/max bounds.
4. Not Validating Enums
Accepting any status value when only "active", "inactive" are valid. Use enum validation.
5. Overly Complex Schemas
One massive schema is hard to maintain. Split into reusable sub-schemas.
Practice Questions
1. What is schema validation?
Defining a blueprint for valid data, specifying types, required fields, and constraints.
2. What fields should a user schema include?
username (string, 3-50), email (email format), password (min 8 chars), age (13-150).
3. How do you validate nested objects?
Use nested schemas that recursively validate sub-objects with their own field definitions.
4. How do you validate arrays?
Define item type, min/max items, and validate each item against the type.
Challenge
Build a schema validator for an e-commerce order that validates: items (array of {product_id, quantity, price}), shipping_address (nested object), payment (type, card details).
FAQ
Mini Project: Schema Builder
# schema_builder.py
from typing import Any, Dict, List
class SchemaBuilder:
def __init__(self):
self.fields = {}
def string(self, name: str, required=False, min_len=0, max_len=None):
self.fields[name] = {"type": str, "required": required, "min": min_len, "max": max_len}
return self
def integer(self, name: str, required=False, min_val=None, max_val=None):
self.fields[name] = {"type": int, "required": required, "min": min_val, "max": max_val}
return self
def validate(self, data: Dict) -> List[str]:
errs = []
for name, rule in self.fields.items():
val = data.get(name)
if rule["required"] and val is None:
errs.append(f"{name}: required")
elif val is not None:
if not isinstance(val, rule["type"]):
errs.append(f"{name}: type error")
elif rule["type"] == str and rule["min"] and len(val) < rule["min"]:
errs.append(f"{name}: too short")
return errs
schema = SchemaBuilder().string("name", required=True, min_len=2).integer("age", required=True)
print(schema.validate({"name": "Al", "age": "old"}))
print(schema.validate({"name": "Alice", "age": 30}))
Expected output:
['name: too short', 'age: type error']
[]
What's Next
You understand schema validation. Next, learn middleware validation, then input sanitization.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro