Skip to content

Schema Validation for Requests — Complete Guide

DodaTech Updated 2026-06-28 5 min read

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

What is the difference between schema and validation?

Schema defines the structure. Validation compares data against the schema. Schema is the rule, validation is the check.

Should I validate on every request?

Yes. Every request that accepts input should be validated against a schema.

Can I compose schemas?

Yes. Reusable schemas (like address) can be composed into larger schemas (like user. address).

How do I handle optional fields?

Mark them as not required. Validate them if present, skip if absent.

What is a nullable field?

A field that can be null. Different from optional. An optional field can be absent; a nullable field can be null.

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