How to Validate Request Body JSON Against a Schema in Python

Build a lightweight schema validator to check required fields, types, string lengths, allowed values, and nested objects in a JSON request body.

Medium Python 3.9+ Aug 9, 2026 API design & gRPC 15 views 0 copies

Python code

71 lines
Python 3.9+
import json


def validate_against_schema(data, schema, path=""):
    errors = []

    if not isinstance(data, dict):
        errors.append(f"{path}: expected object, got {type(data).__name__}")
        return errors

    for field, rules in schema.items():
        field_path = f"{path}.{field}" if path else field

        if field not in data:
            if rules.get("required", False):
                errors.append(f"{field_path}: missing required field")
            continue

        value = data[field]
        expected_type = rules.get("type")

        if expected_type:
            if not isinstance(value, expected_type):
                errors.append(
                    f"{field_path}: expected {expected_type.__name__}, got {type(value).__name__}"
                )
                continue

        if expected_type is str and "min_length" in rules:
            if len(value) < rules["min_length"]:
                errors.append(
                    f"{field_path}: length {len(value)} < {rules['min_length']}"
                )

        if "allowed" in rules:
            if value not in rules["allowed"]:
                errors.append(f"{field_path}: value must be one of {rules['allowed']}")

        if expected_type is dict and "properties" in rules:
            nested_errors = validate_against_schema(
                value, rules["properties"], field_path
            )
            errors.extend(nested_errors)

    return errors


if __name__ == "__main__":
    schema = {
        "username": {"type": str, "required": True, "min_length": 3},
        "email": {"type": str, "required": True},
        "role": {"type": str, "required": True, "allowed": ["admin", "user", "guest"]},
        "age": {"type": int, "required": False},
        "address": {
            "type": dict,
            "required": False,
            "properties": {
                "city": {"type": str, "required": True},
                "zip": {"type": str, "required": True}
            }
        }
    }

    request_body = {
        "username": "alice",
        "email": "alice@example.com",
        "role": "admin"
    }

    result = validate_against_schema(request_body, schema)
    print("Valid" if not result else "Errors: " + json.dumps(result))

Output

stdout
Valid

How it works

The function walks the schema recursively, tracking field paths for clear error messages. For each field, it checks presence, type, and optional constraints like min_length or allowed values. Nested dicts recurse into their own properties, accumulating errors across all levels. Using isinstance maps cleanly to Python's built-in types, so the schema reads like a plain dictionary instead of a third-party library.

Common mistakes

  • Using json.loads on the body and then passing it directly without checking it is a dict first.
  • Forgetting that booleans are subclasses of int, so isinstance(True, int) is True.
  • Not skipping required checks when the field is absent from optional nested objects.

Variations

  1. Use the third-party jsonschema library with draft-07 JSON Schema syntax for standards-compliant validation.
  2. Validate with Pydantic models for automatic type coercion and nested schema handling.

Real-world use cases

  • FastAPI or Flask route handlers validating incoming POST payloads before database insert.
  • API gateway middleware rejecting malformed requests early in the request lifecycle.
  • Internal microservice endpoints verifying event payloads before dispatching to event consumers.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from API design & gRPC

Related tutorials and quizzes for this topic.