Skip to main content

Validation

Ryx validates data at two levels: field-level (automatic) and model-level (custom).

Field-Level Validationโ€‹

Built into field definitions:

class Product(Model):
name = CharField(
max_length=200,
min_length=3,
blank=False,
)
price = DecimalField(
max_digits=10,
decimal_places=2,
min_value=0,
max_value=999999,
)
status = CharField(
max_length=20,
choices=["draft", "published", "archived"],
)
email = CharField(max_length=254, validators=[EmailValidator()])
website = CharField(max_length=200, null=True, validators=[URLValidator()])

Built-in Validatorsโ€‹

ValidatorDescription
MaxLengthValidator(n)String/list length โ‰ค n
MinLengthValidator(n)String/list length โ‰ฅ n
MaxValueValidator(n)Value โ‰ค n
MinValueValidator(n)Value โ‰ฅ n
RangeValidator(min, max)Value in range
NotBlankValidator()Not empty string
NotNullValidator()Not None
RegexValidator(pattern)Matches regex
EmailValidator()Valid email format
URLValidator()Valid URL format
ChoicesValidator(values)Value in allowed list
UniqueValueValidator()Unique in table (DB-enforced)

Custom Validator (FunctionValidator)โ€‹

from ryx import FunctionValidator

def must_be_even(value):
if value % 2 != 0:
raise ValidationError("Value must be even")

class Product(Model):
code = CharField(
max_length=10,
validators=[FunctionValidator(must_be_even, message="Code must be even")],
)

Model-Level Validationโ€‹

Override clean() for cross-field validation:

class Event(Model):
start_date = DateTimeField()
end_date = DateTimeField()
status = CharField(max_length=20, choices=["draft", "published"])

async def clean(self):
errors = {}
if self.end_date and self.end_date < self.start_date:
errors["end_date"] = ["End date must be after start date"]
if self.status == "published" and not self.start_date:
errors["start_date"] = ["Published events need a start date"]
if errors:
raise ValidationError(errors)

Running Validationโ€‹

product = Product(name="x", price=-1)

# Manual
try:
await product.full_clean()
except ValidationError as e:
print(e.errors)
# โ†’ {
# "name": ["Ensure this value has at least 3 characters."],
# "price": ["Ensure this value is greater than or equal to 0."],
# }

# Automatic โ€” save() calls full_clean() by default
await product.save() # Raises ValidationError

# Skip validation (for bulk ops)
await product.save(validate=False)

Merging Validation Errorsโ€‹

from ryx import ValidationError

try:
await product.full_clean()
except ValidationError as e:
# e.errors โ†’ {"name": ["Too short"], "price": ["Negative"]}
pass

# Merge multiple errors
errors = ValidationError({"name": ["Invalid"]})
errors.merge(ValidationError({"price": ["Out of range"]}))
# errors.errors โ†’ {"name": ["Invalid"], "price": ["Out of range"]}

Collecting All Errorsโ€‹

full_clean() collects ALL errors from ALL fields before raising โ€” you get the complete picture at once:

try:
await product.full_clean()
except ValidationError as e:
for field, errors in e.errors.items():
for error in errors:
print(f"{field}: {error}")

Next Stepsโ€‹

โ†’ Signals โ€” Lifecycle event observers