Declarative Django request validation for RESTful APIs
Project description
Django API Forms
Django Forms approach in the processing of a RESTful HTTP request payload (especially for content type like JSON or MessagePack) without HTML front-end.
Motivation
The main idea was to create a simple and declarative way to specify the format of expecting requests with the ability to validate them. Firstly, I tried to use Django Forms to validate my API requests (I use pure Django in my APIs). I have encountered a problem with nesting my requests without a huge boilerplate. Also, the whole HTML thing was pretty useless in my RESTful APIs.
I wanted to:
- define my requests as object (
Form), - pass the request to my defined object (
form = Form.create_from_request(request)), - validate my request
form.is_valid(), - extract data
form.clean_dataproperty.
I wanted to keep:
- friendly declarative Django syntax, (DeclarativeFieldsMetaclass is beautiful),
- Validators,
- ValidationError,
- Form fields (In the end, I had to "replace" some of them).
So I have decided to create a simple Python package to cover all my expectations.
Installation
# Using pip
pip install django-api-forms
# Using poetry
peotry add django-api-forms
# Using setup.py
python setup.py install
Optional:
# msgpack support (for requests with Content-Type: application/x-msgpack)
peotry add msgpack
# ImageField support
peotry add Pillow
Install application in your Django project by adding django_api_forms to yours INSTALLED_APPS:
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django_api_forms'
)
You can change the default behavior of population strategies or parsers using these settings (listed with default values). Keep in mind, that dictionaries are not replaced by your settings they are merged with defaults.
For more information about the parsers and the population strategies check the documentation.
DJANGO_API_FORMS_POPULATION_STRATEGIES = {
'django_api_forms.fields.FormFieldList': 'django_api_forms.population_strategies.IgnoreStrategy',
'django_api_forms.fields.FileField': 'django_api_forms.population_strategies.IgnoreStrategy',
'django_api_forms.fields.ImageField': 'django_api_forms.population_strategies.IgnoreStrategy',
'django_api_forms.fields.FormField': 'django_api_forms.population_strategies.IgnoreStrategy',
'django.forms.models.ModelMultipleChoiceField': 'django_api_forms.population_strategies.IgnoreStrategy',
'django.forms.models.ModelChoiceField': 'django_api_forms.population_strategies.ModelChoiceFieldStrategy'
}
DJANGO_API_FORMS_DEFAULT_POPULATION_STRATEGY = 'django_api_forms.population_strategies.BaseStrategy'
DJANGO_API_FORMS_PARSERS = {
'application/json': 'json.loads',
'application/x-msgpack': 'msgpack.loads'
}
Example
Simple nested JSON request
{
"title": "Unknown Pleasures",
"type": "vinyl",
"artist": {
"_name": "Joy Division",
"genres": [
"rock",
"punk"
],
"members": 4
},
"year": 1979,
"songs": [
{
"title": "Disorder",
"duration": "3:29"
},
{
"title": "Day of the Lords",
"duration": "4:48",
"metadata": {
"_section": {
"type": "ID3v2",
"offset": 0,
"byteLength": 2048
},
"header": {
"majorVersion": 3,
"minorRevision": 0,
"size": 2038
}
}
}
],
"metadata": {
"created_at": "2019-10-21T18:57:03+0100",
"updated_at": "2019-10-21T18:57:03+0100"
}
}
Django API Forms equivalent + validation
from enum import Enum
from django.core.exceptions import ValidationError
from django.forms import fields
from django_api_forms import FieldList, FormField, FormFieldList, DictionaryField, EnumField, AnyField, Form
class AlbumType(Enum):
CD = 'cd'
VINYL = 'vinyl'
class ArtistForm(Form):
class Meta:
mapping = {
'_name': 'name'
}
name = fields.CharField(required=True, max_length=100)
genres = FieldList(field=fields.CharField(max_length=30))
members = fields.IntegerField()
class SongForm(Form):
title = fields.CharField(required=True, max_length=100)
duration = fields.DurationField(required=False)
metadata = AnyField(required=False)
class AlbumForm(Form):
title = fields.CharField(max_length=100)
year = fields.IntegerField()
artist = FormField(form=ArtistForm)
songs = FormFieldList(form=SongForm)
type = EnumField(enum=AlbumType, required=True)
metadata = DictionaryField(fields.DateTimeField())
def clean_year(self):
if self.cleaned_data['year'] == 1992:
raise ValidationError("Year 1992 is forbidden!", 'forbidden-value')
return self.cleaned_data['year']
def clean(self):
if (self.cleaned_data['year'] == 1998) and (self.cleaned_data['artist']['name'] == "Nirvana"):
raise ValidationError("Sounds like a bullshit", code='time-traveling')
if not self._request.user.is_authenticated():
raise ValidationError("You can use request in form validation!")
return self.cleaned_data
"""
Django view example
"""
def create_album(request):
form = AlbumForm.create_from_request(request)
if not form.is_valid():
# Process your validation error
print(form.errors)
# Cleaned valid payload
payload = form.cleaned_data
print(payload)
If you want example with whole Django project, check out repository created by pawl django_api_forms_modelchoicefield_example, where he uses library with ModelChoiceField.
Running Tests
# install all dependencies
poetry install
# run code-style check
poetry run flake8 .
# run the tests
poetry run python runtests.py
Sponsorship
Navicat Premium is a super awesome database development tool for cool kids in the neighborhood that allows you to simultaneously connect to MySQL, MariaDB, MongoDB, SQL Server, Oracle, PostgreSQL, and SQLite databases from a single application. Compatible with cloud databases like Amazon RDS, Amazon Aurora, Amazon Redshift, Microsoft Azure, Oracle Cloud, Google Cloud and MongoDB Atlas. You can quickly and easily build, manage and maintain your databases.
Especially, I have to recommend their database design tool. Many thanks Navicat for supporting Open Source projects 🌈.
Made with ❤️ and ☕️ by Jakub Dubec, BACKBONE s.r.o. & contributors.
Changelog
0.21.0 : 15.12.2021
- Feature: Introduced
mapping - Feature: Override strategies using
field_type_strategyandfield_strategy
0.20.1 : 13.1.2022
- Fix:
DictionaryFieldwas unable to raise validation errors for keys
0.20.0 : 14.10.2021
Anniversary release 🥳
- Feature: Population strategies introduced
- Feature:
fillmethod is deprecated and replaced bypopulate - Feature:
Settingsobject introduced (form.settings) - Feature: Pluggable content-type parsers using
DJANGO_API_FORMS_PARSERSsetting
0.19.1 : 17.09.2021
- Typing:
mimeargument inFileFieldis supposed to be atuple
0.19.0 : 12.07.2021
- Feature:
FieldListandFormFieldListnow supports optional min/max constrains usingmin_length/max_length
0.18.0 : 16.04.2021
- Feature:
ModelFormclass introduced (experimental, initial support - not recommended for production)
0.17.0 : 24.02.2021
- Feature:
fill_methodintroduced
0.16.4 : 20.12.2020
- Fix: Pillow image object have to be reopened after
Image.verify()call inImageField::to_python
0.16.3 : 13.11.2020
- Fix:
ApiFormException('No clean data provided! Try to call is_valid() first.')was incorrectly raised if request payload was empty duringForm::fillmethod call - Change:
clean_dataproperty is by defaultNoneinstead of empty dictionary
0.16.2 : 06.11.2020
- Fix: Fixed issue with
clean_methods returning values resolved as False (False,None,'')
0.16.1 : 29.10.2020
- Fix: Ignore
ModelMultipleChoiceFieldinForm::fill()
0.16.0 : 14.09.2020
- Change: Correctly resolve key postfix if
ModelChoiceFieldis used inForm::fill() - Change:
DjangoApiFormsConfigis created - Note: One more step to get rid of
pytestin project (we don't need it)
0.15.1 : 29.08.2020
- Feature:
FileField.content_typeintroduced (contains mime)
0.15.0 : 23.08.2020
- Feature:
FileFieldandImageFieldintroduced - Note: Defined extras in
setup.pyfor optionalPillowandmsgpackdependencies - Feature: Working
Form::fill()method for primitive data types. IntroducedIgnoreFillMixin
0.14.0 : 07.08.2020
- Feature:
BaseForm._requestproperty introduced (now it's possible to use request inclean_methods)
0.13.0 : 09.07.2020
- Fix: Fixed
Content-Typehandling ifcharsetorboundaryis present
0.12.0 : 11.06.2020
- Fix: Do not call resolvers methods, if property is not required and not present in request
0.11.0 : 10.06.2020
- Change: Non specified non-required fields will no longer be available in the cleaned_data form attribute.
0.10.0 : 01.06.2020
- Change: All package exceptions inherits from
ApiFormException. - Fix: Specifying encoding while opening files in
setup.py(failing on Windows OS).
0.9.0 : 11.05.2020
- Change: Moved field error messages to default_error_messages for easier overriding and testing.
- Fix: Fix KeyError when invalid values are sent to FieldList.
- Fix: Removed unnecessary error checking in FieldList.
0.8.0 : 05.05.2020
- Maintenance: Add tests for fields
- Change: Remove DeclarativeFieldsMetaclass and import from Django instead.
- Change: Msgpack dependency is no longer required.
- Change: Empty values passed into a FormField now return {} rather than None.
- Fix: Throw a more user friendly error when passing non-Enums or invalid values to EnumField.
0.7.1 : 13.04.2020
- Change Use poetry instead of pipenv
- Change: Library renamed from
django_api_formstodjango-api-forms(cosmetic change without effect)
0.7.0 : 03.03.2020
- Change: Library renamed from
django_request_formattertodjango_api_forms - Change: Imports in main module
django_api_forms
0.6.0 : 18.02.2020
- Feature:
BooleanFieldintroduced
0.5.8 : 07.01.2020
- Fix: Pass
Invalid valueasValidationErrornot as astring
0.5.7 : 07.01.2020
- Fix: Introduced generic
Invalid valueerror message, if there isAttributeError,TypeError,ValueError
0.5.6 : 01.01.2020
- Fix: Fixing issue from version
0.5.5but this time for real - Change: Renamed version file from
__version__.pytoversion.py
0.5.5 : 01.01.2020
- Fix: Check instance only if there is a value in
FieldListandFormFieldList
0.5.4 : 24.12.2019
- Fix: Added missing
msgpack`` dependency tosetup.py`
0.5.3 : 20.12.2019
- Feature: Introduced generic
AnyField
0.5.2 : 19.12.2019
- Fix: Skip processing of the
FormFieldif value is not required and empty
0.5.1 : 19.12.2019
- Fix: Process
EnumFieldeven if it's not marked as required
0.5.0 : 16.12.2019
- Change: Use native
django.form.fieldsif possible - Change: Removed
kwargspropagation from release0.3.0 - Change: Changed syntax back to
django.formscompatible (e.g.form.validate_{key}()->form.clean_{key}()) - Change:
FieldListraisesValidationErrorinstead ofRuntimeExceptionif there is a type in validation - Change: Use private properties for internal data in field objects
- Fixed:
FieldListreturns values instead ofNone - Fix: Fixed validation in
DictionaryField - Maintenance: Basic unit tests
0.4.3 : 29.11.2019
- Fix: Fixed
Formhas no attributeself._data
0.4.2 : 29.11.2019
- Fix: If payload is empty, create empty dictionary to avoid
NoneTypeerror
0.4.1 : 14.11.2019
- Feature: Introduced
UUIDField
0.4.0 : 13.11.2019
- Feature: Introduced
DictionaryField
0.3.0 : 11.11.2019
- Feature: Propagate
kwargsfromForm.is_valid()toForm.validate()andForm.validate_{key}()methods
0.2.1 : 4.11.2019
- Fix: Fixed
to_python()in FormFieldList
0.2.0 : 31.10.2019
- Change:
Form.validate()replaced byForm.is_valid() - Feature:
Form.validate()is now used as a last step of form validation and it's aimed to be overwritten if needed - Note: Unit tests initialization
0.1.6 : 24.10.2019
- Fix: Non-required EnumField is now working
- Feature: WIP: Initial method for filling objects
Form::fill()
0.1.5 : 23.10.2019
- Fix: Assign errors to form before raising
ValidationError
0.1.4 : 23.10.2019
- Fix: Do not return empty error records in
Form:errors
0.1.3 : 23.10.2019
- Fix: Use custom
DeclarativeFieldsMetaclassbecause of customFieldclass - Fix: Do not return untouched fields in
Form::payload - Fix: Fix for None
default_validatorsinField
0.1.2 : 22:10.2019
- Feature: Support for
validation_{field}methods inForm(initial support)
0.1.1 : 22.10.2019
- Feature:
EnumField
0.1.0 : 22.10.2019
- Feature: First version of
Formclass - Feature:
CharField - Feature:
IntegerField - Feature:
FloatField - Feature:
DecimalField - Feature:
DateField - Feature:
TimeField - Feature:
DateTimeField - Feature:
DurationField - Feature:
RegexField - Feature:
EmailField - Feature:
BooleanField - Feature:
RegexField - Feature:
FieldList - Feature:
FormField - Feature:
FormFieldList
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django-api-forms-0.21.0.tar.gz.
File metadata
- Download URL: django-api-forms-0.21.0.tar.gz
- Upload date:
- Size: 20.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.8.0 pkginfo/1.8.2 readme-renderer/32.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.8 tqdm/4.62.3 importlib-metadata/4.10.1 keyring/23.5.0 rfc3986/2.0.0 colorama/0.4.4 CPython/3.10.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
586263a4296baefd3ae5f4e04c875fe670398a8924a313d96bae9bbf54cd36f9
|
|
| MD5 |
589604bf556e0c1862fd03f7fd0cafaa
|
|
| BLAKE2b-256 |
9058acc4dd6d506b013217f255e6c7e2db1aad69067ffcf943f4ca1c44ea7a9c
|
File details
Details for the file django_api_forms-0.21.0-py3-none-any.whl.
File metadata
- Download URL: django_api_forms-0.21.0-py3-none-any.whl
- Upload date:
- Size: 15.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.8.0 pkginfo/1.8.2 readme-renderer/32.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.8 tqdm/4.62.3 importlib-metadata/4.10.1 keyring/23.5.0 rfc3986/2.0.0 colorama/0.4.4 CPython/3.10.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab29f1a9715565b550550687ecc20b8888a94964ca20d5caa469d2365f0c28d3
|
|
| MD5 |
98341ff8f9d6217322db3bdf76c244e0
|
|
| BLAKE2b-256 |
52fadf2ac8f6bdb653d2019be7f912a290081f61389d32955adbd1a6d3d15c75
|