Validators
mydborm provides 6 built-in validators that attach directly to field definitions.
Validation runs automatically on every create() and update() call.
Import validators
from mydborm import (
EmailValidator,
UrlValidator,
RegexValidator,
RangeValidator,
MinLengthValidator,
ChoiceValidator,
ValidationRule, # base class for custom validators
)
EmailValidator
Validates email address format using RFC-compliant regex.
from mydborm import BaseModel, IntField, StrField, EmailValidator
class Contact(BaseModel):
__tablename__ = "contacts"
id = IntField(primary_key=True)
name = StrField(max_length=100, nullable=False)
email = StrField(max_length=255, nullable=False,
validators=[EmailValidator()])
# Valid emails
Contact.create(name="Alice", email="alice@example.com") # OK
Contact.create(name="Bob", email="bob.smith+tag@domain.co.uk") # OK
Contact.create(name="Carol", email="carol@sub.domain.org") # OK
# Invalid emails
try:
Contact.create(name="Dave", email="notanemail")
except ValueError as e:
print(e)
# Field 'email' must be a valid email address. Got: 'notanemail'
try:
Contact.create(name="Eve", email="missing@domain")
except ValueError as e:
print(e)
# Field 'email' must be a valid email address. Got: 'missing@domain'
try:
Contact.create(name="Frank", email="@nodomain.com")
except ValueError as e:
print(e)
# Field 'email' must be a valid email address. Got: '@nodomain.com'
Pattern used: ^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$
None passes: email = None is valid when nullable=True.
UrlValidator
Validates URL format — requires http:// or https:// prefix.
from mydborm import UrlValidator
class Website(BaseModel):
__tablename__ = "websites"
id = IntField(primary_key=True)
url = StrField(max_length=500, nullable=False,
validators=[UrlValidator()])
# Valid URLs
Website.create(url="https://example.com") # OK
Website.create(url="http://example.com/path?query=1") # OK
Website.create(url="https://sub.domain.org/page#anchor") # OK
# Invalid URLs
try:
Website.create(url="example.com") # missing https://
except ValueError as e:
print(e) # Field 'url' must be a valid URL. Got: 'example.com'
try:
Website.create(url="ftp://example.com") # only http/https allowed
except ValueError as e:
print(e) # Field 'url' must be a valid URL. Got: 'ftp://example.com'
try:
Website.create(url="not a url at all")
except ValueError as e:
print(e) # Field 'url' must be a valid URL. Got: 'not a url at all'
RegexValidator
Validates a value matches a custom regular expression pattern.
from mydborm import RegexValidator
class Product(BaseModel):
__tablename__ = "products"
id = IntField(primary_key=True)
sku = StrField(max_length=20, nullable=False, validators=[
RegexValidator(
r'^[A-Z]{2,4}-\d{4}$',
message="SKU must be 2-4 uppercase letters, dash, 4 digits. E.g. PROD-0001"
)
])
hex_color = StrField(max_length=7, nullable=True, validators=[
RegexValidator(r'^#[0-9A-Fa-f]{6}$')
])
phone = StrField(max_length=20, nullable=True, validators=[
RegexValidator(
r'^\+?[\d\s\-\(\)]{7,20}$',
message="Invalid phone number format"
)
])
# Valid
Product.create(sku="PROD-0001", hex_color="#FF5733") # OK
Product.create(sku="AB-1234", hex_color="#000000") # OK
Product.create(sku="WXYZ-9999", hex_color=None) # OK — nullable
# Invalid
try:
Product.create(sku="prod-0001") # lowercase not allowed
except ValueError as e:
print(e)
# SKU must be 2-4 uppercase letters, dash, 4 digits. E.g. PROD-0001
try:
Product.create(sku="PROD-0001", hex_color="red") # not hex format
except ValueError as e:
print(e)
# Field 'hex_color' does not match pattern '^#[0-9A-Fa-f]{6}$'. Got: 'red'
Constructor:
RegexValidator(
pattern: str, # regex pattern string
message: str = None, # custom error message (optional)
)
RangeValidator
Validates a numeric value is within a minimum and maximum range.
from mydborm import RangeValidator, IntField, FloatField
class Survey(BaseModel):
__tablename__ = "surveys"
id = IntField(primary_key=True)
rating = IntField(nullable=False,
validators=[RangeValidator(min_val=1, max_val=5)])
price = FloatField(nullable=False,
validators=[RangeValidator(min_val=0.01, max_val=99999.99)])
discount = IntField(nullable=True,
validators=[RangeValidator(min_val=0, max_val=100)])
age = IntField(nullable=False,
validators=[RangeValidator(min_val=13)]) # min only
score = FloatField(nullable=True,
validators=[RangeValidator(max_val=100.0)]) # max only
# Valid
Survey.create(rating=5, price=29.99, discount=10, age=25) # OK
Survey.create(rating=1, price=0.01, age=18) # OK — min boundary
# Invalid
try:
Survey.create(rating=6, price=10.0, age=20) # rating > 5
except ValueError as e:
print(e)
# Field 'rating' must be <= 5. Got: 6
try:
Survey.create(rating=3, price=-1.0, age=20) # negative price
except ValueError as e:
print(e)
# Field 'price' must be >= 0.01. Got: -1.0
try:
Survey.create(rating=3, price=10.0, age=10) # underage
except ValueError as e:
print(e)
# Field 'age' must be >= 13. Got: 10
Constructor:
RangeValidator(
min_val = None, # minimum value (inclusive)
max_val = None, # maximum value (inclusive)
)
Both min_val and max_val are optional — use one or both.
MinLengthValidator
Validates a string meets a minimum character length.
from mydborm import MinLengthValidator
class UserAccount(BaseModel):
__tablename__ = "user_accounts"
id = IntField(primary_key=True)
username = StrField(max_length=30, nullable=False,
validators=[MinLengthValidator(3)])
password = StrField(max_length=255, nullable=False,
validators=[MinLengthValidator(8)])
bio = StrField(max_length=500, nullable=True,
validators=[MinLengthValidator(10)])
company = StrField(max_length=100, nullable=True,
validators=[MinLengthValidator(2)])
# Valid
UserAccount.create(username="alice", password="strongpass123") # OK
UserAccount.create(username="bob", password="p@ssw0rd!") # OK
# Invalid
try:
UserAccount.create(username="ab", password="validpass") # too short
except ValueError as e:
print(e)
# Field 'username' must be at least 3 characters. Got: 2
try:
UserAccount.create(username="carol", password="short") # < 8 chars
except ValueError as e:
print(e)
# Field 'password' must be at least 8 characters. Got: 5
Constructor:
ChoiceValidator
Validates a value is one of a fixed set of allowed choices.
from mydborm import ChoiceValidator
STATUSES = ["pending", "processing", "shipped", "delivered", "cancelled", "refunded"]
PRIORITIES = ["low", "medium", "high", "critical"]
SIZES = ["XS", "S", "M", "L", "XL", "XXL"]
REGIONS = ["NA", "EU", "APAC", "LATAM", "MEA"]
class Order(BaseModel):
__tablename__ = "orders"
id = IntField(primary_key=True)
status = StrField(max_length=20, nullable=False, default="pending",
validators=[ChoiceValidator(STATUSES)])
priority = StrField(max_length=10, nullable=False, default="medium",
validators=[ChoiceValidator(PRIORITIES)])
region = StrField(max_length=10, nullable=True,
validators=[ChoiceValidator(REGIONS)])
# Valid
Order.create(status="pending", priority="high") # OK
Order.create(status="shipped", priority="low", region="EU") # OK
# Invalid
try:
Order.create(status="unknown", priority="medium")
except ValueError as e:
print(e)
# Field 'status' must be one of ['pending', 'processing', 'shipped',
# 'delivered', 'cancelled', 'refunded']. Got: 'unknown'
try:
Order.create(status="pending", priority="CRITICAL") # case sensitive!
except ValueError as e:
print(e)
# Field 'priority' must be one of ['low', 'medium', 'high', 'critical']. Got: 'CRITICAL'
Use ChoiceValidator vs EnumField
ChoiceValidator validates at the Python level — the DB stores a plain VARCHAR.
EnumField validates AND creates a MySQL ENUM column — stricter at the DB level.
Use ChoiceValidator when you might add/remove choices without a migration.
Use EnumField when you want DB-level enforcement too.
Combining multiple validators
Attach multiple validators to one field — they run in order, stopping at the first failure:
from mydborm import MinLengthValidator, RegexValidator, ChoiceValidator
class BlogPost(BaseModel):
__tablename__ = "blog_posts"
id = IntField(primary_key=True)
slug = StrField(max_length=100, nullable=False, validators=[
MinLengthValidator(3),
RegexValidator(
r'^[a-z0-9\-]+$',
message="Slug may only contain lowercase letters, numbers, and hyphens"
),
])
status = StrField(max_length=20, nullable=False, validators=[
ChoiceValidator(["draft", "review", "published", "archived"]),
])
# Valid
BlogPost.create(slug="my-first-post", status="draft") # OK
# Fails MinLengthValidator first
try:
BlogPost.create(slug="ab", status="draft")
except ValueError as e:
print(e) # Field 'slug' must be at least 3 characters. Got: 2
# Passes MinLength, fails Regex
try:
BlogPost.create(slug="My Post Title", status="draft")
except ValueError as e:
print(e) # Slug may only contain lowercase letters, numbers, and hyphens
Cross-field validation
Use __validators__ on the model class for rules that span multiple fields:
class Discount(BaseModel):
__tablename__ = "discounts"
id = IntField(primary_key=True)
code = StrField(max_length=20, nullable=False)
min_order = FloatField(nullable=True)
max_uses = IntField(nullable=True)
percentage = FloatField(nullable=False,
validators=[RangeValidator(min_val=0.01, max_val=100.0)])
expires_at = StrField(max_length=20, nullable=True)
__validators__ = [
# min_order must be positive if set
lambda data: (_ for _ in ()).throw(
ValueError("min_order must be a positive number")
) if data.get("min_order") is not None and data["min_order"] <= 0 else None,
# max_uses must be positive if set
lambda data: (_ for _ in ()).throw(
ValueError("max_uses must be at least 1")
) if data.get("max_uses") is not None and data["max_uses"] < 1 else None,
]
# Valid
Discount.create(code="SAVE10", percentage=10.0, min_order=50.0, max_uses=100)
# Fails cross-field validator
try:
Discount.create(code="BAD", percentage=10.0, min_order=-5.0)
except ValueError as e:
print(e) # min_order must be a positive number
Custom validators
Create your own validator by subclassing ValidationRule:
from mydborm.fields import ValidationRule
class UKPostcodeValidator(ValidationRule):
"""Validates UK postcode format e.g. SW1A 1AA"""
import re as _re
PATTERN = _re.compile(
r'^[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}$',
_re.IGNORECASE
)
def validate(self, value, field_name: str):
if value is not None and not self.PATTERN.match(str(value)):
raise ValueError(
f"Field '{field_name}' must be a valid UK postcode. "
f"Examples: SW1A 1AA, EC1A 1BB. Got: {value!r}"
)
class CreditCardValidator(ValidationRule):
"""Luhn algorithm check for credit card numbers"""
def validate(self, value, field_name: str):
if value is None:
return
digits = str(value).replace(" ", "").replace("-", "")
if not digits.isdigit() or len(digits) < 13:
raise ValueError(f"Field '{field_name}' is not a valid card number.")
total = 0
for i, d in enumerate(reversed(digits)):
n = int(d)
if i % 2 == 1:
n *= 2
if n > 9:
n -= 9
total += n
if total % 10 != 0:
raise ValueError(f"Field '{field_name}' failed Luhn check.")
class Address(BaseModel):
__tablename__ = "addresses"
id = IntField(primary_key=True)
postcode = StrField(max_length=10, nullable=False,
validators=[UKPostcodeValidator()])
Address.create(postcode="SW1A 1AA") # OK
Address.create(postcode="EC1A1BB") # OK — space optional
try:
Address.create(postcode="12345") # US zip — not valid UK
except ValueError as e:
print(e)
Validator reference
| Validator | Constructor | What it checks |
|---|---|---|
EmailValidator() |
no args | RFC email format |
UrlValidator() |
no args | http/https URL |
RegexValidator(pattern, message=None) |
pattern str | custom regex |
RangeValidator(min_val=None, max_val=None) |
numeric bounds | min ≤ value ≤ max |
MinLengthValidator(min_length) |
int | len(value) ≥ min |
ChoiceValidator(choices) |
list | value in choices |
ValidationRule (subclass) |
custom | anything |