Flask

Basic

from dataclasses import dataclass
from typing import Annotated, Optional

from flask import Flask, jsonify, request
from flask.typing import ResponseValue

from koda_validate import StringValidator, DataclassValidator, EmailPredicate
from koda_validate.serialization import to_serializable_errs

app = Flask(__name__)


@dataclass
class ContactForm:
    name: str
    message: str
    # `Annotated` `Validator`s are used if found
    email: Annotated[str, StringValidator(EmailPredicate())]
    subject: Optional[str] = None


@app.route("/contact", methods=["POST"])
def contact_api() -> tuple[ResponseValue, int]:
    result = DataclassValidator(ContactForm)(request.json)
    match result:
        case Valid(contact_form):
            print(contact_form)  # do something with the valid data
            return {"success": True}, 200
        case Invalid() as inv:
            return jsonify(to_serializable_errs(inv)), 400


if __name__ == "__main__":
    app.run()

Note

In the example above, ContactForm is a dataclass, so we use a DataclassValidator. We could have used a TypedDict and TypedDictValidator, or a NamedTuple and NamedTupleValidator, and the code would have been essentially the same.

Fuller Example (with Async)

import asyncio
from typing import Annotated, Optional, TypedDict

from flask import Flask, jsonify, request
from flask.typing import ResponseValue

from koda_validate import *
from koda_validate.serialization import SerializableErr, to_serializable_errs

app = Flask(__name__)


class Captcha(TypedDict):
    seed: Annotated[str, StringValidator(ExactLength(16))]
    response: Annotated[str, StringValidator(MaxLength(16))]


async def validate_captcha(captcha: Captcha) -> Optional[ErrType]:
    """
    after we validate that the seed and response on their own,
    we need to check our database to make sure the response is correct
    """

    async def pretend_check_captcha_service(seed: str, response: str) -> bool:
        await asyncio.sleep(0.01)  # pretend to call
        return seed == response[::-1]

    if await pretend_check_captcha_service(captcha["seed"], captcha["response"]):
        # everything's valid
        return None
    else:
        return SerializableErr({"response": "bad captcha response"})


class ContactForm(TypedDict):
    email: Annotated[str, StringValidator(EmailPredicate())]
    message: Annotated[str, StringValidator(MaxLength(500), MinLength(10))]
    captcha: Annotated[
        Captcha,
        # explicitly adding some extra validation
        TypedDictValidator(Captcha, validate_object_async=validate_captcha),
    ]


contact_validator = TypedDictValidator(ContactForm)


@app.route("/contact", methods=["POST"])
async def contact_api() -> tuple[ResponseValue, int]:
    result = await contact_validator.validate_async(request.json)
    match result:
        case Valid(contact_form):
            print(contact_form)
            return {"success": True}, 200
        case Invalid() as inv:
            return jsonify(to_serializable_errs(inv)), 400


# if you want a JSON Schema from a ``Validator``, there's `to_json_schema()`
# schema = to_json_schema(contact_validator)
# hook_into_some_api_definition(schema)


if __name__ == "__main__":
    app.run()