Django under the Hood - Validation
Speaker: Loïc Bistuer: core dev, works mostly on forms and ORM
Concerns
- Enforcement
- User experience
- Performance
- Convenience
… and the tradeoffs.
Where to validate data?
-
Frontend:
- Good UX
- Works offline
- Fast
-
Django view
- Not designed for the task
- Easy to circumvent
-
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
-
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()
insave()
- 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
-
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
andparams
- 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)'
- Wrap a string with
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
- calls
-
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