├── tests ├── __init__.py ├── conftest.py └── test_marshmallow.py ├── env └── pip-selfcheck.json ├── MANIFEST.in ├── setup.cfg ├── mkdocs.yml ├── .gitignore ├── requirements.txt ├── docs ├── css │ └── extra.css └── index.md ├── tox.ini ├── LICENSE ├── .travis.yml ├── runtests.py ├── setup.py ├── rest_marshmallow └── __init__.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /env/pip-selfcheck.json: -------------------------------------------------------------------------------- 1 | {"last_check":"2015-09-11T09:06:09Z","pypi_version":"7.1.2"} -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE 2 | recursive-exclude * __pycache__ 3 | recursive-exclude * *.py[co] 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | ignore = E501 6 | exclude = .git,.ropeproject,.tox,docs,.git,build,env,venv 7 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: django-rest-marshmallow 2 | site_description: Marshmallow schemas for Django REST framework 3 | repo_url: https://github.com/marshmallow-code/django-rest-marshmallow 4 | site_dir: html 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.db 3 | *~ 4 | .* 5 | 6 | html/ 7 | htmlcov/ 8 | coverage/ 9 | build/ 10 | dist/ 11 | *.egg-info/ 12 | MANIFEST 13 | 14 | bin/ 15 | include/ 16 | lib/ 17 | local/ 18 | 19 | !.gitignore 20 | !.travis.yml 21 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | def pytest_configure(): 2 | from django.conf import settings 3 | 4 | settings.configure() 5 | 6 | try: 7 | import django 8 | django.setup() 9 | except AttributeError: 10 | pass 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Minimum Django and REST framework version 2 | Django>=1.11.14,<4.0 3 | djangorestframework>=3.6,<3.13 4 | marshmallow>=3.0,<4.0 5 | 6 | # Test requirements 7 | pytest 8 | pytest-django==3.8.0 9 | pytest-cov==2.12.1 10 | flake8==3.9.2 11 | 12 | # MkDocs for documentation 13 | mkdocs 14 | -------------------------------------------------------------------------------- /docs/css/extra.css: -------------------------------------------------------------------------------- 1 | body.homepage div.col-md-9 h1:first-of-type { 2 | text-align: center; 3 | font-size: 60px; 4 | font-weight: 300; 5 | margin-top: 0; 6 | } 7 | 8 | body.homepage div.col-md-9 p:first-of-type { 9 | text-align: center; 10 | } 11 | 12 | body.homepage .badges { 13 | text-align: right; 14 | } 15 | 16 | body.homepage .badges a { 17 | display: inline-block; 18 | } 19 | 20 | body.homepage .badges a img { 21 | padding: 0; 22 | margin: 0; 23 | } 24 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py38-{flake8,docs} 4 | {py36,py37.py38}-drf{3.8,3.9}-marshmallow3 5 | 6 | [travis:env] 7 | DRF = 8 | 3.8: drf3.8 9 | 3.9: drf3.9 10 | 11 | [testenv] 12 | commands = ./runtests.py --fast 13 | setenv = 14 | PYTHONDONTWRITEBYTECODE=1 15 | deps = 16 | drf3.8: djangorestframework==3.8.2 17 | drf3.9: djangorestframework==3.9.0 18 | marshmallow3: marshmallow>=3.0.0,<4.0.0 19 | Django==1.11 20 | pytest-django==3.2.1 21 | pytest==3.6.0 22 | 23 | [testenv:py36-flake8] 24 | commands = ./runtests.py --lintonly 25 | deps = 26 | pytest 27 | flake8==3.6.0 28 | 29 | [testenv:py36-docs] 30 | commands = mkdocs build 31 | deps = 32 | mkdocs==1.0.4 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Tom Christie 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.8" 5 | - "3.7" 6 | - "3.6" 7 | 8 | cache: pip 9 | script: skip 10 | 11 | jobs: 12 | include: 13 | - stage: test 14 | env: 15 | matrix: 16 | - DRF=3.8 17 | - DRF=3.9 18 | install: 19 | - travis_retry pip install -U pip setuptools 20 | - travis_retry pip install -U virtualenv tox tox-travis 21 | 22 | script: 23 | - tox 24 | 25 | - stage: release 26 | if: tag IS present 27 | python: "3.8" 28 | script: skip 29 | deploy: 30 | provider: pypi 31 | user: sloria 32 | on: 33 | tags: true 34 | distributions: "sdist bdist_wheel" 35 | password: 36 | secure: ozRXU8oimoU9MxlC3eL/iEt8j3cZzO8HJcb4yutR6PAsPRvjAk6f0eqr+PhZbHU483uXImNsWDYYHFnlISFNtX14NiYcEoqSpLxp94Dslm/siqC+ZdSVjLAV7SE4NRJXIccM+FDKCvRqdUX1oVSaxL7mGr0B5BJEfHTEQj+R9ZG/wOD/6rSK9i1ZQqQx6Uu8TSQS/ErlLuSschlbiDUU6AUm5BZZEUg9E+XqfWwLHRfpAmzbhz3gnB/ef7kOu+qTtaOb3+F6eYg4fV2VDZIH8UyUc3gG4SA1lttDhJL3R4wU7+UgTEN2P4MAdGtCfdJfBkzbUt1X7apGsKAI7oH4WSQgHR1/Q7MyhN7eaN4YxH7x3HyaL/VL3phrXppVokEJSTiuZduRDdFRBe2zo4nxJl9+WsfsHbJUU/qWrSTUAjoQnQgt4cyDxyju06WD6T3uOuA5o884T/tXQuB7ECwmqwv5oowlwQ5mYlkQ3myJCZA5yp4wkksfNZEi7Ww/B6kbyFfiZpB+l717V7KLCTm3dvdA2uDU4KvPFvqvmRBcIFr33PlIL3YNVq/Bb2+rfgoMF+uT1t7sqa+c8bR3TEcrZBezd0GGAyUzQAajpwvWzt16AC87wbJKxe2UNJ02Ty48DmvBjR/+N1YDLvLAkF6Ayijn4iKL/GhGDuzh+UKwBeM= 37 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | from __future__ import print_function 3 | 4 | import pytest 5 | import sys 6 | import os 7 | import subprocess 8 | 9 | 10 | PYTEST_ARGS = { 11 | 'default': ['tests'], 12 | 'fast': ['tests', '-q'], 13 | } 14 | 15 | sys.path.append(os.path.dirname(__file__)) 16 | 17 | 18 | def exit_on_failure(ret, message=None): 19 | if ret: 20 | sys.exit(ret) 21 | 22 | 23 | def flake8_main(): 24 | print('Running flake8 code linting') 25 | ret = subprocess.call('flake8') 26 | print('flake8 failed' if ret else 'flake8 passed') 27 | return ret 28 | 29 | 30 | def split_class_and_function(string): 31 | class_string, function_string = string.split('.', 1) 32 | return "%s and %s" % (class_string, function_string) 33 | 34 | 35 | def is_function(string): 36 | # `True` if it looks like a test function is included in the string. 37 | return string.startswith('test_') or '.test_' in string 38 | 39 | 40 | def is_class(string): 41 | # `True` if first character is uppercase - assume it's a class name. 42 | return string[0] == string[0].upper() 43 | 44 | 45 | if __name__ == "__main__": 46 | try: 47 | sys.argv.remove('--nolint') 48 | except ValueError: 49 | run_flake8 = True 50 | else: 51 | run_flake8 = False 52 | 53 | try: 54 | sys.argv.remove('--lintonly') 55 | except ValueError: 56 | run_tests = True 57 | else: 58 | run_tests = False 59 | 60 | try: 61 | sys.argv.remove('--fast') 62 | except ValueError: 63 | style = 'default' 64 | else: 65 | style = 'fast' 66 | run_flake8 = False 67 | 68 | if len(sys.argv) > 1: 69 | pytest_args = sys.argv[1:] 70 | first_arg = pytest_args[0] 71 | if first_arg.startswith('-'): 72 | # `runtests.py [flags]` 73 | pytest_args = ['tests'] + pytest_args 74 | elif is_class(first_arg) and is_function(first_arg): 75 | # `runtests.py TestCase.test_function [flags]` 76 | expression = split_class_and_function(first_arg) 77 | pytest_args = ['tests', '-k', expression] + pytest_args[1:] 78 | elif is_class(first_arg) or is_function(first_arg): 79 | # `runtests.py TestCase [flags]` 80 | # `runtests.py test_function [flags]` 81 | pytest_args = ['tests', '-k', pytest_args[0]] + pytest_args[1:] 82 | else: 83 | pytest_args = PYTEST_ARGS[style] 84 | 85 | if run_tests: 86 | exit_on_failure(pytest.main(pytest_args)) 87 | if run_flake8: 88 | exit_on_failure(flake8_main()) 89 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import os 5 | from codecs import open 6 | from setuptools import setup 7 | 8 | here = os.path.abspath(os.path.dirname(__file__)) 9 | name = 'django-rest-marshmallow' 10 | package = 'rest_marshmallow' 11 | description = 'Marshmallow schemas for Django REST framework' 12 | url = 'https://github.com/marshmallow-code/django-rest-marshmallow' 13 | author = 'Tom Christie' 14 | author_email = 'tom@tomchristie.com' 15 | maintainer = 'Steven Loria' 16 | maintainer_email = 'sloria1@gmail.com' 17 | license = 'BSD' 18 | 19 | 20 | def get_version(package): 21 | """ 22 | Return package version as listed in `__version__` in `init.py`. 23 | """ 24 | init_py = open(os.path.join(package, '__init__.py')).read() 25 | return re.search("^__version__ = ['\"]([^'\"]+)['\"]", 26 | init_py, re.MULTILINE).group(1) 27 | 28 | 29 | def get_packages(package): 30 | """ 31 | Return root package and all sub-packages. 32 | """ 33 | return [dirpath 34 | for dirpath, dirnames, filenames in os.walk(package) 35 | if os.path.exists(os.path.join(dirpath, '__init__.py'))] 36 | 37 | 38 | def get_package_data(package): 39 | """ 40 | Return all files under the root package, that are not in a 41 | package themselves. 42 | """ 43 | walk = [(dirpath.replace(package + os.sep, '', 1), filenames) 44 | for dirpath, dirnames, filenames in os.walk(package) 45 | if not os.path.exists(os.path.join(dirpath, '__init__.py'))] 46 | 47 | filepaths = [] 48 | for base, filenames in walk: 49 | filepaths.extend([os.path.join(base, filename) 50 | for filename in filenames]) 51 | return {package: filepaths} 52 | 53 | 54 | version = get_version(package) 55 | 56 | with open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 57 | long_description = f.read() 58 | 59 | setup( 60 | name=name, 61 | version=version, 62 | url=url, 63 | license=license, 64 | description=description, 65 | author=author, 66 | author_email=author_email, 67 | maintainer=maintainer, 68 | maintainer_email=maintainer_email, 69 | packages=get_packages(package), 70 | package_data=get_package_data(package), 71 | install_requires=[], 72 | long_description=long_description, 73 | long_description_content_type='text/markdown', 74 | python_requires=">=3.6", 75 | classifiers=[ 76 | 'Development Status :: 4 - Beta', 77 | 'Environment :: Web Environment', 78 | 'Framework :: Django', 79 | 'Intended Audience :: Developers', 80 | 'License :: OSI Approved :: BSD License', 81 | 'Operating System :: OS Independent', 82 | 'Natural Language :: English', 83 | 'Programming Language :: Python :: 3', 84 | 'Programming Language :: Python :: 3.6', 85 | 'Programming Language :: Python :: 3.7', 86 | 'Programming Language :: Python :: 3.8', 87 | 'Programming Language :: Python :: 3.9', 88 | 'Topic :: Internet :: WWW/HTTP', 89 | ] 90 | ) 91 | -------------------------------------------------------------------------------- /rest_marshmallow/__init__.py: -------------------------------------------------------------------------------- 1 | import marshmallow 2 | from marshmallow import Schema as MarshmallowSchema 3 | from marshmallow import fields # noqa 4 | from marshmallow.exceptions import ValidationError as MarshmallowValidationError 5 | from rest_framework.serializers import BaseSerializer, ValidationError 6 | 7 | 8 | IS_MARSHMALLOW_LT_3 = int(marshmallow.__version__.split('.')[0]) < 3 9 | 10 | 11 | __version__ = '4.0.2' 12 | 13 | _schema_kwargs = ( 14 | 'only', 'exclude', 'dump_only', 'load_only', 'context', 'partial' 15 | ) 16 | 17 | 18 | class Schema(BaseSerializer, MarshmallowSchema): 19 | 20 | def __new__(cls, *args, **kwargs): 21 | # We're overriding the DRF implementation here, because ListSerializer 22 | # clashes with Nested implementation. 23 | kwargs.pop('many', False) 24 | return super(Schema, cls).__new__(cls, *args, **kwargs) 25 | 26 | def __init__(self, *args, **kwargs): 27 | schema_kwargs = { 28 | 'many': kwargs.get('many', False) 29 | } 30 | # Remove any kwargs that are only valid for marshmallow schemas 31 | for key in _schema_kwargs: 32 | if key in kwargs: 33 | schema_kwargs[key] = kwargs.pop(key) 34 | 35 | super(Schema, self).__init__(*args, **kwargs) 36 | # XXX: Remove parent attribute so that Field.root resolves properly 37 | # https://github.com/marshmallow-code/django-rest-marshmallow/issues/131#issuecomment-601089549 38 | delattr(self, 'parent') 39 | MarshmallowSchema.__init__(self, **schema_kwargs) 40 | 41 | def to_representation(self, instance): 42 | if IS_MARSHMALLOW_LT_3: 43 | return self.dump(instance).data 44 | return self.dump(instance) 45 | 46 | def to_internal_value(self, data): 47 | if IS_MARSHMALLOW_LT_3: 48 | ret = self.load(data) 49 | if ret.errors: 50 | raise ValidationError(ret.errors) 51 | return ret.data 52 | try: 53 | return self.load(data) 54 | except MarshmallowValidationError as err: 55 | raise ValidationError(err.messages) 56 | 57 | @property 58 | def data(self): 59 | # We're overriding the default implementation here, because the 60 | # '_data' property clashes with marshmallow's implementation. 61 | if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'): 62 | msg = ( 63 | 'When a serializer is passed a `data` keyword argument you ' 64 | 'must call `.is_valid()` before attempting to access the ' 65 | 'serialized `.data` representation.\n' 66 | 'You should either call `.is_valid()` first, ' 67 | 'or access `.initial_data` instead.' 68 | ) 69 | raise AssertionError(msg) 70 | 71 | if not hasattr(self, '_serializer_data'): 72 | if self.instance is not None and not getattr(self, '_errors', None): 73 | self._serializer_data = self.to_representation(self.instance) 74 | elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None): 75 | self._serializer_data = self.to_representation(self.validated_data) 76 | else: 77 | self._serializer_data = self.get_initial() 78 | return self._serializer_data 79 | 80 | @property 81 | def context(self): 82 | return self._context 83 | 84 | @context.setter 85 | def context(self, value): 86 | self._context = value 87 | 88 | get_attribute = MarshmallowSchema.get_attribute 89 | -------------------------------------------------------------------------------- /tests/test_marshmallow.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from rest_marshmallow import Schema, fields 4 | 5 | 6 | import marshmallow 7 | 8 | IS_MARSHMALLOW_1 = marshmallow.__version__.split('.')[0] == '1' 9 | 10 | 11 | class Object(object): 12 | def __init__(self, **kwargs): 13 | for key, value in kwargs.items(): 14 | setattr(self, key, value) 15 | 16 | 17 | class ExampleSerializer(Schema): 18 | number = fields.Integer() 19 | text = fields.String() 20 | date = fields.Date() 21 | 22 | def create(self, validated_data): 23 | return Object(**validated_data) 24 | 25 | def update(self, instance, validated_data): 26 | for key, value in validated_data.items(): 27 | setattr(instance, key, value) 28 | return instance 29 | 30 | 31 | class NestedSerializer(Schema): 32 | top = fields.Integer() 33 | child = fields.Nested(ExampleSerializer) 34 | 35 | 36 | class ManyNestedSerializer(Schema): 37 | top = fields.Integer() 38 | children = fields.Nested(ExampleSerializer, many=True) 39 | 40 | 41 | def test_serialize(): 42 | instance = Object(number=123, text='abc') 43 | serializer = ExampleSerializer(instance) 44 | assert serializer.data == {'number': 123, 'text': 'abc'} 45 | 46 | 47 | def test_serialize_nested(): 48 | instance = Object(top=1, child=Object(number=123, text='abc')) 49 | serializer = NestedSerializer(instance) 50 | assert serializer.data == {'top': 1, 'child': {'number': 123, 'text': 'abc'}} 51 | 52 | 53 | def test_serialize_many(): 54 | instances = [Object(number=123, text='abc') for i in range(3)] 55 | serializer = ExampleSerializer(instances, many=True) 56 | assert serializer.data == [ 57 | {'number': 123, 'text': 'abc'}, 58 | {'number': 123, 'text': 'abc'}, 59 | {'number': 123, 'text': 'abc'}, 60 | ] 61 | 62 | 63 | def test_serialize_nested_many(): 64 | instance = Object(top=1, children=[Object(number=123, text='abc') for i in range(3)]) 65 | serializer = ManyNestedSerializer(instance) 66 | assert serializer.data == {'top': 1, 'children': [ 67 | {'number': 123, 'text': 'abc'}, 68 | {'number': 123, 'text': 'abc'}, 69 | {'number': 123, 'text': 'abc'}, 70 | ]} 71 | 72 | 73 | def test_serialize_only(): 74 | instance = Object(number=123, text='abc', date=date.today()) 75 | serializer = ExampleSerializer(instance, only=('text',)) 76 | assert serializer.data == {'text': 'abc'} 77 | 78 | 79 | def test_deserialize(): 80 | data = {'number': 123, 'text': 'abc'} 81 | serializer = ExampleSerializer(data=data) 82 | assert serializer.is_valid() 83 | assert serializer.validated_data == {'number': 123, 'text': 'abc'} 84 | 85 | 86 | def test_deserialize_validation_failed(): 87 | data = {'number': 'abc', 'text': 'abc'} 88 | serializer = ExampleSerializer(data=data) 89 | assert not serializer.is_valid() 90 | if IS_MARSHMALLOW_1: 91 | expected_error = "invalid literal for int() with base 10: 'abc'" 92 | else: 93 | expected_error = 'Not a valid integer.' 94 | 95 | assert serializer.errors == { 96 | 'number': [expected_error] 97 | } 98 | 99 | 100 | def test_create(): 101 | data = {'number': 123, 'text': 'abc'} 102 | serializer = ExampleSerializer(data=data) 103 | assert serializer.is_valid() 104 | instance = serializer.save() 105 | assert isinstance(instance, Object) 106 | assert instance.number == 123 107 | assert instance.text == 'abc' 108 | assert serializer.data == {'number': 123, 'text': 'abc'} 109 | 110 | 111 | def test_update(): 112 | instance = Object(number=123, text='abc') 113 | data = {'number': 456, 'text': 'def'} 114 | serializer = ExampleSerializer(instance, data=data) 115 | assert serializer.is_valid() 116 | serializer.save() 117 | assert instance.number == 456 118 | assert instance.text == 'def' 119 | assert serializer.data == {'number': 456, 'text': 'def'} 120 | 121 | 122 | def test_context_data(): 123 | instance = Object(number=123, text='abc') 124 | serializer = ExampleSerializer(instance, context={'test': 'data'}) 125 | assert serializer.context == {'test': 'data'} 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Travis CI 5 | 6 | 7 | django-rest-marshmallow on PyPI 9 | 10 | 11 | marshmallow 3 compatible 13 | 14 |
15 | 16 | --- 17 | 18 | # [django-rest-marshmallow](https://marshmallow-code.github.io/django-rest-marshmallow/) 19 | 20 | [Marshmallow schemas][marshmallow] for Django REST framework. 21 | 22 | --- 23 | 24 | ## Overview 25 | 26 | `django-rest-marshmallow` provides an alternative serializer implementation to the built-in serializers, by using the python [marshmallow] library, but exposing the same API as REST framework's `Serializer` class. 27 | 28 | ## Requirements 29 | 30 | * Python (3.6+) 31 | * Django REST framework (3.8+) 32 | * Marshmallow (3.0.0+) 33 | 34 | ## Installation 35 | 36 | Install using `pip`... 37 | 38 | ```bash 39 | $ pip install django-rest-marshmallow 40 | ``` 41 | 42 | --- 43 | 44 | ## Usage 45 | 46 | Define your schemas as you would with marshmallow, but importing the `Schema` class from `rest_marshmallow` instead. 47 | 48 | ```python 49 | from rest_marshmallow import Schema, fields 50 | 51 | class CustomerSchema(Schema): 52 | name = fields.String() 53 | email = fields.Email() 54 | created_at = fields.DateTime() 55 | ``` 56 | 57 | The Schema class has the same interface as a Django REST framework serializer, so you can use it in your generic views... 58 | 59 | ```python 60 | class CustomerListView(generics.ListAPIView): 61 | queryset = Customer.objects.all() 62 | serializer_class = CustomerSchema 63 | ``` 64 | 65 | Or use the serializer API directly, for either serialization... 66 | 67 | ```python 68 | serializer = CustomerSchema(queryset, many=True) 69 | return Response(serializer.data) 70 | ``` 71 | 72 | Or for validation... 73 | 74 | ```python 75 | serializer = CustomerSchema(data=request.data) 76 | serializer.is_valid(raise_exception=True) 77 | serializer.validated_data 78 | ``` 79 | 80 | #### Instance create and update 81 | 82 | If you want to support `serializer.save()` you'll need to define the `.create()` and/or `.update()` methods explicitly. 83 | 84 | ```python 85 | class CustomerSchema(Schema): 86 | name = fields.String() 87 | email = fields.Email() 88 | created_at = fields.DateTime() 89 | 90 | def create(self, validated_data): 91 | return Customer.objects.create(**validated_data) 92 | 93 | def update(self, instance, validated_data): 94 | for key, value in validated_data.items(): 95 | setattr(instance, key, value) 96 | instance.save() 97 | return instance 98 | ``` 99 | 100 | You can now use `.save()` from your view code… 101 | 102 | ```python 103 | serializer = CustomerSchema(data=request.data) 104 | serializer.is_valid(raise_exception=True) 105 | serializer.save() 106 | return Response(serializer.data, status=status.HTTP_201_CREATED) 107 | ``` 108 | 109 | Or use the schema together with generic views that create or update instances... 110 | 111 | ```python 112 | class CustomerListView(generics.ListCreateAPIView): 113 | queryset = Customer.objects.all() 114 | serializer_class = CustomerSchema 115 | ``` 116 | 117 | Note that you should always use the `create()` and `update()` methods instead of overriding the `make_object()` marshmallow method. 118 | 119 | #### Nested representations 120 | 121 | For nested representations, use marshmallow's standard `Nested` field as usual. 122 | 123 | ```python 124 | from rest_marshmallow import fields, Schema 125 | 126 | class ArtistSchema(Schema): 127 | name = fields.String() 128 | 129 | class AlbumSchema(Schema): 130 | title = fields.String() 131 | release_date = fields.Date() 132 | artist = fields.Nested(ArtistSchema) 133 | ``` 134 | 135 | #### Excluding fields 136 | 137 | The marshmallow `only` and `exclude` arguments are also valid as serializer arguments: 138 | 139 | ```python 140 | serializer = CustomerSchema(queryset, many=True, only=('name', 'email')) 141 | return Response(serializer.data) 142 | ``` 143 | 144 | --- 145 | 146 | ## Testing 147 | 148 | Install testing requirements. 149 | 150 | ```bash 151 | $ pip install -r requirements.txt 152 | ``` 153 | 154 | Run with runtests. 155 | 156 | ```bash 157 | $ ./runtests.py 158 | ``` 159 | 160 | You can also use the excellent [tox](http://tox.readthedocs.org/en/latest/) testing tool to run the tests against all supported versions of Python and Django. Install tox globally, and then simply run: 161 | 162 | ```bash 163 | $ tox 164 | ``` 165 | 166 | ## Documentation 167 | 168 | To build the documentation, you'll need to install `mkdocs`. 169 | 170 | ```bash 171 | $ pip install mkdocs 172 | ``` 173 | 174 | To preview the documentation: 175 | 176 | ```bash 177 | $ mkdocs serve 178 | Running at: http://127.0.0.1:8000/ 179 | ``` 180 | 181 | To build the documentation: 182 | 183 | ```bash 184 | $ mkdocs build 185 | ``` 186 | 187 | 188 | [marshmallow]: https://marshmallow.readthedocs.org/en/latest/ 189 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | --- 11 | 12 | # django-rest-marshmallow 13 | 14 | [Marshmallow schemas][marshmallow] for Django REST framework. 15 | 16 | --- 17 | 18 | ## Overview 19 | 20 | `django-rest-marshmallow` provides an alternative serializer implementation to the built-in serializers, by using the python [marshmallow] library, but exposing the same API as REST framework's `Serializer` class. 21 | 22 | ## Requirements 23 | 24 | * Python (3.6+) 25 | * Django REST framework (3.8+) 26 | * Marshmallow (3.0.0+) 27 | 28 | ## Installation 29 | 30 | Install using `pip`... 31 | 32 | ```bash 33 | $ pip install django-rest-marshmallow 34 | ``` 35 | 36 | --- 37 | 38 | ## Usage 39 | 40 | Define your schemas as you would with marshmallow, but importing the `Schema` class from `rest_marshmallow` instead. 41 | 42 | from rest_marshmallow import Schema, fields 43 | 44 | class CustomerSchema(Schema): 45 | name = fields.String() 46 | email = fields.Email() 47 | created_at = fields.DateTime() 48 | 49 | The Schema class has the same interface as a Django REST framework serializer, so you can use it in your generic views... 50 | 51 | class CustomerListView(generics.ListAPIView): 52 | queryset = Customer.objects.all() 53 | serializer_class = CustomerSchema 54 | 55 | Or use the serializer API directly, for either serialization... 56 | 57 | serializer = CustomerSchema(queryset, many=True) 58 | return Response(serializer.data) 59 | 60 | Or for validation... 61 | 62 | serializer = CustomerSchema(data=request.data) 63 | serializer.is_valid(raise_exception=True) 64 | serializer.validated_data 65 | 66 | #### Instance create and update 67 | 68 | If you want to support `serializer.save()` you'll need to define the `.create()` and/or `.update()` methods explicitly. 69 | 70 | class CustomerSchema(Schema): 71 | name = fields.String() 72 | email = fields.Email() 73 | created_at = fields.DateTime() 74 | 75 | def create(self, validated_data): 76 | return Customer.objects.create(**validated_data) 77 | 78 | def update(self, instance, validated_data): 79 | for key, value in validated_data.items(): 80 | setattr(instance, key, value) 81 | instance.save() 82 | return instance 83 | 84 | You can now use `.save()` from your view code… 85 | 86 | serializer = CustomerSchema(data=request.data) 87 | serializer.is_valid(raise_exception=True) 88 | serializer.save() 89 | return Response(serializer.data, status=status.HTTP_201_CREATED) 90 | 91 | Or use the schema together with generic views that create or update instances... 92 | 93 | class CustomerListView(generics.ListCreateAPIView): 94 | queryset = Customer.objects.all() 95 | serializer_class = CustomerSchema 96 | 97 | Note that you should always use the `create()` and `update()` methods instead of overriding the `make_object()` marshmallow method. 98 | 99 | #### Nested representations 100 | 101 | For nested representations, use marshmallow's standard `Nested` field as usual. 102 | 103 | from rest_marshmallow import fields, Schema 104 | 105 | class ArtistSchema(Schema): 106 | name = fields.String() 107 | 108 | class AlbumSchema(Schema): 109 | title = fields.String() 110 | release_date = fields.Date() 111 | artist = fields.Nested(ArtistSchema) 112 | 113 | #### Excluding fields 114 | 115 | The marshmallow `only` and `exclude` arguments are also valid as serializer arguments: 116 | 117 | serializer = CustomerSchema(queryset, many=True, only=('name', 'email')) 118 | return Response(serializer.data) 119 | 120 | --- 121 | 122 | ## Testing 123 | 124 | Install testing requirements. 125 | 126 | ```bash 127 | $ pip install -r requirements.txt 128 | ``` 129 | 130 | Run with runtests. 131 | 132 | ```bash 133 | $ ./runtests.py 134 | ``` 135 | 136 | You can also use the excellent [tox](http://tox.readthedocs.org/en/latest/) testing tool to run the tests against all supported versions of Python and Django. Install tox globally, and then simply run: 137 | 138 | ```bash 139 | $ tox 140 | ``` 141 | 142 | ## Documentation 143 | 144 | To build the documentation, you'll need to install `mkdocs`. 145 | 146 | ```bash 147 | $ pip install mkdocs 148 | ``` 149 | 150 | To preview the documentation: 151 | 152 | ```bash 153 | $ mkdocs serve 154 | Running at: http://127.0.0.1:8000/ 155 | ``` 156 | 157 | To build the documentation: 158 | 159 | ```bash 160 | $ mkdocs build 161 | ``` 162 | 163 | ## Changelog 164 | 165 | ### 5.0.0 (unreleased) 166 | 167 | * Drop support for Python 2 and marshmallow 2. 168 | Only Python>=3.6 and marshmallow>=3 are supported. 169 | 170 | ### 4.0.2 (2020-03-19) 171 | 172 | * Fix serializing `Date` field 173 | ([#110](https://github.com/marshmallow-code/django-rest-marshmallow/issues/110) and [#131](https://github.com/marshmallow-code/django-rest-marshmallow/issues/131)). 174 | Thanks [@michaelwiles](https://github.com/michaelwiles) for the fix. 175 | 176 | ### 4.0.1 (2019-07-30) 177 | 178 | * Allow passing `partial` to constructor ([#103](https://github.com/marshmallow-code/django-rest-marshmallow/issues/103)). 179 | Thanks [@davidzwa](https://github.com/davidzwa) for the catch and patch. 180 | 181 | ### 4.0.0 (2019-02-14) 182 | 183 | * Drop official support for Python 3.4. Only Python 2.7 and >= 3.5 are supported. 184 | * Officially support django-rest-framework>=3.8. 185 | * Officially support marshmallow>=2.15.3 and >=3.0.0b18. 186 | * Fix behavior when passing `many=True` to a `Nested` field ([#72](https://github.com/marshmallow-code/django-rest-marshmallow/issues/72)). Thanks [@tyhoff](https://github.com/tyhoff) 187 | for reporting and thanks [@droppoint](https://github.com/marshmallow-code/django-rest-marshmallow/pull/75) for the PR. 188 | 189 | ### 3.1.1 (2018-05-24) 190 | 191 | * Support passing `context` argument ([#17](https://github.com/marshmallow-code/django-rest-marshmallow/issues/17)). Thanks [@pablotrinidad](https://github.com/pablotrinidad) for reporting 192 | and thanks [@dhararon](https://github.com/dhararon) for the PR. 193 | 194 | ### 3.1.0 (2018-02-11) 195 | 196 | * Support marshmallow>=3.0.0b7. Thanks [@trnsnt](https://github.com/trnsnt). 197 | 198 | ### 3.0.0 (2017-05-29) 199 | 200 | * Officially support Python 3.6. 201 | * Fix error thrown when using a `Nested` field ([#12](https://github.com/marshmallow-code/django-rest-marshmallow/issues/12)). Thanks [@devashishsharma2302](https://github.com/devashishsharma2302) for the fix. 202 | * Drop support for Django<1.10 and DRF<3.4. 203 | 204 | ### 2.0.0 (2016-10-09) 205 | 206 | * Drop support for marshmallow 1.x. Only marshmallow>=2.0.0 is supported. 207 | * Officially support Python 3.4 and 3.5. 208 | * Drop support for Python 3.3 (which is no longer supported by Django). 209 | 210 | ### 1.0.1 (2016-10-02) 211 | 212 | * Fix bug that raised a ``TypeError`` on serialization ([#6](https://github.com/marshmallow-code/django-rest-marshmallow/issues/6)). 213 | 214 | ### 1.0.0 (2015-09-11) 215 | 216 | * First release. 217 | 218 | [marshmallow]: https://marshmallow.readthedocs.org/en/latest/ 219 | --------------------------------------------------------------------------------