Skillversitas

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=True with a default never 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 StringField that is required and given "" passes presence but fails min_length=1. Add an explicit check in clean if empty should mean missing.
  • Validation is per-document: there is no built-in "unique among siblings" beyond the unique=True index (which is enforced by the database, not validate()).

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.