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_data
property.
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_strategy
andfield_strategy
0.20.1 : 13.1.2022
- Fix:
DictionaryField
was unable to raise validation errors for keys
0.20.0 : 14.10.2021
Anniversary release 🥳
- Feature: Population strategies introduced
- Feature:
fill
method is deprecated and replaced bypopulate
- Feature:
Settings
object introduced (form.settings
) - Feature: Pluggable content-type parsers using
DJANGO_API_FORMS_PARSERS
setting
0.19.1 : 17.09.2021
- Typing:
mime
argument inFileField
is supposed to be atuple
0.19.0 : 12.07.2021
- Feature:
FieldList
andFormFieldList
now supports optional min/max constrains usingmin_length
/max_length
0.18.0 : 16.04.2021
- Feature:
ModelForm
class introduced (experimental, initial support - not recommended for production)
0.17.0 : 24.02.2021
- Feature:
fill_method
introduced
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::fill
method call - Change:
clean_data
property is by defaultNone
instead 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
ModelMultipleChoiceField
inForm::fill()
0.16.0 : 14.09.2020
- Change: Correctly resolve key postfix if
ModelChoiceField
is used inForm::fill()
- Change:
DjangoApiFormsConfig
is created - Note: One more step to get rid of
pytest
in project (we don't need it)
0.15.1 : 29.08.2020
- Feature:
FileField.content_type
introduced (contains mime)
0.15.0 : 23.08.2020
- Feature:
FileField
andImageField
introduced - Note: Defined extras in
setup.py
for optionalPillow
andmsgpack
dependencies - Feature: Working
Form::fill()
method for primitive data types. IntroducedIgnoreFillMixin
0.14.0 : 07.08.2020
- Feature:
BaseForm._request
property introduced (now it's possible to use request inclean_
methods)
0.13.0 : 09.07.2020
- Fix: Fixed
Content-Type
handling ifcharset
orboundary
is 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_forms
todjango-api-forms
(cosmetic change without effect)
0.7.0 : 03.03.2020
- Change: Library renamed from
django_request_formatter
todjango_api_forms
- Change: Imports in main module
django_api_forms
0.6.0 : 18.02.2020
- Feature:
BooleanField
introduced
0.5.8 : 07.01.2020
- Fix: Pass
Invalid value
asValidationError
not as astring
0.5.7 : 07.01.2020
- Fix: Introduced generic
Invalid value
error message, if there isAttributeError
,TypeError
,ValueError
0.5.6 : 01.01.2020
- Fix: Fixing issue from version
0.5.5
but this time for real - Change: Renamed version file from
__version__.py
toversion.py
0.5.5 : 01.01.2020
- Fix: Check instance only if there is a value in
FieldList
andFormFieldList
0.5.4 : 24.12.2019
- Fix: Added missing
msgpack`` dependency to
setup.py`
0.5.3 : 20.12.2019
- Feature: Introduced generic
AnyField
0.5.2 : 19.12.2019
- Fix: Skip processing of the
FormField
if value is not required and empty
0.5.1 : 19.12.2019
- Fix: Process
EnumField
even if it's not marked as required
0.5.0 : 16.12.2019
- Change: Use native
django.form.fields
if possible - Change: Removed
kwargs
propagation from release0.3.0
- Change: Changed syntax back to
django.forms
compatible (e.g.form.validate_{key}()
->form.clean_{key}()
) - Change:
FieldList
raisesValidationError
instead ofRuntimeException
if there is a type in validation - Change: Use private properties for internal data in field objects
- Fixed:
FieldList
returns values instead ofNone
- Fix: Fixed validation in
DictionaryField
- Maintenance: Basic unit tests
0.4.3 : 29.11.2019
- Fix: Fixed
Form
has no attributeself._data
0.4.2 : 29.11.2019
- Fix: If payload is empty, create empty dictionary to avoid
NoneType
error
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
kwargs
fromForm.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
DeclarativeFieldsMetaclass
because of customField
class - Fix: Do not return untouched fields in
Form::payload
- Fix: Fix for None
default_validators
inField
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
Form
class - 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
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 |