Examples ======== These are self-contained examples showing validatedata solving real problems. Each one reflects a workflow you might actually have — no boilerplate, no contrived data. ---- Fast validation with `validator()` ------------------------------------- When you only need a **boolean pass/fail** result (no error messages), use `validator()`. It compiles a rule into a callable that returns `True` or `False` with minimal overhead. Its faster than Pydantic v2 and msgspec on invalid data dicts. The performance advantage of the *validator* function on invalid data comes from early‑exit optimisations. Other libraries often have to validate everything and report errors. .. code-block:: python from validatedata import validator # Single value – pipe syntax is_valid_username = validator('str|min:3|max:32') is_valid_username('alice') # True is_valid_username('a') # False # Multiple fields – flat dict rule validate_user = validator({ 'username': 'str|min:3|max:32', 'email': 'email', 'age': 'int|min:18' }) validate_user({'username': 'bob', 'email': 'bob@example.com', 'age': 25}) # True validate_user({'username': 'bob', 'email': 'bob@example.com', 'age': 15}) # False # Parameterized containers is_str_list = validator('list[str]') is_str_list(['a', 'b', 'c']) # True is_str_list(['a', 1, 'c']) # False is_str_or_int_list = validator('list[str,int]') is_str_or_int_list(['a', 1, 'c']) # True User registration ----------------- A typical sign-up form: username, email, password with strength requirements, and an optional phone number. .. code-block:: python from validatedata import validate_data rule = { 'username': 'str|strip|min:3|max:32|re:^[\\w.-]+$|msg:username must be 3–32 characters, letters, digits, dots, or hyphens only', 'email': 'email|msg:please enter a valid email address', 'password': 'str|min:8|re:(?=.*[A-Z])(?=.*\\d).+|msg:password must be at least 8 characters with one uppercase letter and one digit', 'phone': 'phone|nullable', } result = validate_data( data={ 'username': 'alice_99', 'email': 'alice@example.com', 'password': 'Secure123', 'phone': None, }, rule=rule, ) if result.ok: print('registration accepted') else: # errors are grouped per field — easy to map back to form inputs for group in result.errors: if group: print(group[0]) The ``phone`` field is ``nullable`` so submitting the form without it passes. Everything else is required and validated in a single call. ---- Flask route with the decorator ------------------------------- Validate incoming JSON before your route body runs. On failure the decorator returns the error dict directly — you just need to check for it. .. code-block:: python from flask import Flask, request, jsonify from validatedata import validate, ValidationError app = Flask(__name__) signup_rule = { 'username': 'str|strip|min:3|max:32', 'email': 'email', 'password': 'str|min:8|re:(?=.*[A-Z])(?=.*\\d).+', } @app.route('/signup', methods=['POST']) def signup(): body = request.get_json() result = validate_data(body, signup_rule) if not result.ok: return jsonify({'errors': result.errors}), 422 # body is clean — proceed user = create_user(body['username'], body['email'], body['password']) return jsonify({'id': user.id}), 201 Or register a Flask error handler and use ``raise_exceptions=True`` to keep the route body completely free of validation logic: .. code-block:: python from validatedata import ValidationError @app.errorhandler(ValidationError) def handle_validation_error(e): return jsonify({'errors': str(e)}), 422 @app.route('/signup', methods=['POST']) @validate(signup_rule, raise_exceptions=True) def signup(username, email, password): user = create_user(username, email, password) return jsonify({'id': user.id}), 201 ---- Application config file ----------------------- Validate a config dict loaded from YAML, TOML, or environment variables before your app starts. Mirror-structure rules match the shape of the config exactly — no structural boilerplate required. .. code-block:: python import yaml from validatedata import validate_data with open('config.yaml') as f: config = yaml.safe_load(f) # config.yaml looks like: # # app: # name: MyService # version: 1.4.0 # debug: false # # database: # host: 127.0.0.1 # port: 5432 # name: mydb # # server: # host: 0.0.0.0 # port: 8080 rule = { 'app': { 'name': 'str|min:1', 'version': 'semver', 'debug': 'bool', }, 'database': { 'host': 'ip', 'port': 'int|between:1,65535', 'name': 'str|min:1', }, 'server': { 'host': 'ip', 'port': 'int|between:1024,65535', }, } result = validate_data(data=config, rule=rule, mutate=True) if not result.ok: for error in result.errors: print(f'Config error: {error}') raise SystemExit('Invalid configuration — aborting startup') # result.data is a dict with the same shape as config — # use it directly so any transforms (e.g. strip on string fields) are applied app_config = result.data Bad config fails loudly at startup with a clear field path (e.g. ``database.port: invalid integer``) rather than surfacing as a cryptic runtime error later. Passing ``mutate=True`` means ``result.data`` gives you back the validated — and optionally transformed — config in exactly the same nested structure, ready to use without re-reading the original dict. ---- Bulk data import ---------------- Validate rows before writing them to a database. Collect all errors up front so you can report the bad rows without stopping at the first failure. .. code-block:: python from validatedata import validate_data row_rule = [ 'str|strip|min:1|max:128', # name 'email', # email 'int|min:0', # age 'str|in:active,inactive', # status ] rows = [ ['Alice', 'alice@example.com', 30, 'active'], ['', 'bob@example.com', 25, 'active'], # blank name ['Carol', 'not-an-email', 28, 'active'], # bad email ['Dave', 'dave@example.com', -1, 'pending'], # bad age, bad status ] bad_rows = [] for i, row in enumerate(rows): result = validate_data(row, row_rule) if not result.ok: bad_rows.append({'row': i + 1, 'errors': result.errors}) if bad_rows: for entry in bad_rows: print(f"Row {entry['row']}: {entry['errors']}") else: write_to_database(rows) Running through all rows before writing means you can return a full report to the user — not just the first bad row. ---- Conditional fields on a checkout form -------------------------------------- Delivery method determines which fields are required. ``depends_on`` skips validation on a field entirely when the condition isn't met. .. code-block:: python from collections import OrderedDict from validatedata import validate_data rule = { 'delivery_method': 'str|in:pickup,delivery', 'address': { 'type': 'str', 'range': (10, 'any'), 'depends_on': {'field': 'delivery_method', 'value': 'delivery'}, 'message': 'a delivery address is required', }, 'promo_code': { 'type': 'str', 'length': 8, 'nullable': True, 'message': 'promo code must be exactly 8 characters', }, } # pickup — address is skipped, promo code is optional result = validate_data( data=OrderedDict([ ('delivery_method', 'pickup'), ('address', None), ('promo_code', None), ]), rule=rule, ) result.ok # True # delivery without address — fails result = validate_data( data=OrderedDict([ ('delivery_method', 'delivery'), ('address', None), ('promo_code', None), ]), rule=rule, ) result.ok # False result.errors # [[], ['a delivery address is required', 'a delivery address is required'], []] ---- Normalising data before saving ------------------------------- Use transforms with ``mutate=True`` to clean user input in the same pass as validation. The function receives the cleaned values — no separate sanitisation step needed. .. code-block:: python from validatedata import validate @validate( rule={ 'username': 'str|strip|lower|min:3|max:32', 'bio': 'str|strip|max:280|nullable', 'website': 'url|nullable', }, mutate=True, ) def update_profile(username, bio, website): # username is already stripped and lowercased # bio is stripped, website is validated db.update(username=username, bio=bio, website=website) return 'profile updated' update_profile( username=' Alice_99 ', bio=' Building things. ', website='https://alice.dev', ) # saves username='alice_99', bio='Building things.' — whitespace stripped Input arrives messy, your function receives it clean. No intermediate variables, no separate call to ``.strip()`` or ``.lower()``. The same thing works with :func:`validate_data` directly. When the input is a dict, ``result.data`` is returned as a dict with the same keys — so you can use the cleaned values straight away without tracking positional order: .. code-block:: python from validatedata import validate_data result = validate_data( data={ 'username': ' Alice_99 ', 'bio': ' Building things. ', 'website': 'https://alice.dev', }, rule={ 'username': 'str|strip|lower|min:3|max:32', 'bio': 'str|strip|max:280|nullable', 'website': 'url|nullable', }, mutate=True, ) if result.ok: db.update(**result.data) # result.data == { # 'username': 'alice_99', # 'bio': 'Building things.', # 'website': 'https://alice.dev', # } ``result.data`` mirrors the shape of the input dict exactly — nested dicts are preserved, so a config-shaped input comes back as a config-shaped output.