Django under the Hood - Validation

Speaker: Loïc Bistuer: core dev, works mostly on forms and ORM

Concerns

  1. Enforcement
  2. User experience
  3. Performance
  4. Convenience

… and the tradeoffs.

Where to validate data?

  1. Frontend:

    • Good UX
    • Works offline
    • Fast
  2. Django view

    • Not designed for the task
    • Easy to circumvent
  3. Forms/DRF serializers

    • Designed for the task
    • Easy to circumvent accidentally - will be bypassed at one point or another
    • Requires discipline
    • Serializers > Forms, but won't include Django Admin
  4. Model

    • Designed for the task
    • Doesn't run by default (only called by ModelForm/ModelSerializer)
    • By default only runs partially when called by form
    • Enforce by calling full_clean() in save()
    • But won't work with
    • update()
    • custom save methods
    • bulk_create()
    • migrations
    • Redundant with higher-level validation in forms and lower-level database
    • Breaks expectations
  5. Database

    • Designed for the task
    • Always enforced
    • Fast, and works with bulk_create
    • But: backend specific awfully soon
    • Harder to write
    • Harder to manage

Field Validation

  • Actual type validation is performed in to_python
  • Fields also have a list of validators, in addition to its default_validators (which cannot be removed)
  • For more fancy validation, use validator classes with a __call__() method
  • Add @deconstructible and __eq__() for migrations compatibility
  • Document using partials with those classes
  • Fields have error_messages as a dict, in __init__()
  • Validation cycle: Field.clean()
    • to_python()
    • validate
    • run_validators()
  • Validators can be reusable

Validation Error

  • Takes message, code and params
  • Message may be an element (string or ValidationError), list of errors (or a ValidationError of lists) or a dict of fields and their errors (or ValidationErrors)
  • Raising an error
    • Wrap a string with ugettext
    • Add an error code (e.g. 'invalid'), for customization
    • Add params dict instead of formatting your message directly, and format message like '%(something)'

Triggering validation

  • Form.is_valid()
  • Form.errors
  • Form.full_clean() - will always reset validation state and re-run, losing state. Override for composite magic.
    • calls _clean fields()
    • calls field.clean() per field
    • collects all error messages, running through all fields in the process
  • Form._post_clean(): Used by ModelForm

Form validation utils

  • Form.add_error(self, field, error) normalizes error input and provides an error class
  • Form.has_errors(self, field, code)
  • Raising a validation error is less verbous and more clear, but it's a matter of personal taste
  • Use error codes!

Form.errors (class ErrorDict)

  • as_ul()
  • as_text()
  • as_data() - original form
  • as_json()

Form.errors['some_field'] (class ErrorList(UserList, list))

  • It's a list with magic methods.
  • as_data
  • get_json_data - the dict
  • as_json - the json string

Model Validation

  • Very similar
  • Trigger it by calling Model.full_clean(), catch the errors
  • You can exclude fields
  • Doesn't have a state
  • Directly manipulates model instance instead of using a clean_data
  • Validates unique constraints
  • Any field that has failed validation is excluded from uniqueness validation, even and especially composite

ModelForm validation

  • Adds a _post_clean() method
  • Excludes failed fields, fields not on the form
  • Calls full_clean() without uniqueness validation
  • Calls _validate_unique() separately

Closing

  • Validate on frontend, if you can afford it
  • Validate on database for critical stuff
  • In between: choose your poison, and stick to it, there is no perfect solution
  • DRF Serializers do a lot of things well, better than either models or forms
  • Loic would like fat models, spanning more than a table where needed