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
| Validator | Description |
|---|---|
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 |
RegexValidator(pattern) | Matches regex |
EmailValidator() | Valid email format |
URLValidator() | Valid URL format |
ChoicesValidator(values) | Value in allowed list |
NotNullValidator() | Not None |
UniqueValueValidator() | Unique in table |
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)
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