Skip to main content

Declarative Django request validation for RESTful APIs

Project description

Django API Forms

PyPI version codecov

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:

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

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 and field_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 by populate
  • 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 in FileField is supposed to be a tuple

0.19.0 : 12.07.2021

  • Feature: FieldList and FormFieldList now supports optional min/max constrains using min_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 in ImageField::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 during Form::fill method call
  • Change: clean_data property is by default None 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 in Form::fill()

0.16.0 : 14.09.2020

  • Change: Correctly resolve key postfix if ModelChoiceField is used in Form::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 and ImageField introduced
  • Note: Defined extras in setup.py for optional Pillow and msgpack dependencies
  • Feature: Working Form::fill() method for primitive data types. Introduced IgnoreFillMixin

0.14.0 : 07.08.2020

  • Feature: BaseForm._request property introduced (now it's possible to use request in clean_ methods)

0.13.0 : 09.07.2020

  • Fix: Fixed Content-Type handling if charset or boundary 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 to django-api-forms (cosmetic change without effect)

0.7.0 : 03.03.2020

  • Change: Library renamed from django_request_formatter to django_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 as ValidationError not as a string

0.5.7 : 07.01.2020

  • Fix: Introduced generic Invalid value error message, if there is AttributeError, 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 to version.py

0.5.5 : 01.01.2020

  • Fix: Check instance only if there is a value in FieldList and FormFieldList

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 release 0.3.0
  • Change: Changed syntax back to django.forms compatible (e.g. form.validate_{key}() -> form.clean_{key}())
  • Change: FieldList raises ValidationError instead of RuntimeException if there is a type in validation
  • Change: Use private properties for internal data in field objects
  • Fixed: FieldList returns values instead of None
  • Fix: Fixed validation in DictionaryField
  • Maintenance: Basic unit tests

0.4.3 : 29.11.2019

  • Fix: Fixed Form has no attribute self._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 from Form.is_valid() to Form.validate() and Form.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 by Form.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 custom Field class
  • Fix: Do not return untouched fields in Form::payload
  • Fix: Fix for None default_validators in Field

0.1.2 : 22:10.2019

  • Feature: Support for validation_{field} methods in Form (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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django-api-forms-0.21.0.tar.gz (20.0 kB view hashes)

Uploaded Source

Built Distribution

django_api_forms-0.21.0-py3-none-any.whl (15.9 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page