Field Validation
Validation in MongoEngine runs when a document is saved (or when you call validate() explicitly): required fields must be present, values must match their declared constraints, and your own clean() hook can enforce rules that span fields. The result on failure is a single ValidationError whose errors dictionary reports every problem at once - not just the first - which is what makes it directly usable for form feedback.
The Built-In Checks
from mongoengine import Document, StringField, IntField, ValidationError
class User(Document):
username = StringField(required=True, min_length=3, max_length=30,
regex=r"^[a-z0-9_]+$")
age = IntField(min_value=0, max_value=130)
try:
User(username="ab", age=200).validate()
except ValidationError as err:
print(err.errors)
# {'username': 'String must have length between 3 and 30',
# 'age': 'Int must be between 0 and 130'}
Every constraint from the field-types chapter participates: required, min_length/max_length, min_value/max_value, regex, choices, and list/element constraints on ListField. save() calls the same validation, so an invalid document cannot reach the database through the ODM - but note that this is application-side enforcement; raw writes bypass it.
Cross-Field Rules with clean()
from mongoengine import Document, StringField, DateTimeField, ValidationError
import datetime
class Booking(Document):
room = StringField(required=True)
starts = DateTimeField(required=True)
ends = DateTimeField(required=True)
def clean(self):
super().clean() # field-level checks first
if self.ends <= self.starts:
raise ValidationError("ends must be after starts")
if self.starts < datetime.datetime.now(datetime.timezone.utc):
raise ValidationError("bookings cannot start in the past")
clean() is called before field validation on every validate()/save(). It is the place for rules that need two fields together - date ranges, mutually exclusive options, "at least one of these" - and for raising a ValidationError with a message that belongs to the whole document (ValidationError("message")) or a specific field (ValidationError(myfield="message")).
Validating Without Saving
draft = User(username="alice_42", age=30)
draft.validate() # raises ValidationError if invalid
# a reusable "is this ok?" helper
def is_valid(doc):
try:
doc.validate()
return True, {}
except ValidationError as err:
return False, err.errors
API layers usually validate the incoming payload before construction, but validate() is the single source of truth when you need a pre-save check - for example, validating a document loaded from the database before a transformation, or reporting all form errors at once instead of failing on the first.
Gotchas
required=Truewith adefaultnever fails: the default is applied at instantiation, so the field is always present. Required-and-defaulted is redundant, not broken.- Empty strings are not None: a
StringFieldthat is required and given""passes presence but failsmin_length=1. Add an explicit check incleanif empty should mean missing. - Validation is per-document: there is no built-in "unique among siblings" beyond the
unique=Trueindex (which is enforced by the database, notvalidate()).
Summary
Declare constraints on the fields, put cross-field rules in clean(), and let save() or an explicit validate() raise one ValidationError that carries all failures in err.errors. Use the errors dictionary for user-facing messages, remember the checks run in Python at save time, and pair unique=True with real indexes - the model validates, the index enforces.