├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build-release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── LICENSE ├── NOTICE ├── README.rst ├── RELEASING.md ├── SECURITY.md ├── docs ├── .gitignore ├── _static │ ├── apple-touch-icon.png │ ├── custom.css │ ├── favicon.ico │ ├── marshmallow-logo-200.png │ ├── marshmallow-logo-with-title-for-dark-theme.png │ ├── marshmallow-logo-with-title.png │ └── marshmallow-logo.png ├── api_reference.rst ├── authors.rst ├── changelog.rst ├── code_of_conduct.rst ├── conf.py ├── contributing.rst ├── custom_fields.rst ├── dashing.json ├── donate.rst ├── examples │ ├── index.rst │ ├── inflection.rst │ ├── quotes_api.rst │ └── validating_package_json.rst ├── extending │ ├── custom_error_handling.rst │ ├── custom_error_messages.rst │ ├── custom_options.rst │ ├── index.rst │ ├── overriding_attribute_access.rst │ ├── pre_and_post_processing_methods.rst │ ├── schema_validation.rst │ └── using_original_input_data.rst ├── index.rst ├── install.rst ├── kudos.rst ├── license.rst ├── marshmallow.class_registry.rst ├── marshmallow.decorators.rst ├── marshmallow.error_store.rst ├── marshmallow.exceptions.rst ├── marshmallow.experimental.context.rst ├── marshmallow.fields.rst ├── marshmallow.schema.rst ├── marshmallow.types.rst ├── marshmallow.utils.rst ├── marshmallow.validate.rst ├── nesting.rst ├── quickstart.rst ├── top_level.rst ├── upgrading.rst ├── whos_using.rst └── why.rst ├── examples ├── flask_example.py ├── inflection_example.py ├── invalid_package.json ├── package.json └── package_json_example.py ├── performance └── benchmark.py ├── pyproject.toml ├── src └── marshmallow │ ├── __init__.py │ ├── class_registry.py │ ├── constants.py │ ├── decorators.py │ ├── error_store.py │ ├── exceptions.py │ ├── experimental │ ├── __init__.py │ └── context.py │ ├── fields.py │ ├── orderedset.py │ ├── py.typed │ ├── schema.py │ ├── types.py │ ├── utils.py │ └── validate.py ├── tests ├── __init__.py ├── base.py ├── conftest.py ├── foo_serializer.py ├── mypy_test_cases │ ├── test_class_registry.py │ ├── test_schema.py │ └── test_validation_error.py ├── test_context.py ├── test_decorators.py ├── test_deserialization.py ├── test_error_store.py ├── test_exceptions.py ├── test_fields.py ├── test_options.py ├── test_registry.py ├── test_schema.py ├── test_serialization.py ├── test_utils.py └── test_validate.py └── tox.ini /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: "marshmallow" 2 | tidelift: "pypi/marshmallow" 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: ["dev", "*.x-line"] 5 | tags: ["*"] 6 | pull_request: 7 | 8 | jobs: 9 | docs: 10 | name: docs 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4.0.0 14 | - uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.13" 17 | - run: pip install tox 18 | - run: tox -e docs 19 | tests: 20 | name: ${{ matrix.name }} 21 | runs-on: ubuntu-latest 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | include: 26 | - { name: "3.9", python: "3.9", tox: py39 } 27 | - { name: "3.13", python: "3.13", tox: py313 } 28 | - { name: "mypy", python: "3.13", tox: mypy } 29 | steps: 30 | - uses: actions/checkout@v4.0.0 31 | - uses: actions/setup-python@v5 32 | with: 33 | python-version: ${{ matrix.python }} 34 | allow-prereleases: true 35 | - run: python -m pip install tox 36 | - run: python -m tox -e ${{ matrix.tox }} 37 | build: 38 | name: Build package 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: actions/setup-python@v5 43 | with: 44 | python-version: "3.13" 45 | - name: Install pypa/build 46 | run: python -m pip install build 47 | - name: Build a binary wheel and a source tarball 48 | run: python -m build 49 | - name: Install twine 50 | run: python -m pip install twine 51 | - name: Check build 52 | run: python -m twine check --strict dist/* 53 | - name: Store the distribution packages 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: python-package-distributions 57 | path: dist/ 58 | # this duplicates pre-commit.ci, so only run it on tags 59 | # it guarantees that linting is passing prior to a release 60 | lint-pre-release: 61 | if: startsWith(github.ref, 'refs/tags') 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: actions/checkout@v4.0.0 65 | - uses: actions/setup-python@v5 66 | with: 67 | python-version: "3.13" 68 | - run: python -m pip install tox 69 | - run: python -m tox -e lint 70 | publish-to-pypi: 71 | name: PyPI release 72 | if: startsWith(github.ref, 'refs/tags/') 73 | needs: [build, tests, lint-pre-release] 74 | runs-on: ubuntu-latest 75 | environment: 76 | name: pypi 77 | url: https://pypi.org/p/marshmallow 78 | permissions: 79 | id-token: write 80 | steps: 81 | - name: Download all the dists 82 | uses: actions/download-artifact@v4 83 | with: 84 | name: python-package-distributions 85 | path: dist/ 86 | - name: Publish distribution to PyPI 87 | uses: pypa/gh-action-pypi-publish@release/v1 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | pip-wheel-metadata 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | htmlcov 28 | .tox 29 | nosetests.xml 30 | .cache 31 | .pytest_cache 32 | 33 | # Translations 34 | *.mo 35 | 36 | # Mr Developer 37 | .mr.developer.cfg 38 | 39 | # IDE 40 | .project 41 | .pydevproject 42 | .idea 43 | 44 | # Coverage 45 | cover 46 | .coveragerc 47 | 48 | # Sphinx 49 | docs/_build 50 | README.html 51 | 52 | *.ipynb 53 | .ipynb_checkpoints 54 | 55 | Vagrantfile 56 | .vagrant 57 | 58 | *.db 59 | *.ai 60 | .konchrc 61 | _sandbox 62 | pylintrc 63 | 64 | # Virtualenvs 65 | env 66 | venv 67 | 68 | # pyenv 69 | .python-version 70 | 71 | # pytest 72 | .pytest_cache 73 | 74 | # Other 75 | .directory 76 | *.pprof 77 | 78 | # mypy 79 | .mypy_cache/ 80 | .dmypy.json 81 | dmypy.json 82 | 83 | # ruff 84 | .ruff_cache 85 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_schedule: monthly 3 | repos: 4 | - repo: https://github.com/astral-sh/ruff-pre-commit 5 | rev: v0.11.8 6 | hooks: 7 | - id: ruff 8 | - id: ruff-format 9 | - repo: https://github.com/python-jsonschema/check-jsonschema 10 | rev: 0.33.0 11 | hooks: 12 | - id: check-github-workflows 13 | - id: check-readthedocs 14 | # TODO: Remove blacken-docs when https://github.com/astral-sh/ruff/issues/8237 is implemented 15 | - repo: https://github.com/asottile/blacken-docs 16 | rev: 1.19.1 17 | hooks: 18 | - id: blacken-docs 19 | additional_dependencies: [black==24.10.0] 20 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | sphinx: 3 | configuration: docs/conf.py 4 | formats: 5 | - pdf 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.13" 10 | python: 11 | install: 12 | - method: pip 13 | path: . 14 | extra_requirements: 15 | - docs 16 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | Authors 3 | ******* 4 | 5 | Leads 6 | ===== 7 | 8 | - Steven Loria `@sloria `_ 9 | - Jérôme Lafréchoux `@lafrech `_ 10 | - Jared Deckard `@deckar01 `_ 11 | 12 | Contributors (chronological) 13 | ============================ 14 | 15 | - Sebastian Vetter `@elbaschid `_ 16 | - Eduard Carreras `@ecarreras `_ 17 | - Joakim Ekberg `@kalasjocke `_ 18 | - Mark Grey `@DeaconDesperado `_ 19 | - Anders Steinlein `@asteinlein `_ 20 | - Cyril Thomas `@Ketouem `_ 21 | - Austin Macdonald `@asmacdo `_ 22 | - Josh Carp `@jmcarp `_ 23 | - `@amikholap `_ 24 | - Sven-Hendrik Haase `@svenstaro `_ 25 | - Eric Wang `@ewang `_ 26 | - `@philtay `_ 27 | - `@malexer `_ 28 | - Andriy Yurchuk `@Ch00k `_ 29 | - Vesa Uimonen `@vesauimonen `_ 30 | - David Lord `@davidism `_ 31 | - Daniel Castro `@0xDCA `_ 32 | - Ben Jones `@RealSalmon `_ 33 | - Patrick Woods `@hakjoon `_ 34 | - Lukas Heiniger `@3rdcycle `_ 35 | - Ryan Lowe `@ryanlowe0 `_ 36 | - Jimmy Jia `@taion `_ 37 | - `@lustdante `_ 38 | - Sergey Aganezov, Jr. `@sergey-aganezov-jr `_ 39 | - Kevin Stone `@kevinastone `_ 40 | - Alex Morken `@alexmorken `_ 41 | - Sergey Polzunov `@traut `_ 42 | - Kelvin Hammond `@kelvinhammond `_ 43 | - Matt Stobo `@mwstobo `_ 44 | - Max Orhai `@max-orhai `_ 45 | - Praveen `@praveen-p `_ 46 | - Stas Sușcov `@stas `_ 47 | - Florian `@floqqi `_ 48 | - Evgeny Sureev `@evgeny-sureev `_ 49 | - Matt Bachmann `@Bachmann1234 `_ 50 | - Daniel Imhoff `@dwieeb `_ 51 | - Juan Rossi `@juanrossi `_ 52 | - Andrew Haigh `@nelfin `_ 53 | - `@Mise `_ 54 | - Taylor Edmiston `@tedmiston `_ 55 | - Francisco Demartino `@franciscod `_ 56 | - Eric Wang `@ewang `_ 57 | - Eugene Prikazchikov `@eprikazc `_ 58 | - Damian Heard `@DamianHeard `_ 59 | - Alec Reiter `@justanr `_ 60 | - Dan Sutherland `@d-sutherland `_ 61 | - Jeff Widman `@jeffwidman `_ 62 | - Simeon Visser `@svisser `_ 63 | - Taylan Develioglu `@tdevelioglu `_ 64 | - Danilo Akamine `@daniloakamine `_ 65 | - Maxim Kulkin `@maximkulkin `_ 66 | - `@immerrr `_ 67 | - Mike Yumatov `@yumike `_ 68 | - Tim Mundt `@Tim-Erwin `_ 69 | - Russell Davies `@russelldavies `_ 70 | - Jared Deckard `@deckar01 `_ 71 | - David Thornton `@davidthornton `_ 72 | - Vuong Hoang `@vuonghv `_ 73 | - David Bertouille `@dbertouille `_ 74 | - Alexandre Bonnetain `@Shir0kamii `_ 75 | - Tuukka Mustonen `@tuukkamustonen `_ 76 | - Tero Vuotila `@tvuotila `_ 77 | - Paul Zumbrun `@pauljz `_ 78 | - Gary Wilson Jr. `@gdub `_ 79 | - Sabine Maennel `@sabinem `_ 80 | - Victor Varvaryuk `@mindojo-victor `_ 81 | - Jāzeps Baško `@jbasko `_ 82 | - `@podhmo `_ 83 | - Dmitry Orlov `@mosquito `_ 84 | - Yuri Heupa `@YuriHeupa `_ 85 | - Roy Williams `@rowillia `_ 86 | - Vlad Frolov `@frol `_ 87 | - Erling Børresen `@erlingbo `_ 88 | - Jérôme Lafréchoux `@lafrech `_ 89 | - Roy Williams `@rowillia `_ 90 | - `@dradetsky `_ 91 | - Michal Kononenko `@MichalKononenko `_ 92 | - Yoichi NAKAYAMA `@yoichi `_ 93 | - Bernhard M. Wiedemann `@bmwiedemann `_ 94 | - Scott Werner `@scottwernervt `_ 95 | - Leonardo Fedalto `@Fedalto `_ 96 | - `@sduthil `_ 97 | - Steven Sklar `@sklarsa `_ 98 | - Alisson Silveira `@4lissonsilveira `_ 99 | - Harlov Nikita `@harlov `_ 100 | - `@stj `_ 101 | - Tomasz Magulski `@magul `_ 102 | - Suren Khorenyan `@mahenzon `_ 103 | - Jeffrey Berger `@JeffBerger `_ 104 | - Felix Yan `@felixonmars `_ 105 | - Prasanjit Prakash `@ikilledthecat `_ 106 | - Guillaume Gelin `@ramnes `_ 107 | - Maxim Novikov `@m-novikov `_ 108 | - James Remeika `@remeika `_ 109 | - Karandeep Singh Nagra `@knagra `_ 110 | - Dushyant Rijhwani `@dushr `_ 111 | - Viktor Kerkez `@alefnula `_ 112 | - Victor Gavro `@vgavro `_ 113 | - Kamil Gałuszka `@galuszkak `_ 114 | - David Watson `@arbor-dwatson `_ 115 | - Jan Margeta `@jmargeta `_ 116 | - AlexV `@asmodehn `_ 117 | - `@toffan `_ 118 | - Hampus Dunström `@Dunstrom `_ 119 | - Robert Jensen `@r1b `_ 120 | - Arijit Basu `@sayanarijit `_ 121 | - Sanjay P `@snjypl `_ 122 | - Víctor Zabalza `@zblz `_ 123 | - Riley Gibbs `@rileyjohngibbs `_ 124 | - Henry Doupe `@hdoupe `_ 125 | - `@miniscruff `_ 126 | - `@maxalbert `_ 127 | - Kim Gustyr `@khvn26 `_ 128 | - Bryce Drennan `@brycedrennan `_ 129 | - Tim Shaffer `@timster `_ 130 | - Hugo van Kemenade `@hugovk `_ 131 | - Maciej Urbański `@rooterkyberian `_ 132 | - Kostas Konstantopoulos `@kdop `_ 133 | - Stephen J. Fuhry `@fuhrysteve `_ 134 | - `@dursk `_ 135 | - Ezra MacDonald `@macdonaldezra `_ 136 | - Stanislav Rogovskiy `@atmo `_ 137 | - Cristi Scoarta `@cristi23 `_ 138 | - Anthony Sottile `@asottile `_ 139 | - Charles-Axel Dein `@charlax `_ 140 | - `@phrfpeixoto `_ 141 | - `@jceresini `_ 142 | - Nikolay Shebanov `@killthekitten `_ 143 | - Taneli Hukkinen `@hukkinj1 `_ 144 | - `@Reskov `_ 145 | - Albert Tugushev `@atugushev `_ 146 | - `@dfirst `_ 147 | - Tim Gates `@timgates42 `_ 148 | - Nathan `@nbanmp `_ 149 | - Ronan Murphy `@Resinderate `_ 150 | - Laurie Opperman `@EpicWink `_ 151 | - Ram Rachum `@cool-RR `_ 152 | - `@weeix `_ 153 | - Juan Norris `@juannorris `_ 154 | - 장준영 `@jun0jang `_ 155 | - `@ebargtuo `_ 156 | - Michał Getka `@mgetka `_ 157 | - Nadège Michel `@nadege `_ 158 | - Tamara `@infinityxxx `_ 159 | - Stephen Rosen `@sirosen `_ 160 | - Vladimir Mikhaylov `@vemikhaylov `_ 161 | - Stephen Eaton `@madeinoz67 `_ 162 | - Antonio Lassandro `@lassandroan `_ 163 | - Javier Fernández `@jfernandz `_ 164 | - Michael Dimchuk `@michaeldimchuk `_ 165 | - Jochen Kupperschmidt `@homeworkprod `_ 166 | - `@yourun-proger `_ 167 | - Ryan Morehart `@traherom `_ 168 | - Ben Windsor `@bwindsor `_ 169 | - Kevin Kirsche `@kkirsche `_ 170 | - Isira Seneviratne `@Isira-Seneviratne `_ 171 | - Karthikeyan Singaravelan `@tirkarthi `_ 172 | - Marco Satti `@marcosatti `_ 173 | - Ivo Reumkens `@vanHoi `_ 174 | - Aditya Tewary `@aditkumar72 `_ 175 | - Sebastien Lovergne `@TheBigRoomXXL `_ 176 | - Peter C `@somethingnew2-0 `_ 177 | - Marcel Jackwerth `@mrcljx` `_ 178 | - Fares Abubaker `@Fares-Abubaker `_ 179 | - Dharanikumar Sekar `@dharani7998 `_ 180 | - Nicolas Simonds `@0xDEC0DE `_ 181 | - Florian Laport `@Florian-Laport `_ 182 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | For the marshmallow code of conduct, see https://marshmallow.readthedocs.io/en/latest/code_of_conduct.html 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing guidelines 2 | ======================= 3 | 4 | So you're interested in contributing to marshmallow or `one of our associated 5 | projects `__? That's awesome! We 6 | welcome contributions from anyone willing to work in good faith with 7 | other contributors and the community (see also our 8 | :doc:`code_of_conduct`). 9 | 10 | Security contact information 11 | ---------------------------- 12 | 13 | To report a security vulnerability, please use the 14 | `Tidelift security contact `_. 15 | Tidelift will coordinate the fix and disclosure. 16 | 17 | Questions, feature requests, bug reports, and feedback… 18 | ------------------------------------------------------- 19 | 20 | …should all be reported on the `Github Issue Tracker`_ . 21 | 22 | .. _`Github Issue Tracker`: https://github.com/marshmallow-code/marshmallow/issues?state=open 23 | 24 | Ways to contribute 25 | ------------------ 26 | 27 | - Comment on some of marshmallow's `open issues `_ (especially those `labeled "feedback welcome" `_). Share a solution or workaround. Make a suggestion for how a feature can be made better. Opinions are welcome! 28 | - Improve `the docs `_. 29 | For straightforward edits, 30 | click the ReadTheDocs menu button in the bottom-right corner of the page and click "Edit". 31 | See the :ref:`Documentation ` section of this page if you want to build the docs locally. 32 | - If you think you've found a bug, `open an issue `_. 33 | - Contribute an :ref:`example usage ` of marshmallow. 34 | - Send a PR for an open issue (especially one `labeled "help wanted" `_). The next section details how to contribute code. 35 | 36 | 37 | Contributing code 38 | ----------------- 39 | 40 | Setting up for local development 41 | ++++++++++++++++++++++++++++++++ 42 | 43 | 1. Fork marshmallow_ on Github. 44 | 45 | .. code-block:: shell-session 46 | 47 | $ git clone https://github.com/marshmallow-code/marshmallow.git 48 | $ cd marshmallow 49 | 50 | 2. Install development requirements. **It is highly recommended that you use a virtualenv.** 51 | Use the following command to install an editable version of 52 | marshmallow along with its development requirements. 53 | 54 | .. code-block:: shell-session 55 | 56 | # After activating your virtualenv 57 | $ pip install -e '.[dev]' 58 | 59 | 3. Install the pre-commit hooks, which will format and lint your git staged files. 60 | 61 | .. code-block:: shell-session 62 | 63 | # The pre-commit CLI was installed above 64 | $ pre-commit install --allow-missing-config 65 | 66 | Git branch structure 67 | ++++++++++++++++++++ 68 | 69 | marshmallow abides by the following branching model: 70 | 71 | ``dev`` 72 | Current development branch. **New features should branch off here**. 73 | 74 | ``X.Y-line`` 75 | Maintenance branch for release ``X.Y``. **Bug fixes should be sent to the most recent release branch.** A maintainer will forward-port the fix to ``dev``. Note: exceptions may be made for bug fixes that introduce large code changes. 76 | 77 | **Always make a new branch for your work**, no matter how small. Also, **do not put unrelated changes in the same branch or pull request**. This makes it more difficult to merge your changes. 78 | 79 | Pull requests 80 | ++++++++++++++ 81 | 82 | 1. Create a new local branch. 83 | 84 | For a new feature: 85 | 86 | .. code-block:: shell-session 87 | 88 | $ git checkout -b name-of-feature dev 89 | 90 | For a bugfix: 91 | 92 | .. code-block:: shell-session 93 | 94 | $ git checkout -b fix-something 3.x-line 95 | 96 | 2. Commit your changes. Write `good commit messages `_. 97 | 98 | .. code-block:: shell-session 99 | 100 | $ git commit -m "Detailed commit message" 101 | $ git push origin name-of-feature 102 | 103 | 3. Before submitting a pull request, check the following: 104 | 105 | - If the pull request adds functionality, it is tested and the docs are updated. 106 | - You've added yourself to ``AUTHORS.rst``. 107 | 108 | 4. Submit a pull request to ``marshmallow-code:dev`` or the appropriate maintenance branch. The `CI `_ build must be passing before your pull request is merged. 109 | 110 | Running tests 111 | +++++++++++++ 112 | 113 | To run all tests: 114 | 115 | .. code-block:: shell-session 116 | 117 | $ pytest 118 | 119 | To run formatting and syntax checks: 120 | 121 | .. code-block:: shell-session 122 | 123 | $ tox -e lint 124 | 125 | (Optional) To run tests in all supported Python versions in their own virtual environments (must have each interpreter installed): 126 | 127 | .. code-block:: shell-session 128 | 129 | $ tox 130 | 131 | .. _contributing_documentation: 132 | 133 | Documentation 134 | +++++++++++++ 135 | 136 | Contributions to the documentation are welcome. Documentation is written in `reStructuredText`_ (rST). A quick rST reference can be found `here `_. Builds are powered by Sphinx_. 137 | 138 | To build and serve the docs in "watch" mode: 139 | 140 | .. code-block:: shell-session 141 | 142 | $ tox -e docs-serve 143 | 144 | Changes to documentation will automatically trigger a rebuild. 145 | 146 | 147 | .. _contributing_examples: 148 | 149 | Contributing examples 150 | +++++++++++++++++++++ 151 | 152 | Have a usage example you'd like to share? A custom `Field ` that others might find useful? Feel free to add it to the `examples `_ directory and send a pull request. 153 | 154 | 155 | .. _Sphinx: https://www.sphinx-doc.org/ 156 | .. _`reStructuredText`: https://docutils.sourceforge.io/rst.html 157 | .. _marshmallow: https://github.com/marshmallow-code/marshmallow 158 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Steven Loria and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | marshmallow includes code adapted from Django. 2 | 3 | Django License 4 | ============== 5 | 6 | Copyright (c) Django Software Foundation and individual contributors. 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | 1. Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | 15 | 2. Redistributions in binary form must reproduce the above copyright 16 | notice, this list of conditions and the following disclaimer in the 17 | documentation and/or other materials provided with the distribution. 18 | 19 | 3. Neither the name of Django nor the names of its contributors may be used 20 | to endorse or promote products derived from this software without 21 | specific prior written permission. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 27 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 28 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 30 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ******************************************** 2 | marshmallow: simplified object serialization 3 | ******************************************** 4 | 5 | |pypi| |build-status| |pre-commit| |docs| 6 | 7 | .. |pypi| image:: https://badgen.net/pypi/v/marshmallow 8 | :target: https://pypi.org/project/marshmallow/ 9 | :alt: Latest version 10 | 11 | .. |build-status| image:: https://github.com/marshmallow-code/marshmallow/actions/workflows/build-release.yml/badge.svg 12 | :target: https://github.com/marshmallow-code/marshmallow/actions/workflows/build-release.yml 13 | :alt: Build status 14 | 15 | .. |pre-commit| image:: https://results.pre-commit.ci/badge/github/marshmallow-code/marshmallow/dev.svg 16 | :target: https://results.pre-commit.ci/latest/github/marshmallow-code/marshmallow/dev 17 | :alt: pre-commit.ci status 18 | 19 | .. |docs| image:: https://readthedocs.org/projects/marshmallow/badge/ 20 | :target: https://marshmallow.readthedocs.io/ 21 | :alt: Documentation 22 | 23 | .. start elevator-pitch 24 | 25 | **marshmallow** is an ORM/ODM/framework-agnostic library for converting complex datatypes, such as objects, to and from native Python datatypes. 26 | 27 | .. code-block:: python 28 | 29 | from datetime import date 30 | from pprint import pprint 31 | 32 | from marshmallow import Schema, fields 33 | 34 | 35 | class ArtistSchema(Schema): 36 | name = fields.Str() 37 | 38 | 39 | class AlbumSchema(Schema): 40 | title = fields.Str() 41 | release_date = fields.Date() 42 | artist = fields.Nested(ArtistSchema()) 43 | 44 | 45 | bowie = dict(name="David Bowie") 46 | album = dict(artist=bowie, title="Hunky Dory", release_date=date(1971, 12, 17)) 47 | 48 | schema = AlbumSchema() 49 | result = schema.dump(album) 50 | pprint(result, indent=2) 51 | # { 'artist': {'name': 'David Bowie'}, 52 | # 'release_date': '1971-12-17', 53 | # 'title': 'Hunky Dory'} 54 | 55 | In short, marshmallow schemas can be used to: 56 | 57 | - **Validate** input data. 58 | - **Deserialize** input data to app-level objects. 59 | - **Serialize** app-level objects to primitive Python types. The serialized objects can then be rendered to standard formats such as JSON for use in an HTTP API. 60 | 61 | Get it now 62 | ========== 63 | 64 | .. code-block:: shell-session 65 | 66 | $ pip install -U marshmallow 67 | 68 | .. end elevator-pitch 69 | 70 | Documentation 71 | ============= 72 | 73 | Full documentation is available at https://marshmallow.readthedocs.io/ . 74 | 75 | Ecosystem 76 | ========= 77 | 78 | A list of marshmallow-related libraries can be found at the GitHub wiki here: 79 | 80 | https://github.com/marshmallow-code/marshmallow/wiki/Ecosystem 81 | 82 | Credits 83 | ======= 84 | 85 | Contributors 86 | ------------ 87 | 88 | This project exists thanks to all the people who contribute. 89 | 90 | **You're highly encouraged to participate in marshmallow's development.** 91 | Check out the `Contributing Guidelines `_ to see how you can help. 92 | 93 | Thank you to all who have already contributed to marshmallow! 94 | 95 | .. image:: https://opencollective.com/marshmallow/contributors.svg?width=890&button=false 96 | :target: https://marshmallow.readthedocs.io/en/latest/authors.html 97 | :alt: Contributors 98 | 99 | Backers 100 | ------- 101 | 102 | If you find marshmallow useful, please consider supporting the team with 103 | a donation. Your donation helps move marshmallow forward. 104 | 105 | Thank you to all our backers! [`Become a backer`_] 106 | 107 | .. _`Become a backer`: https://opencollective.com/marshmallow#backer 108 | 109 | .. image:: https://opencollective.com/marshmallow/backers.svg?width=890 110 | :target: https://opencollective.com/marshmallow#backers 111 | :alt: Backers 112 | 113 | Sponsors 114 | -------- 115 | 116 | .. start sponsors 117 | 118 | marshmallow is sponsored by `Route4Me `_. 119 | 120 | .. image:: https://github.com/user-attachments/assets/018c2e23-032e-4a11-98da-8b6dc25b9054 121 | :target: https://route4me.com 122 | :alt: Routing Planner 123 | 124 | Support this project by becoming a sponsor (or ask your company to support this project by becoming a sponsor). 125 | Your logo will be displayed here with a link to your website. [`Become a sponsor`_] 126 | 127 | .. _`Become a sponsor`: https://opencollective.com/marshmallow#sponsor 128 | 129 | .. end sponsors 130 | 131 | Professional Support 132 | ==================== 133 | 134 | Professionally-supported marshmallow is now available through the 135 | `Tidelift Subscription `_. 136 | 137 | Tidelift gives software development teams a single source for purchasing and maintaining their software, 138 | with professional-grade assurances from the experts who know it best, 139 | while seamlessly integrating with existing tools. [`Get professional support`_] 140 | 141 | .. _`Get professional support`: https://tidelift.com/subscription/pkg/pypi-marshmallow?utm_source=marshmallow&utm_medium=referral&utm_campaign=github 142 | 143 | .. image:: https://user-images.githubusercontent.com/2379650/45126032-50b69880-b13f-11e8-9c2c-abd16c433495.png 144 | :target: https://tidelift.com/subscription/pkg/pypi-marshmallow?utm_source=pypi-marshmallow&utm_medium=readme 145 | :alt: Get supported marshmallow with Tidelift 146 | 147 | 148 | Project Links 149 | ============= 150 | 151 | - Docs: https://marshmallow.readthedocs.io/ 152 | - Changelog: https://marshmallow.readthedocs.io/en/latest/changelog.html 153 | - Contributing Guidelines: https://marshmallow.readthedocs.io/en/latest/contributing.html 154 | - PyPI: https://pypi.org/project/marshmallow/ 155 | - Issues: https://github.com/marshmallow-code/marshmallow/issues 156 | - Donate: https://opencollective.com/marshmallow 157 | 158 | License 159 | ======= 160 | 161 | MIT licensed. See the bundled `LICENSE `_ file for more details. 162 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Bump version in `pyproject.toml` and update the changelog 4 | with today's date. 5 | 2. Commit: `git commit -m "Bump version and update changelog"` 6 | 3. Tag the commit: `git tag x.y.z` 7 | 4. Push: `git push --tags origin dev`. CI will take care of the 8 | PyPI release. 9 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Contact Information 2 | 3 | To report a security vulnerability, please use the 4 | [Tidelift security contact](https://tidelift.com/security). 5 | Tidelift will coordinate the fix and disclosure. 6 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | marshmallow.docset 2 | marshmallow.tgz 3 | -------------------------------------------------------------------------------- /docs/_static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/marshmallow/7266de0c42e26c521801b7c01417d1f738e8a314/docs/_static/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* Headings */ 2 | 3 | h2, h3, h4, h5, h6 { 4 | font-weight: 400; 5 | } 6 | 7 | /* UI elements: left and right sidebars, "Back to top" button, admonitions, Copy button */ 8 | .sidebar-drawer, .toc-drawer, .back-to-top, .admonition, .copybtn { 9 | /* Sans-serif system font stack */ 10 | font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif; 11 | } 12 | 13 | /* Hide ToC caption text within the main body (but leave them in the side-bar). */ 14 | /* https://github.com/hynek/structlog/blob/b488a8bf589a01aabc41e3bf8df81a9848cd426c/docs/_static/custom.css#L17-L20 */ 15 | #furo-main-content span.caption-text { 16 | display: none; 17 | } 18 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/marshmallow/7266de0c42e26c521801b7c01417d1f738e8a314/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/marshmallow-logo-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/marshmallow/7266de0c42e26c521801b7c01417d1f738e8a314/docs/_static/marshmallow-logo-200.png -------------------------------------------------------------------------------- /docs/_static/marshmallow-logo-with-title-for-dark-theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/marshmallow/7266de0c42e26c521801b7c01417d1f738e8a314/docs/_static/marshmallow-logo-with-title-for-dark-theme.png -------------------------------------------------------------------------------- /docs/_static/marshmallow-logo-with-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/marshmallow/7266de0c42e26c521801b7c01417d1f738e8a314/docs/_static/marshmallow-logo-with-title.png -------------------------------------------------------------------------------- /docs/_static/marshmallow-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/marshmallow/7266de0c42e26c521801b7c01417d1f738e8a314/docs/_static/marshmallow-logo.png -------------------------------------------------------------------------------- /docs/api_reference.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | .. toctree:: 4 | :caption: API Reference 5 | 6 | top_level 7 | marshmallow.schema 8 | marshmallow.fields 9 | marshmallow.decorators 10 | marshmallow.validate 11 | marshmallow.utils 12 | marshmallow.experimental.context 13 | marshmallow.exceptions 14 | 15 | Private API 16 | =========== 17 | 18 | .. toctree:: 19 | :caption: Private API 20 | 21 | marshmallow.types 22 | marshmallow.class_registry 23 | marshmallow.error_store 24 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. seealso:: 2 | Need help upgrading to newer versions? Check out the :doc:`upgrading guide `. 3 | 4 | .. include:: ../CHANGELOG.rst 5 | -------------------------------------------------------------------------------- /docs/code_of_conduct.rst: -------------------------------------------------------------------------------- 1 | Code of Conduct 2 | =============== 3 | 4 | This code of conduct applies to the marshmallow project and all associated 5 | projects in the `marshmallow-code `__ 6 | organization. 7 | 8 | 9 | .. _coc-when-something-happens: 10 | 11 | When something happens 12 | ---------------------- 13 | 14 | If you see a Code of Conduct violation, follow these steps: 15 | 16 | 1. Let the person know that what they did is not appropriate and ask 17 | them to stop and/or edit their message(s) or commits. 18 | 2. That person should immediately stop the behavior and correct the 19 | issue. 20 | 3. If this doesn’t happen, or if you're uncomfortable speaking up, 21 | :ref:`contact the maintainers `. 22 | 4. As soon as possible, a maintainer will look into the issue, and take 23 | :ref:`further action (see below) `, starting with 24 | a warning, then temporary block, then long-term repo or organization 25 | ban. 26 | 27 | When reporting, please include any relevant details, links, screenshots, 28 | context, or other information that may be used to better understand and 29 | resolve the situation. 30 | 31 | **The maintainer team will prioritize the well-being and comfort of the 32 | recipients of the violation over the comfort of the violator.** See 33 | :ref:`some examples below `. 34 | 35 | Our pledge 36 | ---------- 37 | 38 | In the interest of fostering an open and welcoming environment, we as 39 | contributors and maintainers of this project pledge to making 40 | participation in our community a harassment-free experience for 41 | everyone, regardless of age, body size, disability, ethnicity, gender 42 | identity and expression, level of experience, technical preferences, 43 | nationality, personal appearance, race, religion, or sexual identity and 44 | orientation. 45 | 46 | Our standards 47 | ------------- 48 | 49 | Examples of behavior that contributes to creating a positive environment 50 | include: 51 | 52 | - Using welcoming and inclusive language. 53 | - Being respectful of differing viewpoints and experiences. 54 | - Gracefully accepting constructive feedback. 55 | - Focusing on what is best for the community. 56 | - Showing empathy and kindness towards other community members. 57 | - Encouraging and raising up your peers in the project so you can all 58 | bask in hacks and glory. 59 | 60 | Examples of unacceptable behavior by participants include: 61 | 62 | - The use of sexualized language or imagery and unwelcome sexual 63 | attention or advances, including when simulated online. The only 64 | exception to sexual topics is channels/spaces specifically for topics 65 | of sexual identity. 66 | - Casual mention of slavery or indentured servitude and/or false 67 | comparisons of one's occupation or situation to slavery. Please 68 | consider using or asking about alternate terminology when referring 69 | to such metaphors in technology. 70 | - Making light of/making mocking comments about trigger warnings and 71 | content warnings. 72 | - Trolling, insulting/derogatory comments, and personal or political 73 | attacks. 74 | - Public or private harassment, deliberate intimidation, or threats. 75 | - Publishing others' private information, such as a physical or 76 | electronic address, without explicit permission. This includes any 77 | sort of "outing" of any aspect of someone's identity without their 78 | consent. 79 | - Publishing private screenshots or quotes of interactions in the 80 | context of this project without all quoted users' *explicit* consent. 81 | - Publishing of private communication that doesn't have to do with 82 | reporting harassment. 83 | - Any of the above even when `presented as "ironic" or 84 | "joking" `__. 85 | - Any attempt to present "reverse-ism" versions of the above as 86 | violations. Examples of reverse-isms are "reverse racism", "reverse 87 | sexism", "heterophobia", and "cisphobia". 88 | - Unsolicited explanations under the assumption that someone doesn't 89 | already know it. Ask before you teach! Don't assume what people's 90 | knowledge gaps are. 91 | - `Feigning or exaggerating 92 | surprise `__ when 93 | someone admits to not knowing something. 94 | - "`Well-actuallies `__" 95 | - Other conduct which could reasonably be considered inappropriate in a 96 | professional or community setting. 97 | 98 | Scope 99 | ----- 100 | 101 | This Code of Conduct applies both within spaces involving this project 102 | and in other spaces involving community members. This includes the 103 | repository, its Pull Requests and Issue tracker, its Twitter community, 104 | private email communications in the context of the project, and any 105 | events where members of the project are participating, as well as 106 | adjacent communities and venues affecting the project's members. 107 | 108 | Depending on the violation, the maintainers may decide that violations 109 | of this code of conduct that have happened outside of the scope of the 110 | community may deem an individual unwelcome, and take appropriate action 111 | to maintain the comfort and safety of its members. 112 | 113 | .. _coc-other-community-standards: 114 | 115 | Other community standards 116 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 117 | 118 | As a project on GitHub, this project is additionally covered by the 119 | `GitHub Community 120 | Guidelines `__. 121 | 122 | Enforcement of those guidelines after violations overlapping with the 123 | above are the responsibility of the entities, and enforcement may happen 124 | in any or all of the services/communities. 125 | 126 | Maintainer enforcement process 127 | ------------------------------ 128 | 129 | Once the maintainers get involved, they will follow a documented series 130 | of steps and do their best to preserve the well-being of project 131 | members. This section covers actual concrete steps. 132 | 133 | 134 | .. _coc-contacting-maintainers: 135 | 136 | Contacting maintainers 137 | ~~~~~~~~~~~~~~~~~~~~~~ 138 | 139 | As a small and young project, we don't yet have a Code of Conduct 140 | enforcement team. Hopefully that will be addressed as we grow, but for 141 | now, any issues should be addressed to `Steven Loria 142 | `__, via `email `__ 143 | or any other medium that you feel comfortable with. Using words like 144 | "marshmallow code of conduct" in your subject will help make sure your 145 | message is noticed quickly. 146 | 147 | 148 | .. _coc-further-enforcement: 149 | 150 | Further enforcement 151 | ~~~~~~~~~~~~~~~~~~~ 152 | 153 | If you've already followed the :ref:`initial enforcement steps 154 | `, these are the steps maintainers will 155 | take for further enforcement, as needed: 156 | 157 | 1. Repeat the request to stop. 158 | 2. If the person doubles down, they will be given an official warning. The PR or Issue 159 | may be locked. 160 | 3. If the behavior continues or is repeated later, the person will be 161 | blocked from participating for 24 hours. 162 | 4. If the behavior continues or is repeated after the temporary block, a 163 | long-term (6-12mo) ban will be used. 164 | 5. If after this the behavior still continues, a permanent ban may be 165 | enforced. 166 | 167 | On top of this, maintainers may remove any offending messages, images, 168 | contributions, etc, as they deem necessary. 169 | 170 | Maintainers reserve full rights to skip any of these steps, at their 171 | discretion, if the violation is considered to be a serious and/or 172 | immediate threat to the health and well-being of members of the 173 | community. These include any threats, serious physical or verbal 174 | attacks, and other such behavior that would be completely unacceptable 175 | in any social setting that puts our members at risk. 176 | 177 | Members expelled from events or venues with any sort of paid attendance 178 | will not be refunded. 179 | 180 | Who watches the watchers? 181 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 182 | 183 | Maintainers and other leaders who do not follow or enforce the Code of 184 | Conduct in good faith may face temporary or permanent repercussions as 185 | determined by other members of the project's leadership. These may 186 | include anything from removal from the maintainer team to a permanent 187 | ban from the community. 188 | 189 | Additionally, as a project hosted on GitHub, :ref:`their Code of 190 | Conduct may be applied against maintainers of this project 191 | `, externally of this project's 192 | procedures. 193 | 194 | 195 | .. _coc-enforcement-examples: 196 | 197 | Enforcement examples 198 | -------------------- 199 | 200 | The best case 201 | ~~~~~~~~~~~~~ 202 | 203 | The vast majority of situations work out like this. This interaction is 204 | common, and generally positive. 205 | 206 | Alex: "Yeah I used X and it was really crazy!" 207 | 208 | Patt (not a maintainer): "Hey, could you not use that word? What 209 | about 'ridiculous' instead?" 210 | 211 | Alex: "oh sorry, sure." -> edits old comment to say "it was really 212 | confusing!" 213 | 214 | The maintainer case 215 | ~~~~~~~~~~~~~~~~~~~ 216 | 217 | Sometimes, though, you need to get maintainers involved. Maintainers 218 | will do their best to resolve conflicts, but people who were harmed by 219 | something **will take priority**. 220 | 221 | Patt: "Honestly, sometimes I just really hate using $library and 222 | anyone who uses it probably sucks at their job." 223 | 224 | Alex: "Whoa there, could you dial it back a bit? There's a CoC thing 225 | about attacking folks' tech use like that." 226 | 227 | Patt: "I'm not attacking anyone, what's your problem?" 228 | 229 | Alex: "@maintainers hey uh. Can someone look at this issue? Patt is 230 | getting a bit aggro. I tried to nudge them about it, but nope." 231 | 232 | KeeperOfCommitBits: (on issue) "Hey Patt, maintainer here. Could you 233 | tone it down? This sort of attack is really not okay in this space." 234 | 235 | Patt: "Leave me alone I haven't said anything bad wtf is wrong with 236 | you." 237 | 238 | KeeperOfCommitBits: (deletes user's comment), "@patt I mean it. 239 | Please refer to the CoC over at (URL to this CoC) if you have 240 | questions, but you can consider this an actual warning. I'd 241 | appreciate it if you reworded your messages in this thread, since 242 | they made folks there uncomfortable. Let's try and be kind, yeah?" 243 | 244 | Patt: "@KeeperOfCommitBits Okay sorry. I'm just frustrated and I'm kinda 245 | burnt out and I guess I got carried away. I'll DM Alex a note 246 | apologizing and edit my messages. Sorry for the trouble." 247 | 248 | KeeperOfCommitBits: "@patt Thanks for that. I hear you on the 249 | stress. Burnout sucks :/. Have a good one!" 250 | 251 | The nope case 252 | ~~~~~~~~~~~~~ 253 | 254 | PepeTheFrog🐸: "Hi, I am a literal actual nazi and I think white 255 | supremacists are quite fashionable." 256 | 257 | Patt: "NOOOOPE. OH NOPE NOPE." 258 | 259 | Alex: "JFC NO. NOPE. @KeeperOfCommitBits NOPE NOPE LOOK HERE" 260 | 261 | KeeperOfCommitBits: "👀 Nope. NOPE NOPE NOPE. 🔥" 262 | 263 | PepeTheFrog🐸 has been banned from all organization or user 264 | repositories belonging to KeeperOfCommitBits. 265 | 266 | Attribution 267 | ----------- 268 | 269 | This Code of Conduct is based on `Trio's Code of Conduct `_, which is based on the 270 | `WeAllJS Code of Conduct `__, which 271 | is itself based on `Contributor 272 | Covenant `__, version 1.4, available at 273 | https://contributor-covenant.org/version/1/4, and the LGBTQ in Technology 274 | Slack `Code of Conduct `__. 275 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | extensions = [ 4 | "autodocsumm", 5 | "sphinx.ext.autodoc", 6 | "sphinx.ext.autodoc.typehints", 7 | "sphinx.ext.intersphinx", 8 | "sphinx.ext.viewcode", 9 | "sphinx_copybutton", 10 | "sphinx_issues", 11 | "sphinxext.opengraph", 12 | ] 13 | 14 | primary_domain = "py" 15 | default_role = "py:obj" 16 | 17 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 18 | 19 | issues_github_path = "marshmallow-code/marshmallow" 20 | 21 | source_suffix = ".rst" 22 | master_doc = "index" 23 | 24 | project = "marshmallow" 25 | copyright = "Steven Loria and contributors" # noqa: A001 26 | 27 | version = release = importlib.metadata.version("marshmallow") 28 | 29 | exclude_patterns = ["_build"] 30 | # Ignore WARNING: more than one target found for cross-reference 'Schema': marshmallow.schema.Schema, marshmallow.Schema 31 | suppress_warnings = ["ref.python"] 32 | 33 | # THEME 34 | 35 | html_theme = "furo" 36 | html_theme_options = { 37 | "light_logo": "marshmallow-logo-with-title.png", 38 | "dark_logo": "marshmallow-logo-with-title-for-dark-theme.png", 39 | "source_repository": "https://github.com/marshmallow-code/marshmallow", 40 | "source_branch": "dev", 41 | "source_directory": "docs/", 42 | "sidebar_hide_name": True, 43 | "light_css_variables": { 44 | # Serif system font stack: https://systemfontstack.com/ 45 | "font-stack": "Iowan Old Style, Apple Garamond, Baskerville, Times New Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;", 46 | }, 47 | "top_of_page_buttons": ["view"], 48 | } 49 | pygments_dark_style = "lightbulb" 50 | html_favicon = "_static/favicon.ico" 51 | html_static_path = ["_static"] 52 | html_css_files = ["custom.css"] 53 | html_copy_source = False # Don't copy source files to _build/sources 54 | html_show_sourcelink = False # Don't link to source files 55 | ogp_image = "_static/marshmallow-logo-200.png" 56 | 57 | # Strip the dollar prompt when copying code 58 | # https://sphinx-copybutton.readthedocs.io/en/latest/use.html#strip-and-configure-input-prompts-for-code-cells 59 | copybutton_prompt_text = "$ " 60 | 61 | autodoc_default_options = { 62 | "exclude-members": "__new__", 63 | # Don't show signatures in the summary tables 64 | "autosummary-nosignatures": True, 65 | # Don't render summaries for classes within modules 66 | "autosummary-no-nesting": True, 67 | } 68 | # Only display type hints next to params but not within the signature 69 | # to avoid the signature from getting too long 70 | autodoc_typehints = "description" 71 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/custom_fields.rst: -------------------------------------------------------------------------------- 1 | Custom fields 2 | ============= 3 | 4 | There are three ways to create a custom-formatted field for a `Schema `: 5 | 6 | - Create a custom :class:`Field ` class 7 | - Use a :class:`Method ` field 8 | - Use a :class:`Function ` field 9 | 10 | The method you choose will depend on the manner in which you intend to reuse the field. 11 | 12 | Creating a field class 13 | ---------------------- 14 | 15 | To create a custom field class, create a subclass of :class:`marshmallow.fields.Field` and implement its :meth:`_serialize ` and/or :meth:`_deserialize ` methods. 16 | Field's type argument is the internal type, i.e. the type that the field deserializes to. 17 | 18 | .. code-block:: python 19 | 20 | from marshmallow import fields, ValidationError 21 | 22 | 23 | class PinCode(fields.Field[list[int]]): 24 | """Field that serializes to a string of numbers and deserializes 25 | to a list of numbers. 26 | """ 27 | 28 | def _serialize(self, value, attr, obj, **kwargs): 29 | if value is None: 30 | return "" 31 | return "".join(str(d) for d in value) 32 | 33 | def _deserialize(self, value, attr, data, **kwargs): 34 | try: 35 | return [int(c) for c in value] 36 | except ValueError as error: 37 | raise ValidationError("Pin codes must contain only digits.") from error 38 | 39 | 40 | class UserSchema(Schema): 41 | name = fields.String() 42 | email = fields.String() 43 | created_at = fields.DateTime() 44 | pin_code = PinCode() 45 | 46 | Method fields 47 | ------------- 48 | 49 | A :class:`Method ` field will serialize to the value returned by a method of the Schema. The method must take an ``obj`` parameter which is the object to be serialized. 50 | 51 | .. code-block:: python 52 | 53 | class UserSchema(Schema): 54 | name = fields.String() 55 | email = fields.String() 56 | created_at = fields.DateTime() 57 | since_created = fields.Method("get_days_since_created") 58 | 59 | def get_days_since_created(self, obj): 60 | return dt.datetime.now().day - obj.created_at.day 61 | 62 | Function fields 63 | --------------- 64 | 65 | A :class:`Function ` field will serialize the value of a function that is passed directly to it. Like a :class:`Method ` field, the function must take a single argument ``obj``. 66 | 67 | 68 | .. code-block:: python 69 | 70 | class UserSchema(Schema): 71 | name = fields.String() 72 | email = fields.String() 73 | created_at = fields.DateTime() 74 | uppername = fields.Function(lambda obj: obj.name.upper()) 75 | 76 | `Method` and `Function` field deserialization 77 | --------------------------------------------- 78 | 79 | Both :class:`Function ` and :class:`Method ` receive an optional ``deserialize`` argument which defines how the field should be deserialized. The method or function passed to ``deserialize`` receives the input value for the field. 80 | 81 | .. code-block:: python 82 | 83 | class UserSchema(Schema): 84 | # `Method` takes a method name (str), Function takes a callable 85 | balance = fields.Method("get_balance", deserialize="load_balance") 86 | 87 | def get_balance(self, obj): 88 | return obj.income - obj.debt 89 | 90 | def load_balance(self, value): 91 | return float(value) 92 | 93 | 94 | schema = UserSchema() 95 | result = schema.load({"balance": "100.00"}) 96 | result["balance"] # => 100.0 97 | 98 | .. _using_context: 99 | 100 | Using context 101 | ------------- 102 | 103 | A field may need information about its environment to know how to (de)serialize a value. 104 | 105 | You can use the experimental `Context ` class 106 | to set and retrieve context. 107 | 108 | Let's say your ``UserSchema`` needs to output 109 | whether or not a ``User`` is the author of a ``Blog`` or 110 | whether a certain word appears in a ``Blog's`` title. 111 | 112 | .. code-block:: python 113 | 114 | import typing 115 | from dataclasses import dataclass 116 | 117 | from marshmallow import Schema, fields 118 | from marshmallow.experimental.context import Context 119 | 120 | 121 | @dataclass 122 | class User: 123 | name: str 124 | 125 | 126 | @dataclass 127 | class Blog: 128 | title: str 129 | author: User 130 | 131 | 132 | class ContextDict(typing.TypedDict): 133 | blog: Blog 134 | 135 | 136 | class UserSchema(Schema): 137 | name = fields.String() 138 | 139 | is_author = fields.Function( 140 | lambda user: user == Context[ContextDict].get()["blog"].author 141 | ) 142 | likes_bikes = fields.Method("writes_about_bikes") 143 | 144 | def writes_about_bikes(self, user: User) -> bool: 145 | return "bicycle" in Context[ContextDict].get()["blog"].title.lower() 146 | 147 | .. note:: 148 | You can use `Context.get ` 149 | within custom fields, pre-/post-processing methods, and validators. 150 | 151 | When (de)serializing, set the context by using `Context ` as a context manager. 152 | 153 | .. code-block:: python 154 | 155 | 156 | user = User("Freddie Mercury", "fred@queen.com") 157 | blog = Blog("Bicycle Blog", author=user) 158 | 159 | schema = UserSchema() 160 | with Context({"blog": blog}): 161 | result = schema.dump(user) 162 | print(result["is_author"]) # => True 163 | print(result["likes_bikes"]) # => True 164 | 165 | 166 | Customizing error messages 167 | -------------------------- 168 | 169 | Validation error messages for fields can be configured at the class or instance level. 170 | 171 | At the class level, default error messages are defined as a mapping from error codes to error messages. 172 | 173 | .. code-block:: python 174 | 175 | from marshmallow import fields 176 | 177 | 178 | class MyDate(fields.Date): 179 | default_error_messages = {"invalid": "Please provide a valid date."} 180 | 181 | .. note:: 182 | A `Field's` ``default_error_messages`` dictionary gets merged with its parent classes' ``default_error_messages`` dictionaries. 183 | 184 | Error messages can also be passed to a `Field's` constructor. 185 | 186 | .. code-block:: python 187 | 188 | from marshmallow import Schema, fields 189 | 190 | 191 | class UserSchema(Schema): 192 | name = fields.Str( 193 | required=True, error_messages={"required": "Please provide a name."} 194 | ) 195 | 196 | 197 | Next steps 198 | ---------- 199 | 200 | - Need to add schema-level validation, post-processing, or error handling behavior? See the :doc:`extending/index` page. 201 | - For example applications using marshmallow, check out the :doc:`examples/index` page. 202 | -------------------------------------------------------------------------------- /docs/dashing.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marshmallow", 3 | "package": "marshmallow", 4 | "index": "_build/index.html", 5 | "selectors": { 6 | "dl.class dt": { 7 | "type": "Class", 8 | "attr": "id" 9 | }, 10 | "dl.exception dt": { 11 | "type": "Exception", 12 | "attr": "id" 13 | }, 14 | "dl.function dt": { 15 | "type": "Function", 16 | "attr": "id" 17 | } 18 | }, 19 | "icon32x32": "", 20 | "allowJS": false, 21 | "ExternalURL": "" 22 | } 23 | -------------------------------------------------------------------------------- /docs/donate.rst: -------------------------------------------------------------------------------- 1 | ****** 2 | Donate 3 | ****** 4 | 5 | If you find marshmallow useful, please consider supporting the team with a donation: 6 | 7 | .. image:: https://opencollective.com/marshmallow/donate/button@2x.png 8 | :target: https://opencollective.com/marshmallow 9 | :alt: Donate to our Open Collective 10 | :height: 50px 11 | 12 | Your donation keeps marshmallow healthy and maintained. 13 | -------------------------------------------------------------------------------- /docs/examples/index.rst: -------------------------------------------------------------------------------- 1 | ******** 2 | Examples 3 | ******** 4 | 5 | The below examples demonstrate how to use marshmallow in various contexts. 6 | To run each example, you will need to have `uv `_ installed. 7 | The examples use `PEP 723 inline metadata `_ 8 | to declare the dependencies of each script. ``uv`` will install the 9 | dependencies automatically when running these scripts. 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | 14 | validating_package_json 15 | quotes_api 16 | inflection 17 | -------------------------------------------------------------------------------- /docs/examples/inflection.rst: -------------------------------------------------------------------------------- 1 | ***************************** 2 | Inflection (camel-cased keys) 3 | ***************************** 4 | 5 | HTTP APIs will often use camel-cased keys for their input and output representations. This example shows how you can use the 6 | `Schema.on_bind_field ` hook to automatically inflect keys. 7 | 8 | .. literalinclude:: ../../examples/inflection_example.py 9 | :language: python 10 | 11 | To run the example: 12 | 13 | .. code-block:: shell-session 14 | 15 | $ uv run examples/inflection_example.py 16 | Loaded data: 17 | {'first_name': 'David', 'last_name': 'Bowie'} 18 | Dumped data: 19 | {'firstName': 'David', 'lastName': 'Bowie'} 20 | -------------------------------------------------------------------------------- /docs/examples/quotes_api.rst: -------------------------------------------------------------------------------- 1 | ******************************* 2 | Quotes API (Flask + SQLAlchemy) 3 | ******************************* 4 | 5 | Below is a full example of a REST API for a quotes app using `Flask `_ and `SQLAlchemy `_ with marshmallow. It demonstrates a number of features, including: 6 | 7 | - Custom validation 8 | - Nesting fields 9 | - Using ``dump_only=True`` to specify read-only fields 10 | - Output filtering using the ``only`` parameter 11 | - Using `@pre_load ` to preprocess input data. 12 | 13 | .. literalinclude:: ../../examples/flask_example.py 14 | :language: python 15 | 16 | 17 | **Using The API** 18 | 19 | Run the app. 20 | 21 | .. code-block:: shell-session 22 | 23 | $ uv run examples/flask_example.py 24 | 25 | We'll use the `httpie cli `_ to send requests 26 | Install it with ``uv``. 27 | 28 | .. code-block:: shell-session 29 | 30 | $ uv tool install httpie 31 | 32 | First we'll POST some quotes. 33 | 34 | .. code-block:: shell-session 35 | 36 | $ http POST :5000/quotes/ author="Tim Peters" content="Beautiful is better than ugly." 37 | $ http POST :5000/quotes/ author="Tim Peters" content="Now is better than never." 38 | $ http POST :5000/quotes/ author="Peter Hintjens" content="Simplicity is always better than functionality." 39 | 40 | 41 | If we provide invalid input data, we get 400 error response. Let's omit "author" from the input data. 42 | 43 | .. code-block:: shell-session 44 | 45 | $ http POST :5000/quotes/ content="I have no author" 46 | { 47 | "author": [ 48 | "Data not provided." 49 | ] 50 | } 51 | 52 | Now we can GET a list of all the quotes. 53 | 54 | .. code-block:: shell-session 55 | 56 | $ http :5000/quotes/ 57 | { 58 | "quotes": [ 59 | { 60 | "content": "Beautiful is better than ugly.", 61 | "id": 1 62 | }, 63 | { 64 | "content": "Now is better than never.", 65 | "id": 2 66 | }, 67 | { 68 | "content": "Simplicity is always better than functionality.", 69 | "id": 3 70 | } 71 | ] 72 | } 73 | 74 | We can also GET the quotes for a single author. 75 | 76 | .. code-block:: shell-session 77 | 78 | $ http :5000/authors/1 79 | { 80 | "author": { 81 | "first": "Tim", 82 | "formatted_name": "Peters, Tim", 83 | "id": 1, 84 | "last": "Peters" 85 | }, 86 | "quotes": [ 87 | { 88 | "content": "Beautiful is better than ugly.", 89 | "id": 1 90 | }, 91 | { 92 | "content": "Now is better than never.", 93 | "id": 2 94 | } 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /docs/examples/validating_package_json.rst: -------------------------------------------------------------------------------- 1 | *************************** 2 | Validating ``package.json`` 3 | *************************** 4 | 5 | marshmallow can be used to validate configuration according to a schema. 6 | Below is a schema that could be used to validate 7 | ``package.json`` files. This example demonstrates the following features: 8 | 9 | 10 | - Validation and deserialization using `Schema.load ` 11 | - :doc:`Custom fields <../custom_fields>` 12 | - Specifying deserialization keys using ``data_key`` 13 | - Including unknown keys using ``unknown = INCLUDE`` 14 | 15 | .. literalinclude:: ../../examples/package_json_example.py 16 | :language: python 17 | 18 | 19 | Given the following ``package.json`` file... 20 | 21 | .. literalinclude:: ../../examples/package.json 22 | :language: json 23 | 24 | 25 | We can validate it using the above script. 26 | 27 | .. code-block:: shell-session 28 | 29 | $ uv run examples/package_json_example.py < examples/package.json 30 | {'description': 'The Pythonic JavaScript toolkit', 31 | 'dev_dependencies': {'pest': '^23.4.1'}, 32 | 'license': 'MIT', 33 | 'main': 'index.js', 34 | 'name': 'dunderscore', 35 | 'scripts': {'test': 'pest'}, 36 | 'version': } 37 | 38 | Notice that our custom field deserialized the version string to a ``Version`` object. 39 | 40 | But if we pass an invalid package.json file... 41 | 42 | .. literalinclude:: ../../examples/invalid_package.json 43 | :language: json 44 | 45 | We see the corresponding error messages. 46 | 47 | .. code-block:: shell-session 48 | 49 | $ uv run examples/package_json_example.py < examples/invalid_package.json 50 | ERROR: package.json is invalid 51 | {'homepage': ['Not a valid URL.'], 'version': ['Not a valid version.']} 52 | -------------------------------------------------------------------------------- /docs/extending/custom_error_handling.rst: -------------------------------------------------------------------------------- 1 | Custom error handling 2 | ===================== 3 | 4 | By default, `Schema.load ` will raise a :exc:`ValidationError ` if passed invalid data. 5 | 6 | You can specify a custom error-handling function for a :class:`Schema` by overriding the `handle_error ` method. The method receives the :exc:`ValidationError ` and the original input data to be deserialized. 7 | 8 | .. code-block:: python 9 | 10 | import logging 11 | from marshmallow import Schema, fields 12 | 13 | 14 | class AppError(Exception): 15 | pass 16 | 17 | 18 | class UserSchema(Schema): 19 | email = fields.Email() 20 | 21 | def handle_error(self, exc, data, **kwargs): 22 | """Log and raise our custom exception when (de)serialization fails.""" 23 | logging.error(exc.messages) 24 | raise AppError("An error occurred with input: {0}".format(data)) 25 | 26 | 27 | schema = UserSchema() 28 | schema.load({"email": "invalid-email"}) # raises AppError 29 | -------------------------------------------------------------------------------- /docs/extending/custom_error_messages.rst: -------------------------------------------------------------------------------- 1 | Custom error messages 2 | ===================== 3 | 4 | To customize the schema-level error messages that `load ` and `loads ` use when raising a `ValidationError `, override the `error_messages ` class variable: 5 | 6 | .. code-block:: python 7 | 8 | class MySchema(Schema): 9 | error_messages = { 10 | "unknown": "Custom unknown field error message.", 11 | "type": "Custom invalid type error message.", 12 | } 13 | 14 | 15 | Field-level error message defaults can be set on `Field.default_error_messages `. 16 | 17 | 18 | .. code-block:: python 19 | 20 | from marshmallow import Schema, fields 21 | 22 | fields.Field.default_error_messages["required"] = "You missed something!" 23 | 24 | 25 | class ArtistSchema(Schema): 26 | name = fields.Str(required=True) 27 | label = fields.Str(required=True, error_messages={"required": "Label missing."}) 28 | 29 | 30 | print(ArtistSchema().validate({})) 31 | # {'label': ['Label missing.'], 'name': ['You missed something!']} 32 | -------------------------------------------------------------------------------- /docs/extending/custom_options.rst: -------------------------------------------------------------------------------- 1 | Custom `class Meta ` options 2 | ===================================================== 3 | 4 | `class Meta ` options are a way to configure and modify a `Schema's ` behavior. See `marshmallow.Schema.Meta` for a listing of available options. 5 | 6 | You can add custom `class Meta ` options by subclassing `marshmallow.SchemaOpts`. 7 | 8 | Example: Enveloping, revisited 9 | ------------------------------ 10 | 11 | Let's build upon the :ref:`previous enveloping implementation ` above for adding an envelope to serialized output. 12 | This time, we will allow the envelope key to be customizable with `class Meta ` options. 13 | 14 | :: 15 | 16 | # Example outputs 17 | { 18 | 'user': { 19 | 'name': 'Keith', 20 | 'email': 'keith@stones.com' 21 | } 22 | } 23 | # List output 24 | { 25 | 'users': [{'name': 'Keith'}, {'name': 'Mick'}] 26 | } 27 | 28 | 29 | First, we'll add our namespace configuration to a custom options class. 30 | 31 | .. code-block:: python 32 | 33 | from marshmallow import Schema, SchemaOpts 34 | 35 | 36 | class NamespaceOpts(SchemaOpts): 37 | """Same as the default class Meta options, but adds "name" and 38 | "plural_name" options for enveloping. 39 | """ 40 | 41 | def __init__(self, meta, **kwargs): 42 | SchemaOpts.__init__(self, meta, **kwargs) 43 | self.name = getattr(meta, "name", None) 44 | self.plural_name = getattr(meta, "plural_name", self.name) 45 | 46 | 47 | Then we create a custom :class:`Schema` that uses our options class. 48 | 49 | .. code-block:: python 50 | 51 | class NamespacedSchema(Schema): 52 | OPTIONS_CLASS = NamespaceOpts 53 | 54 | @pre_load(pass_many=True) 55 | def unwrap_envelope(self, data, many, **kwargs): 56 | key = self.opts.plural_name if many else self.opts.name 57 | return data[key] 58 | 59 | @post_dump(pass_many=True) 60 | def wrap_with_envelope(self, data, many, **kwargs): 61 | key = self.opts.plural_name if many else self.opts.name 62 | return {key: data} 63 | 64 | 65 | Our application schemas can now inherit from our custom schema class. 66 | 67 | .. code-block:: python 68 | 69 | class UserSchema(NamespacedSchema): 70 | name = fields.String() 71 | email = fields.Email() 72 | 73 | class Meta: 74 | name = "user" 75 | plural_name = "users" 76 | 77 | 78 | ser = UserSchema() 79 | user = User("Keith", email="keith@stones.com") 80 | result = ser.dump(user) 81 | result # {"user": {"name": "Keith", "email": "keith@stones.com"}} 82 | -------------------------------------------------------------------------------- /docs/extending/index.rst: -------------------------------------------------------------------------------- 1 | Extending schemas 2 | ================= 3 | 4 | The guides below demonstrate how to extend schemas in various ways. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | pre_and_post_processing_methods 10 | schema_validation 11 | using_original_input_data 12 | overriding_attribute_access 13 | custom_error_handling 14 | custom_options 15 | custom_error_messages 16 | -------------------------------------------------------------------------------- /docs/extending/overriding_attribute_access.rst: -------------------------------------------------------------------------------- 1 | Overriding how attributes are accessed 2 | ====================================== 3 | 4 | By default, marshmallow uses `utils.get_value ` to pull attributes from various types of objects for serialization. This will work for *most* use cases. 5 | 6 | However, if you want to specify how values are accessed from an object, you can override the :meth:`get_attribute ` method. 7 | 8 | .. code-block:: python 9 | 10 | class UserDictSchema(Schema): 11 | name = fields.Str() 12 | email = fields.Email() 13 | 14 | # If we know we're only serializing dictionaries, we can 15 | # use dict.get for all input objects 16 | def get_attribute(self, obj, key, default): 17 | return obj.get(key, default) 18 | -------------------------------------------------------------------------------- /docs/extending/pre_and_post_processing_methods.rst: -------------------------------------------------------------------------------- 1 | Pre-processing and post-processing methods 2 | ========================================== 3 | 4 | Decorator API 5 | ------------- 6 | 7 | Data pre-processing and post-processing methods can be registered using the `pre_load `, `post_load `, `pre_dump `, and `post_dump ` decorators. 8 | 9 | 10 | .. code-block:: python 11 | 12 | from marshmallow import Schema, fields, post_load 13 | 14 | 15 | class UserSchema(Schema): 16 | name = fields.Str() 17 | slug = fields.Str() 18 | 19 | @post_load 20 | def slugify_name(self, in_data, **kwargs): 21 | in_data["slug"] = in_data["slug"].lower().strip().replace(" ", "-") 22 | return in_data 23 | 24 | 25 | schema = UserSchema() 26 | result = schema.load({"name": "Steve", "slug": "Steve Loria "}) 27 | result["slug"] # => 'steve-loria' 28 | 29 | Passing "many" 30 | -------------- 31 | 32 | By default, pre- and post-processing methods receive one object/datum at a time, transparently handling the ``many`` parameter passed to the ``Schema``'s `~marshmallow.Schema.dump`/`~marshmallow.Schema.load` method at runtime. 33 | 34 | In cases where your pre- and post-processing methods needs to handle the input collection when processing multiple objects, add ``pass_many=True`` to the method decorators. 35 | 36 | Your method will then receive the input data (which may be a single datum or a collection, depending on the dump/load call). 37 | 38 | .. _enveloping_1: 39 | 40 | Example: Enveloping 41 | ------------------- 42 | 43 | One common use case is to wrap data in a namespace upon serialization and unwrap the data during deserialization. 44 | 45 | .. code-block:: python 46 | 47 | from marshmallow import Schema, fields, pre_load, post_load, post_dump 48 | 49 | 50 | class BaseSchema(Schema): 51 | # Custom options 52 | __envelope__ = {"single": None, "many": None} 53 | __model__ = User 54 | 55 | def get_envelope_key(self, many): 56 | """Helper to get the envelope key.""" 57 | key = self.__envelope__["many"] if many else self.__envelope__["single"] 58 | assert key is not None, "Envelope key undefined" 59 | return key 60 | 61 | @pre_load(pass_many=True) 62 | def unwrap_envelope(self, data, many, **kwargs): 63 | key = self.get_envelope_key(many) 64 | return data[key] 65 | 66 | @post_dump(pass_many=True) 67 | def wrap_with_envelope(self, data, many, **kwargs): 68 | key = self.get_envelope_key(many) 69 | return {key: data} 70 | 71 | @post_load 72 | def make_object(self, data, **kwargs): 73 | return self.__model__(**data) 74 | 75 | 76 | class UserSchema(BaseSchema): 77 | __envelope__ = {"single": "user", "many": "users"} 78 | __model__ = User 79 | name = fields.Str() 80 | email = fields.Email() 81 | 82 | 83 | user_schema = UserSchema() 84 | 85 | user = User("Mick", email="mick@stones.org") 86 | user_data = user_schema.dump(user) 87 | # {'user': {'email': 'mick@stones.org', 'name': 'Mick'}} 88 | 89 | users = [ 90 | User("Keith", email="keith@stones.org"), 91 | User("Charlie", email="charlie@stones.org"), 92 | ] 93 | users_data = user_schema.dump(users, many=True) 94 | # {'users': [{'email': 'keith@stones.org', 'name': 'Keith'}, 95 | # {'email': 'charlie@stones.org', 'name': 'Charlie'}]} 96 | 97 | user_objs = user_schema.load(users_data, many=True) 98 | # [, ] 99 | 100 | Raising errors in pre-/post-processor methods 101 | --------------------------------------------- 102 | 103 | Pre- and post-processing methods may raise a `ValidationError `. By default, errors will be stored on the ``"_schema"`` key in the errors dictionary. 104 | 105 | .. code-block:: python 106 | 107 | from marshmallow import Schema, fields, ValidationError, pre_load 108 | 109 | 110 | class BandSchema(Schema): 111 | name = fields.Str() 112 | 113 | @pre_load 114 | def unwrap_envelope(self, data, **kwargs): 115 | if "data" not in data: 116 | raise ValidationError('Input data must have a "data" key.') 117 | return data["data"] 118 | 119 | 120 | sch = BandSchema() 121 | try: 122 | sch.load({"name": "The Band"}) 123 | except ValidationError as err: 124 | err.messages 125 | # {'_schema': ['Input data must have a "data" key.']} 126 | 127 | If you want to store and error on a different key, pass the key name as the second argument to `ValidationError `. 128 | 129 | .. code-block:: python 130 | 131 | from marshmallow import Schema, fields, ValidationError, pre_load 132 | 133 | 134 | class BandSchema(Schema): 135 | name = fields.Str() 136 | 137 | @pre_load 138 | def unwrap_envelope(self, data, **kwargs): 139 | if "data" not in data: 140 | raise ValidationError( 141 | 'Input data must have a "data" key.', "_preprocessing" 142 | ) 143 | return data["data"] 144 | 145 | 146 | sch = BandSchema() 147 | try: 148 | sch.load({"name": "The Band"}) 149 | except ValidationError as err: 150 | err.messages 151 | # {'_preprocessing': ['Input data must have a "data" key.']} 152 | 153 | Pre-/post-processor invocation order 154 | ------------------------------------ 155 | 156 | In summary, the processing pipeline for deserialization is as follows: 157 | 158 | 1. ``@pre_load(pass_many=True)`` methods 159 | 2. ``@pre_load(pass_many=False)`` methods 160 | 3. ``load(in_data, many)`` (validation and deserialization) 161 | 4. ``@validates`` methods (field validators) 162 | 5. ``@validates_schema`` methods (schema validators) 163 | 6. ``@post_load(pass_many=True)`` methods 164 | 7. ``@post_load(pass_many=False)`` methods 165 | 166 | The pipeline for serialization is similar, except that the ``pass_many=True`` processors are invoked *after* the ``pass_many=False`` processors and there are no validators. 167 | 168 | 1. ``@pre_dump(pass_many=False)`` methods 169 | 2. ``@pre_dump(pass_many=True)`` methods 170 | 3. ``dump(obj, many)`` (serialization) 171 | 4. ``@post_dump(pass_many=False)`` methods 172 | 5. ``@post_dump(pass_many=True)`` methods 173 | 174 | 175 | .. warning:: 176 | 177 | You may register multiple processor methods on a Schema. Keep in mind, however, that **the invocation order of decorated methods of the same type is not guaranteed**. If you need to guarantee order of processing steps, you should put them in the same method. 178 | 179 | 180 | .. code-block:: python 181 | 182 | from marshmallow import Schema, fields, pre_load 183 | 184 | 185 | # YES 186 | class MySchema(Schema): 187 | field_a = fields.Raw() 188 | 189 | @pre_load 190 | def preprocess(self, data, **kwargs): 191 | step1_data = self.step1(data) 192 | step2_data = self.step2(step1_data) 193 | return step2_data 194 | 195 | def step1(self, data): ... 196 | 197 | # Depends on step1 198 | def step2(self, data): ... 199 | 200 | 201 | # NO 202 | class MySchema(Schema): 203 | field_a = fields.Raw() 204 | 205 | @pre_load 206 | def step1(self, data, **kwargs): ... 207 | 208 | # Depends on step1 209 | @pre_load 210 | def step2(self, data, **kwargs): ... 211 | -------------------------------------------------------------------------------- /docs/extending/schema_validation.rst: -------------------------------------------------------------------------------- 1 | .. _schema_validation: 2 | 3 | Schema-level validation 4 | ======================= 5 | 6 | You can register schema-level validation functions for a :class:`Schema` using the `marshmallow.validates_schema ` decorator. By default, schema-level validation errors will be stored on the ``_schema`` key of the errors dictionary. 7 | 8 | .. code-block:: python 9 | 10 | from marshmallow import Schema, fields, validates_schema, ValidationError 11 | 12 | 13 | class NumberSchema(Schema): 14 | field_a = fields.Integer() 15 | field_b = fields.Integer() 16 | 17 | @validates_schema 18 | def validate_numbers(self, data, **kwargs): 19 | if data["field_b"] >= data["field_a"]: 20 | raise ValidationError("field_a must be greater than field_b") 21 | 22 | 23 | schema = NumberSchema() 24 | try: 25 | schema.load({"field_a": 1, "field_b": 2}) 26 | except ValidationError as err: 27 | err.messages["_schema"] 28 | # => ["field_a must be greater than field_b"] 29 | 30 | Storing errors on specific fields 31 | --------------------------------- 32 | 33 | It is possible to report errors on fields and subfields using a `dict`. 34 | 35 | When multiple schema-leval validator return errors, the error structures are merged together in the :exc:`ValidationError ` raised at the end of the validation. 36 | 37 | .. code-block:: python 38 | 39 | from marshmallow import Schema, fields, validates_schema, ValidationError 40 | 41 | 42 | class NumberSchema(Schema): 43 | field_a = fields.Integer() 44 | field_b = fields.Integer() 45 | field_c = fields.Integer() 46 | field_d = fields.Integer() 47 | 48 | @validates_schema 49 | def validate_lower_bound(self, data, **kwargs): 50 | errors = {} 51 | if data["field_b"] <= data["field_a"]: 52 | errors["field_b"] = ["field_b must be greater than field_a"] 53 | if data["field_c"] <= data["field_a"]: 54 | errors["field_c"] = ["field_c must be greater than field_a"] 55 | if errors: 56 | raise ValidationError(errors) 57 | 58 | @validates_schema 59 | def validate_upper_bound(self, data, **kwargs): 60 | errors = {} 61 | if data["field_b"] >= data["field_d"]: 62 | errors["field_b"] = ["field_b must be lower than field_d"] 63 | if data["field_c"] >= data["field_d"]: 64 | errors["field_c"] = ["field_c must be lower than field_d"] 65 | if errors: 66 | raise ValidationError(errors) 67 | 68 | 69 | schema = NumberSchema() 70 | try: 71 | schema.load({"field_a": 3, "field_b": 2, "field_c": 1, "field_d": 0}) 72 | except ValidationError as err: 73 | err.messages 74 | # => { 75 | # 'field_b': [ 76 | # 'field_b must be greater than field_a', 77 | # 'field_b must be lower than field_d' 78 | # ], 79 | # 'field_c': [ 80 | # 'field_c must be greater than field_a', 81 | # 'field_c must be lower than field_d' 82 | # ] 83 | # } 84 | -------------------------------------------------------------------------------- /docs/extending/using_original_input_data.rst: -------------------------------------------------------------------------------- 1 | Using original input data 2 | ------------------------- 3 | 4 | If you want to use the original, unprocessed input, you can add ``pass_original=True`` to 5 | `post_load ` or `validates_schema `. 6 | 7 | .. code-block:: python 8 | 9 | from marshmallow import Schema, fields, post_load, ValidationError 10 | 11 | 12 | class MySchema(Schema): 13 | foo = fields.Int() 14 | bar = fields.Int() 15 | 16 | @post_load(pass_original=True) 17 | def add_baz_to_bar(self, data, original_data, **kwargs): 18 | baz = original_data.get("baz") 19 | if baz: 20 | data["bar"] = data["bar"] + baz 21 | return data 22 | 23 | 24 | schema = MySchema() 25 | schema.load({"foo": 1, "bar": 2, "baz": 3}) 26 | # {'foo': 1, 'bar': 5} 27 | 28 | .. seealso:: 29 | 30 | The default behavior for unspecified fields can be controlled with the ``unknown`` option, see :ref:`Handling Unknown Fields ` for more information. 31 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. meta:: 2 | :description: 3 | marshmallow is an ORM/ODM/framework-agnostic library for converting complex datatypes, such as objects, to and from native Python datatypes. 4 | 5 | *********** 6 | marshmallow 7 | *********** 8 | 9 | *Object serialization and deserialization, lightweight and fluffy.* 10 | 11 | Release v\ |version|. (:doc:`changelog`) 12 | 13 | ---- 14 | 15 | .. include:: ../README.rst 16 | :start-after: .. start elevator-pitch 17 | :end-before: .. end elevator-pitch 18 | 19 | Ready to get started? Go on to the :doc:`quickstart` or check out some :doc:`examples `. 20 | 21 | Upgrading from an older version? 22 | ================================ 23 | 24 | See the :doc:`upgrading` page for notes on getting your code up-to-date with the latest version. 25 | 26 | Why another library? 27 | ===================== 28 | 29 | See :doc:`this document ` to learn about what makes marshmallow unique. 30 | 31 | Sponsors 32 | ======== 33 | 34 | .. include:: ../README.rst 35 | :start-after: .. start sponsors 36 | :end-before: .. end sponsors 37 | 38 | .. toctree:: 39 | :maxdepth: 1 40 | :hidden: 41 | :titlesonly: 42 | 43 | Home 44 | 45 | Usage guide 46 | =========== 47 | 48 | .. toctree:: 49 | :caption: Usage guide 50 | :maxdepth: 2 51 | 52 | install 53 | quickstart 54 | nesting 55 | custom_fields 56 | extending/index 57 | examples/index 58 | 59 | 60 | API reference 61 | ============= 62 | 63 | .. toctree:: 64 | :caption: API reference 65 | :maxdepth: 1 66 | 67 | api_reference 68 | 69 | Project info 70 | ============= 71 | 72 | .. toctree:: 73 | :caption: Project info 74 | :maxdepth: 1 75 | 76 | why 77 | changelog 78 | upgrading 79 | whos_using 80 | license 81 | authors 82 | contributing 83 | code_of_conduct 84 | kudos 85 | donate 86 | 87 | .. toctree:: 88 | :hidden: 89 | :caption: Useful links 90 | 91 | marshmallow @ PyPI 92 | marshmallow @ GitHub 93 | Issue Tracker 94 | Ecosystem 95 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Installing/upgrading from the PyPI 5 | ---------------------------------- 6 | 7 | To install the latest stable version from the PyPI: 8 | 9 | .. code-block:: shell-session 10 | 11 | $ pip install -U marshmallow 12 | 13 | To install the latest pre-release version from the PyPI: 14 | 15 | .. code-block:: shell-session 16 | 17 | $ pip install -U marshmallow --pre 18 | 19 | Get the bleeding edge version 20 | ----------------------------- 21 | 22 | To get the latest development version of marshmallow, run 23 | 24 | .. code-block:: shell-session 25 | 26 | $ pip install -U git+https://github.com/marshmallow-code/marshmallow.git@dev 27 | 28 | 29 | .. seealso:: 30 | 31 | Need help upgrading to newer releases? See the :doc:`upgrading` page. 32 | -------------------------------------------------------------------------------- /docs/kudos.rst: -------------------------------------------------------------------------------- 1 | ***** 2 | Kudos 3 | ***** 4 | 5 | A hat tip to `Django Rest Framework`_ , `Flask-RESTful`_, and `colander`_ for ideas and API design. 6 | 7 | .. _Flask-RESTful: https://flask-restful.readthedocs.io/en/latest/ 8 | 9 | .. _Django Rest Framework: https://django-rest-framework.org/ 10 | 11 | .. _colander: https://docs.pylonsproject.org/projects/colander/en/latest/ 12 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | .. literalinclude:: ../LICENSE 5 | -------------------------------------------------------------------------------- /docs/marshmallow.class_registry.rst: -------------------------------------------------------------------------------- 1 | Class registry 2 | ============== 3 | 4 | .. automodule:: marshmallow.class_registry 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/marshmallow.decorators.rst: -------------------------------------------------------------------------------- 1 | Decorators 2 | ========== 3 | 4 | .. automodule:: marshmallow.decorators 5 | :members: 6 | :autosummary: 7 | -------------------------------------------------------------------------------- /docs/marshmallow.error_store.rst: -------------------------------------------------------------------------------- 1 | Error store 2 | =========== 3 | 4 | .. automodule:: marshmallow.error_store 5 | :members: 6 | :private-members: 7 | -------------------------------------------------------------------------------- /docs/marshmallow.exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | 4 | .. automodule:: marshmallow.exceptions 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/marshmallow.experimental.context.rst: -------------------------------------------------------------------------------- 1 | Context (experimental) 2 | ====================== 3 | 4 | .. automodule:: marshmallow.experimental.context 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/marshmallow.fields.rst: -------------------------------------------------------------------------------- 1 | .. _api_fields: 2 | 3 | Fields 4 | ====== 5 | 6 | Base Field Class 7 | ---------------- 8 | 9 | .. autoclass:: marshmallow.fields.Field 10 | :private-members: 11 | 12 | Field subclasses 13 | ---------------- 14 | 15 | .. automodule:: marshmallow.fields 16 | :members: 17 | :autosummary: 18 | :exclude-members: Field, default_error_messages, mapping_type, num_type, DESERIALIZATION_CLASS 19 | -------------------------------------------------------------------------------- /docs/marshmallow.schema.rst: -------------------------------------------------------------------------------- 1 | Schema 2 | ====== 3 | 4 | .. autoclass:: marshmallow.schema.Schema 5 | :members: 6 | :autosummary: 7 | :exclude-members: OPTIONS_CLASS 8 | 9 | .. autoclass:: marshmallow.schema.SchemaOpts 10 | -------------------------------------------------------------------------------- /docs/marshmallow.types.rst: -------------------------------------------------------------------------------- 1 | Types 2 | ===== 3 | 4 | .. automodule:: marshmallow.types 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/marshmallow.utils.rst: -------------------------------------------------------------------------------- 1 | Utility functions 2 | ================= 3 | 4 | .. automodule:: marshmallow.utils 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/marshmallow.validate.rst: -------------------------------------------------------------------------------- 1 | .. _api_validators: 2 | 3 | Validators 4 | ========== 5 | 6 | .. automodule:: marshmallow.validate 7 | :members: 8 | :autosummary: 9 | -------------------------------------------------------------------------------- /docs/nesting.rst: -------------------------------------------------------------------------------- 1 | Nesting schemas 2 | =============== 3 | 4 | Schemas can be nested to represent relationships between objects (e.g. foreign key relationships). 5 | For example, a ``Blog`` may have an author represented by a ``User``. 6 | And a ``User`` may have many friends, each of which is a ``User``. 7 | 8 | .. code-block:: python 9 | 10 | from __future__ import annotations # Enable newer type annotation syntax 11 | 12 | import datetime as dt 13 | from dataclasses import dataclass, field 14 | 15 | 16 | @dataclass 17 | class User: 18 | name: str 19 | email: str 20 | created_at: dt.datetime = field(default_factory=dt.datetime.now) 21 | friends: list[User] = field(default_factory=list) 22 | employer: User | None = None 23 | 24 | 25 | @dataclass 26 | class Blog: 27 | title: str 28 | author: User 29 | 30 | Use a :class:`Nested ` field to represent the relationship, passing in a nested schema. 31 | 32 | .. code-block:: python 33 | 34 | from marshmallow import Schema, fields 35 | 36 | 37 | class UserSchema(Schema): 38 | name = fields.String() 39 | email = fields.Email() 40 | created_at = fields.DateTime() 41 | 42 | 43 | class BlogSchema(Schema): 44 | title = fields.String() 45 | author = fields.Nested(UserSchema) 46 | 47 | The serialized blog will have the nested user representation. 48 | 49 | .. code-block:: python 50 | 51 | from pprint import pprint 52 | 53 | user = User(name="Monty", email="monty@python.org") 54 | blog = Blog(title="Something Completely Different", author=user) 55 | result = BlogSchema().dump(blog) 56 | pprint(result) 57 | # {'title': u'Something Completely Different', 58 | # 'author': {'name': u'Monty', 59 | # 'email': u'monty@python.org', 60 | # 'created_at': '2014-08-17T14:58:57.600623+00:00'}} 61 | 62 | .. note:: 63 | If the field is a collection of nested objects, pass the `Nested ` field to `List `. 64 | 65 | .. code-block:: python 66 | 67 | collaborators = fields.List(fields.Nested(UserSchema)) 68 | 69 | .. _specifying-nested-fields: 70 | 71 | Specifying which fields to nest 72 | ------------------------------- 73 | 74 | You can explicitly specify which attributes of the nested objects you want to (de)serialize with the ``only`` argument to the schema. 75 | 76 | .. code-block:: python 77 | 78 | class BlogSchema2(Schema): 79 | title = fields.String() 80 | author = fields.Nested(UserSchema(only=("email",))) 81 | 82 | 83 | schema = BlogSchema2() 84 | result = schema.dump(blog) 85 | pprint(result) 86 | # { 87 | # 'title': u'Something Completely Different', 88 | # 'author': {'email': u'monty@python.org'} 89 | # } 90 | 91 | Dotted paths may be passed to ``only`` and ``exclude`` to specify nested attributes. 92 | 93 | .. code-block:: python 94 | 95 | class SiteSchema(Schema): 96 | blog = fields.Nested(BlogSchema2) 97 | 98 | 99 | schema = SiteSchema(only=("blog.author.email",)) 100 | result = schema.dump(site) 101 | pprint(result) 102 | # { 103 | # 'blog': { 104 | # 'author': {'email': u'monty@python.org'} 105 | # } 106 | # } 107 | 108 | You can replace nested data with a single value (or flat list of values if ``many=True``) using the :class:`Pluck ` field. 109 | 110 | .. code-block:: python 111 | 112 | class UserSchema(Schema): 113 | name = fields.String() 114 | email = fields.Email() 115 | friends = fields.Pluck("self", "name", many=True) 116 | 117 | 118 | # ... create ``user`` ... 119 | serialized_data = UserSchema().dump(user) 120 | pprint(serialized_data) 121 | # { 122 | # "name": "Steve", 123 | # "email": "steve@example.com", 124 | # "friends": ["Mike", "Joe"] 125 | # } 126 | deserialized_data = UserSchema().load(result) 127 | pprint(deserialized_data) 128 | # { 129 | # "name": "Steve", 130 | # "email": "steve@example.com", 131 | # "friends": [{"name": "Mike"}, {"name": "Joe"}] 132 | # } 133 | 134 | 135 | .. _partial-loading: 136 | 137 | Partial loading 138 | --------------- 139 | 140 | Nested schemas also inherit the ``partial`` parameter of the parent ``load`` call. 141 | 142 | .. code-block:: python 143 | 144 | class UserSchemaStrict(Schema): 145 | name = fields.String(required=True) 146 | email = fields.Email() 147 | created_at = fields.DateTime(required=True) 148 | 149 | 150 | class BlogSchemaStrict(Schema): 151 | title = fields.String(required=True) 152 | author = fields.Nested(UserSchemaStrict, required=True) 153 | 154 | 155 | schema = BlogSchemaStrict() 156 | blog = {"title": "Something Completely Different", "author": {}} 157 | result = schema.load(blog, partial=True) 158 | pprint(result) 159 | # {'author': {}, 'title': 'Something Completely Different'} 160 | 161 | You can specify a subset of the fields to allow partial loading using dot delimiters. 162 | 163 | .. code-block:: python 164 | 165 | author = {"name": "Monty"} 166 | blog = {"title": "Something Completely Different", "author": author} 167 | result = schema.load(blog, partial=("title", "author.created_at")) 168 | pprint(result) 169 | # {'author': {'name': 'Monty'}, 'title': 'Something Completely Different'} 170 | 171 | .. _two-way-nesting: 172 | 173 | Two-way nesting 174 | --------------- 175 | 176 | If you have two objects that nest each other, you can pass a callable to `Nested `. 177 | This allows you to resolve order-of-declaration issues, such as when one schema nests a schema that is declared below it. 178 | 179 | For example, a representation of an ``Author`` model might include the books that have a many-to-one relationship to it. 180 | Correspondingly, a representation of a ``Book`` will include its author representation. 181 | 182 | .. code-block:: python 183 | 184 | class BookSchema(Schema): 185 | id = fields.Int(dump_only=True) 186 | title = fields.Str() 187 | 188 | # Make sure to use the 'only' or 'exclude' 189 | # to avoid infinite recursion 190 | author = fields.Nested(lambda: AuthorSchema(only=("id", "title"))) 191 | 192 | 193 | class AuthorSchema(Schema): 194 | id = fields.Int(dump_only=True) 195 | title = fields.Str() 196 | 197 | books = fields.List(fields.Nested(BookSchema(exclude=("author",)))) 198 | 199 | 200 | .. code-block:: python 201 | 202 | from pprint import pprint 203 | from mymodels import Author, Book 204 | 205 | author = Author(name="William Faulkner") 206 | book = Book(title="As I Lay Dying", author=author) 207 | book_result = BookSchema().dump(book) 208 | pprint(book_result, indent=2) 209 | # { 210 | # "id": 124, 211 | # "title": "As I Lay Dying", 212 | # "author": { 213 | # "id": 8, 214 | # "name": "William Faulkner" 215 | # } 216 | # } 217 | 218 | author_result = AuthorSchema().dump(author) 219 | pprint(author_result, indent=2) 220 | # { 221 | # "id": 8, 222 | # "name": "William Faulkner", 223 | # "books": [ 224 | # { 225 | # "id": 124, 226 | # "title": "As I Lay Dying" 227 | # } 228 | # ] 229 | # } 230 | 231 | You can also pass a class name as a string to `Nested `. 232 | This is useful for avoiding circular imports when your schemas are located in different modules. 233 | 234 | .. code-block:: python 235 | 236 | # books.py 237 | from marshmallow import Schema, fields 238 | 239 | 240 | class BookSchema(Schema): 241 | id = fields.Int(dump_only=True) 242 | title = fields.Str() 243 | 244 | author = fields.Nested("AuthorSchema", only=("id", "title")) 245 | 246 | .. code-block:: python 247 | 248 | # authors.py 249 | from marshmallow import Schema, fields 250 | 251 | 252 | class AuthorSchema(Schema): 253 | id = fields.Int(dump_only=True) 254 | title = fields.Str() 255 | 256 | books = fields.List(fields.Nested("BookSchema", exclude=("author",))) 257 | 258 | .. note:: 259 | 260 | If you have multiple schemas with the same class name, you must pass the full, module-qualified path. :: 261 | 262 | author = fields.Nested("authors.BookSchema", only=("id", "title")) 263 | 264 | .. _self-nesting: 265 | 266 | Nesting a schema within itself 267 | ------------------------------ 268 | 269 | If the object to be marshalled has a relationship to an object of the same type, you can nest the `Schema ` within itself by passing a callable that returns an instance of the same schema. 270 | 271 | .. code-block:: python 272 | 273 | class UserSchema(Schema): 274 | name = fields.String() 275 | email = fields.Email() 276 | # Use the 'exclude' argument to avoid infinite recursion 277 | employer = fields.Nested(lambda: UserSchema(exclude=("employer",))) 278 | friends = fields.List(fields.Nested(lambda: UserSchema())) 279 | 280 | 281 | user = User("Steve", "steve@example.com") 282 | user.friends.append(User("Mike", "mike@example.com")) 283 | user.friends.append(User("Joe", "joe@example.com")) 284 | user.employer = User("Dirk", "dirk@example.com") 285 | result = UserSchema().dump(user) 286 | pprint(result, indent=2) 287 | # { 288 | # "name": "Steve", 289 | # "email": "steve@example.com", 290 | # "friends": [ 291 | # { 292 | # "name": "Mike", 293 | # "email": "mike@example.com", 294 | # "friends": [], 295 | # "employer": null 296 | # }, 297 | # { 298 | # "name": "Joe", 299 | # "email": "joe@example.com", 300 | # "friends": [], 301 | # "employer": null 302 | # } 303 | # ], 304 | # "employer": { 305 | # "name": "Dirk", 306 | # "email": "dirk@example.com", 307 | # "friends": [] 308 | # } 309 | # } 310 | 311 | Next steps 312 | ---------- 313 | 314 | - Want to create your own field type? See the :doc:`custom_fields` page. 315 | - Need to add schema-level validation, post-processing, or error handling behavior? See the :doc:`extending/index` page. 316 | - For more detailed usage examples, check out the :doc:`examples/index` page. 317 | -------------------------------------------------------------------------------- /docs/top_level.rst: -------------------------------------------------------------------------------- 1 | Top-level API 2 | ============= 3 | 4 | .. automodule:: marshmallow 5 | :members: 6 | :autosummary: 7 | :exclude-members: OPTIONS_CLASS 8 | 9 | .. Can't use :autodata: here due to Sphinx bug: https://github.com/sphinx-doc/sphinx/issues/6495 10 | .. data:: missing 11 | 12 | Singleton value that indicates that a field's value is missing from input 13 | dict passed to `Schema.load `. If the field's value is not required, 14 | its ``default`` value is used. 15 | 16 | Constants for ``unknown`` 17 | ------------------------- 18 | 19 | .. seealso:: :ref:`unknown` 20 | 21 | .. data:: EXCLUDE 22 | 23 | Indicates that fields that are not explicitly declared on a schema should be 24 | excluded from the deserialized result. 25 | 26 | 27 | .. data:: INCLUDE 28 | 29 | Indicates that fields that are not explicitly declared on a schema should be 30 | included from the deserialized result. 31 | 32 | .. data:: RAISE 33 | 34 | Indicates that fields that are not explicitly declared on a schema should 35 | result in an error. 36 | -------------------------------------------------------------------------------- /docs/whos_using.rst: -------------------------------------------------------------------------------- 1 | Who's using marshmallow? 2 | ======================== 3 | 4 | Visit the link below to see a list of companies using marshmallow. 5 | 6 | https://github.com/marshmallow-code/marshmallow/wiki/Who's-using-marshmallow%3F 7 | 8 | Is your company or organization using marshmallow? Add it to the wiki. 9 | -------------------------------------------------------------------------------- /docs/why.rst: -------------------------------------------------------------------------------- 1 | Why marshmallow? 2 | ================ 3 | 4 | The Python ecosystem has many great libraries for data formatting and schema validation. 5 | 6 | In fact, marshmallow was influenced by a number of these libraries. marshmallow is inspired by `Django REST Framework`_, `Flask-RESTful`_, and `colander `_. It borrows a number of implementation and design ideas from these libraries to create a flexible and productive solution for marshalling, unmarshalling, and validating data. 7 | 8 | Here are just a few reasons why you might use marshmallow. 9 | 10 | Agnostic 11 | -------- 12 | 13 | marshmallow makes no assumption about web frameworks or database layers. It will work with just about any ORM, ODM, or no ORM at all. This gives you the freedom to choose the components that fit your application's needs without having to change your data formatting code. If you wish, you can build integration layers to make marshmallow work more closely with your frameworks and libraries of choice (for examples, see `Flask-Marshmallow `_ and `Django REST Marshmallow `_). 14 | 15 | Concise, familiar syntax 16 | ------------------------ 17 | 18 | If you have used `Django REST Framework`_ or `WTForms `_, marshmallow's :class:`Schema ` syntax will feel familiar to you. Class-level field attributes define the schema for formatting your data. Configuration is added using the `class Meta ` paradigm. Configuration options can be overridden at application runtime by passing arguments to the `Schema ` constructor. The :meth:`dump ` and :meth:`load ` methods are used for serialization and deserialization (of course!). 19 | 20 | Class-based schemas allow for code reuse and configuration 21 | ---------------------------------------------------------- 22 | 23 | Unlike `Flask-RESTful`_, which uses dictionaries to define output schemas, marshmallow uses classes. This allows for easy code reuse and configuration. It also enables patterns for configuring and extending schemas, such as adding :doc:`post-processing and error handling behavior `. 24 | 25 | Consistency meets flexibility 26 | ----------------------------- 27 | 28 | marshmallow makes it easy to modify a schema's output at application runtime. A single :class:`Schema ` can produce multiple output formats while keeping the individual field outputs consistent. 29 | 30 | As an example, you might have a JSON endpoint for retrieving all information about a video game's state. You then add a low-latency endpoint that only returns a minimal subset of information about game state. Both endpoints can be handled by the same `Schema `. 31 | 32 | .. code-block:: python 33 | 34 | class GameStateSchema(Schema): 35 | _id = fields.UUID(required=True) 36 | score = fields.Nested(ScoreSchema) 37 | players = fields.List(fields.Nested(PlayerSchema)) 38 | last_changed = fields.DateTime(format="rfc") 39 | 40 | 41 | # Serializes full game state 42 | full_serializer = GameStateSchema() 43 | # Serializes a subset of information, for a low-latency endpoint 44 | summary_serializer = GameStateSchema(only=("_id", "last_changed")) 45 | # Also filter the fields when serializing multiple games 46 | gamelist_serializer = GameStateSchema( 47 | many=True, only=("_id", "players", "last_changed") 48 | ) 49 | 50 | In this example, a single schema produced three different outputs! The dynamic nature of a :class:`Schema` leads to **less code** and **more consistent formatting**. 51 | 52 | .. _Django REST Framework: https://www.django-rest-framework.org/ 53 | .. _Flask-RESTful: https://flask-restful.readthedocs.io/ 54 | 55 | Advanced schema nesting 56 | ----------------------- 57 | 58 | Most serialization libraries provide some means for nesting schemas within each other, but they often fail to meet common use cases in clean way. marshmallow aims to fill these gaps by adding a few nice features for :doc:`nesting schemas `: 59 | 60 | - You can specify which :ref:`subset of fields ` to include on nested schemas. 61 | - :ref:`Two-way nesting `. Two different schemas can nest each other. 62 | - :ref:`Self-nesting `. A schema can be nested within itself. 63 | -------------------------------------------------------------------------------- /examples/flask_example.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.9" 3 | # dependencies = [ 4 | # "flask", 5 | # "flask-sqlalchemy>=3.1.1", 6 | # "marshmallow", 7 | # "sqlalchemy>2.0", 8 | # ] 9 | # /// 10 | from __future__ import annotations 11 | 12 | import datetime 13 | 14 | from flask import Flask, request 15 | from flask_sqlalchemy import SQLAlchemy 16 | from sqlalchemy.exc import NoResultFound 17 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship 18 | 19 | from marshmallow import Schema, ValidationError, fields, pre_load 20 | 21 | app = Flask(__name__) 22 | app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////tmp/quotes.db" 23 | 24 | 25 | class Base(DeclarativeBase): 26 | pass 27 | 28 | 29 | db = SQLAlchemy(app, model_class=Base) 30 | 31 | ##### MODELS ##### 32 | 33 | 34 | class Author(db.Model): # type: ignore[name-defined] 35 | id: Mapped[int] = mapped_column(primary_key=True) 36 | first: Mapped[str] 37 | last: Mapped[str] 38 | 39 | 40 | class Quote(db.Model): # type: ignore[name-defined] 41 | id: Mapped[int] = mapped_column(primary_key=True) 42 | content: Mapped[str] = mapped_column(nullable=False) 43 | author_id: Mapped[int] = mapped_column(db.ForeignKey(Author.id)) 44 | author: Mapped[Author] = relationship(backref=db.backref("quotes", lazy="dynamic")) 45 | posted_at: Mapped[datetime.datetime] 46 | 47 | 48 | ##### SCHEMAS ##### 49 | 50 | 51 | class AuthorSchema(Schema): 52 | id = fields.Int(dump_only=True) 53 | first = fields.Str() 54 | last = fields.Str() 55 | formatted_name = fields.Method("format_name", dump_only=True) 56 | 57 | def format_name(self, author): 58 | return f"{author.last}, {author.first}" 59 | 60 | 61 | # Custom validator 62 | def must_not_be_blank(data): 63 | if not data: 64 | raise ValidationError("Data not provided.") 65 | 66 | 67 | class QuoteSchema(Schema): 68 | id = fields.Int(dump_only=True) 69 | author = fields.Nested(AuthorSchema, validate=must_not_be_blank) 70 | content = fields.Str(required=True, validate=must_not_be_blank) 71 | posted_at = fields.DateTime(dump_only=True) 72 | 73 | # Allow client to pass author's full name in request body 74 | # e.g. {"author': 'Tim Peters"} rather than {"first": "Tim", "last": "Peters"} 75 | @pre_load 76 | def process_author(self, data, **kwargs): 77 | author_name = data.get("author") 78 | if author_name: 79 | first, last = author_name.split(" ") 80 | author_dict = {"first": first, "last": last} 81 | else: 82 | author_dict = {} 83 | data["author"] = author_dict 84 | return data 85 | 86 | 87 | author_schema = AuthorSchema() 88 | authors_schema = AuthorSchema(many=True) 89 | quote_schema = QuoteSchema() 90 | quotes_schema = QuoteSchema(many=True, only=("id", "content")) 91 | 92 | ##### API ##### 93 | 94 | 95 | @app.route("/authors") 96 | def get_authors(): 97 | authors = Author.query.all() 98 | # Serialize the queryset 99 | result = authors_schema.dump(authors) 100 | return {"authors": result} 101 | 102 | 103 | @app.route("/authors/") 104 | def get_author(pk): 105 | try: 106 | author = Author.query.filter(Author.id == pk).one() 107 | except NoResultFound: 108 | return {"message": "Author could not be found."}, 400 109 | author_result = author_schema.dump(author) 110 | quotes_result = quotes_schema.dump(author.quotes.all()) 111 | return {"author": author_result, "quotes": quotes_result} 112 | 113 | 114 | @app.route("/quotes/", methods=["GET"]) 115 | def get_quotes(): 116 | quotes = Quote.query.all() 117 | result = quotes_schema.dump(quotes, many=True) 118 | return {"quotes": result} 119 | 120 | 121 | @app.route("/quotes/") 122 | def get_quote(pk): 123 | try: 124 | quote = Quote.query.filter(Quote.id == pk).one() 125 | except NoResultFound: 126 | return {"message": "Quote could not be found."}, 400 127 | result = quote_schema.dump(quote) 128 | return {"quote": result} 129 | 130 | 131 | @app.route("/quotes/", methods=["POST"]) 132 | def new_quote(): 133 | json_data = request.get_json() 134 | if not json_data: 135 | return {"message": "No input data provided"}, 400 136 | # Validate and deserialize input 137 | try: 138 | data = quote_schema.load(json_data) 139 | except ValidationError as err: 140 | return err.messages, 422 141 | first, last = data["author"]["first"], data["author"]["last"] 142 | author = Author.query.filter_by(first=first, last=last).first() 143 | if author is None: 144 | # Create a new author 145 | author = Author(first=first, last=last) 146 | db.session.add(author) 147 | # Create new quote 148 | quote = Quote( 149 | content=data["content"], 150 | author=author, 151 | posted_at=datetime.datetime.now(datetime.UTC), 152 | ) 153 | db.session.add(quote) 154 | db.session.commit() 155 | result = quote_schema.dump(Quote.query.get(quote.id)) 156 | return {"message": "Created new quote.", "quote": result} 157 | 158 | 159 | if __name__ == "__main__": 160 | with app.app_context(): 161 | db.create_all() 162 | app.run(debug=True, port=5000) 163 | -------------------------------------------------------------------------------- /examples/inflection_example.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.9" 3 | # dependencies = [ 4 | # "marshmallow", 5 | # ] 6 | # /// 7 | from marshmallow import Schema, fields 8 | 9 | 10 | def camelcase(s): 11 | parts = iter(s.split("_")) 12 | return next(parts) + "".join(i.title() for i in parts) 13 | 14 | 15 | class CamelCaseSchema(Schema): 16 | """Schema that uses camel-case for its external representation 17 | and snake-case for its internal representation. 18 | """ 19 | 20 | def on_bind_field(self, field_name, field_obj): 21 | field_obj.data_key = camelcase(field_obj.data_key or field_name) 22 | 23 | 24 | # ----------------------------------------------------------------------------- 25 | 26 | 27 | class UserSchema(CamelCaseSchema): 28 | first_name = fields.Str(required=True) 29 | last_name = fields.Str(required=True) 30 | 31 | 32 | schema = UserSchema() 33 | loaded = schema.load({"firstName": "David", "lastName": "Bowie"}) 34 | print("Loaded data:") 35 | print(loaded) 36 | dumped = schema.dump(loaded) 37 | print("Dumped data:") 38 | print(dumped) 39 | -------------------------------------------------------------------------------- /examples/invalid_package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dunderscore", 3 | "version": "INVALID", 4 | "homepage": "INVALID", 5 | "description": "The Pythonic JavaScript toolkit", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dunderscore", 3 | "version": "1.2.3", 4 | "description": "The Pythonic JavaScript toolkit", 5 | "devDependencies": { 6 | "pest": "^23.4.1" 7 | }, 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "pest" 11 | }, 12 | "license": "MIT" 13 | } 14 | -------------------------------------------------------------------------------- /examples/package_json_example.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.9" 3 | # dependencies = [ 4 | # "marshmallow", 5 | # "packaging>=17.0", 6 | # ] 7 | # /// 8 | import json 9 | import sys 10 | from pprint import pprint 11 | 12 | from packaging import version 13 | 14 | from marshmallow import INCLUDE, Schema, ValidationError, fields 15 | 16 | 17 | class Version(fields.Field[version.Version]): 18 | """Version field that deserializes to a Version object.""" 19 | 20 | def _deserialize(self, value, *args, **kwargs): 21 | try: 22 | return version.Version(value) 23 | except version.InvalidVersion as e: 24 | raise ValidationError("Not a valid version.") from e 25 | 26 | def _serialize(self, value, *args, **kwargs): 27 | return str(value) 28 | 29 | 30 | class PackageSchema(Schema): 31 | name = fields.Str(required=True) 32 | version = Version(required=True) 33 | description = fields.Str(required=True) 34 | main = fields.Str(required=False) 35 | homepage = fields.URL(required=False) 36 | scripts = fields.Dict(keys=fields.Str(), values=fields.Str()) 37 | license = fields.Str(required=True) 38 | dependencies = fields.Dict(keys=fields.Str(), values=fields.Str(), required=False) 39 | dev_dependencies = fields.Dict( 40 | keys=fields.Str(), 41 | values=fields.Str(), 42 | required=False, 43 | data_key="devDependencies", 44 | ) 45 | 46 | class Meta: 47 | # Include unknown fields in the deserialized output 48 | unknown = INCLUDE 49 | 50 | 51 | if __name__ == "__main__": 52 | pkg = json.load(sys.stdin) 53 | try: 54 | pprint(PackageSchema().load(pkg)) 55 | except ValidationError as error: 56 | print("ERROR: package.json is invalid") 57 | pprint(error.messages) 58 | sys.exit(1) 59 | -------------------------------------------------------------------------------- /performance/benchmark.py: -------------------------------------------------------------------------------- 1 | """Simple benchmark for marshmallow serialization of a moderately complex object. 2 | 3 | Uses the `timeit` module to benchmark serializing an object through marshmallow. 4 | """ 5 | 6 | # ruff: noqa: A002, T201 7 | import argparse 8 | import cProfile 9 | import datetime 10 | import gc 11 | import timeit 12 | 13 | from marshmallow import Schema, ValidationError, fields, post_dump 14 | 15 | 16 | # Custom validator 17 | def must_not_be_blank(data): 18 | if not data: 19 | raise ValidationError("Data not provided.") 20 | 21 | 22 | class AuthorSchema(Schema): 23 | id = fields.Int(dump_only=True) 24 | first = fields.Str() 25 | last = fields.Str() 26 | book_count = fields.Float() 27 | age = fields.Float() 28 | address = fields.Str() 29 | full_name = fields.Method("get_full_name") 30 | 31 | def get_full_name(self, author): 32 | return f"{author.last}, {author.first}" 33 | 34 | 35 | class QuoteSchema(Schema): 36 | id = fields.Int(dump_only=True) 37 | author = fields.Nested(AuthorSchema, validate=must_not_be_blank) 38 | content = fields.Str(required=True, validate=must_not_be_blank) 39 | posted_at = fields.DateTime(dump_only=True) 40 | book_name = fields.Str() 41 | page_number = fields.Float() 42 | line_number = fields.Float() 43 | col_number = fields.Float() 44 | 45 | @post_dump 46 | def add_full_name(self, data, **kwargs): 47 | data["author_full"] = "{}, {}".format( 48 | data["author"]["last"], data["author"]["first"] 49 | ) 50 | return data 51 | 52 | 53 | class Author: 54 | def __init__(self, id, first, last, book_count, age, address): 55 | self.id = id 56 | self.first = first 57 | self.last = last 58 | self.book_count = book_count 59 | self.age = age 60 | self.address = address 61 | 62 | 63 | class Quote: 64 | def __init__( 65 | self, 66 | id, 67 | author, 68 | content, 69 | posted_at, 70 | book_name, 71 | page_number, 72 | line_number, 73 | col_number, 74 | ): 75 | self.id = id 76 | self.author = author 77 | self.content = content 78 | self.posted_at = posted_at 79 | self.book_name = book_name 80 | self.page_number = page_number 81 | self.line_number = line_number 82 | self.col_number = col_number 83 | 84 | 85 | def run_timeit(quotes, iterations, repeat, *, profile=False): 86 | quotes_schema = QuoteSchema(many=True) 87 | if profile: 88 | profile = cProfile.Profile() 89 | profile.enable() 90 | 91 | gc.collect() 92 | best = min( 93 | timeit.repeat( 94 | lambda: quotes_schema.dump(quotes), 95 | "gc.enable()", 96 | number=iterations, 97 | repeat=repeat, 98 | ) 99 | ) 100 | if profile: 101 | profile.disable() 102 | profile.dump_stats("marshmallow.pprof") 103 | 104 | return best * 1e6 / iterations / len(quotes) 105 | 106 | 107 | def main(): 108 | parser = argparse.ArgumentParser(description="Runs a benchmark of Marshmallow.") 109 | parser.add_argument( 110 | "--iterations", 111 | type=int, 112 | default=1000, 113 | help="Number of iterations to run per test.", 114 | ) 115 | parser.add_argument( 116 | "--repeat", 117 | type=int, 118 | default=5, 119 | help="Number of times to repeat the performance test. The minimum will " 120 | "be used.", 121 | ) 122 | parser.add_argument( 123 | "--object-count", type=int, default=20, help="Number of objects to dump." 124 | ) 125 | parser.add_argument( 126 | "--profile", 127 | action="store_true", 128 | help="Whether or not to profile marshmallow while running the benchmark.", 129 | ) 130 | args = parser.parse_args() 131 | 132 | quotes = [ 133 | Quote( 134 | i, 135 | Author(i, "Foo", "Bar", 42, 66, "123 Fake St"), 136 | "Hello World", 137 | datetime.datetime(2019, 7, 4, tzinfo=datetime.timezone.utc), 138 | "The World", 139 | 34, 140 | 3, 141 | 70, 142 | ) 143 | for i in range(args.object_count) 144 | ] 145 | 146 | print( 147 | f"Benchmark Result: {run_timeit(quotes, args.iterations, args.repeat, profile=args.profile):.2f} usec/dump" 148 | ) 149 | 150 | 151 | if __name__ == "__main__": 152 | main() 153 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "marshmallow" 3 | version = "4.0.0" 4 | description = "A lightweight library for converting complex datatypes to and from native Python datatypes." 5 | readme = "README.rst" 6 | license = { file = "LICENSE" } 7 | authors = [{ name = "Steven Loria", email = "sloria1@gmail.com" }] 8 | maintainers = [ 9 | { name = "Steven Loria", email = "sloria1@gmail.com" }, 10 | { name = "Jérôme Lafréchoux", email = "jerome@jolimont.fr" }, 11 | { name = "Jared Deckard", email = "jared@shademaps.com" }, 12 | ] 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | ] 24 | requires-python = ">=3.9" 25 | dependencies = [ 26 | "backports-datetime-fromisoformat; python_version < '3.11'", 27 | "typing-extensions; python_version < '3.11'", 28 | ] 29 | 30 | [project.urls] 31 | Changelog = "https://marshmallow.readthedocs.io/en/latest/changelog.html" 32 | Funding = "https://opencollective.com/marshmallow" 33 | Issues = "https://github.com/marshmallow-code/marshmallow/issues" 34 | Source = "https://github.com/marshmallow-code/marshmallow" 35 | Tidelift = "https://tidelift.com/subscription/pkg/pypi-marshmallow?utm_source=pypi-marshmallow&utm_medium=pypi" 36 | 37 | [project.optional-dependencies] 38 | docs = [ 39 | "autodocsumm==0.2.14", 40 | "furo==2024.8.6", 41 | "sphinx-copybutton==0.5.2", 42 | "sphinx-issues==5.0.1", 43 | "sphinx==8.2.3", 44 | "sphinxext-opengraph==0.10.0", 45 | ] 46 | tests = ["pytest", "simplejson"] 47 | dev = ["marshmallow[tests]", "tox", "pre-commit>=3.5,<5.0"] 48 | 49 | [build-system] 50 | requires = ["flit_core<4"] 51 | build-backend = "flit_core.buildapi" 52 | 53 | [tool.flit.sdist] 54 | include = [ 55 | "docs/", 56 | "tests/", 57 | "CHANGELOG.rst", 58 | "CONTRIBUTING.rst", 59 | "SECURITY.md", 60 | "NOTICE", 61 | "tox.ini", 62 | ] 63 | exclude = ["docs/_build/"] 64 | 65 | [tool.ruff] 66 | fix = true 67 | 68 | [tool.ruff.format] 69 | docstring-code-format = true 70 | 71 | [tool.ruff.lint] 72 | # use all checks available in ruff except the ones explicitly ignored below 73 | select = ["ALL"] 74 | ignore = [ 75 | "A005", # "module {name} shadows a Python standard-library module" 76 | "ANN", # let mypy handle annotation checks 77 | "ARG", # unused arguments are common w/ interfaces 78 | "COM", # let formatter take care commas 79 | "C901", # don't enforce complexity level 80 | "D", # don't require docstrings 81 | "DTZ007", # ignore false positives due to https://github.com/astral-sh/ruff/issues/1306 82 | "E501", # leave line-length enforcement to formatter 83 | "EM", # allow string messages in exceptions 84 | "FIX", # allow "FIX" comments in code 85 | "INP001", # allow Python files outside of packages 86 | "N806", # allow uppercase variable names for variables that are classes 87 | "PERF203", # allow try-except within loops 88 | "PLR0913", # "Too many arguments" 89 | "PLR0912", # "Too many branches" 90 | "PLR2004", # "Magic value used in comparison" 91 | "PTH", # don't require using pathlib instead of os 92 | "RUF012", # allow mutable class variables 93 | "SIM102", # Sometimes nested ifs are more readable than if...and... 94 | "SIM105", # "Use `contextlib.suppress(...)` instead of `try`-`except`-`pass`" 95 | "SIM108", # sometimes if-else is more readable than a ternary 96 | "TD", # allow TODO comments to be whatever we want 97 | "TRY003", # allow long messages passed to exceptions 98 | "TRY004", # allow ValueError for invalid argument types 99 | ] 100 | 101 | [tool.ruff.lint.per-file-ignores] 102 | "tests/*" = [ 103 | "ARG", # unused arguments are fine in tests 104 | "C408", # allow dict() instead of dict literal 105 | "DTZ", # allow naive datetimes 106 | "FBT003", # allow boolean positional argument 107 | "N803", # fixture names might be uppercase 108 | "PLR0915", # allow lots of statements 109 | "PT007", # ignore false positives due to https://github.com/astral-sh/ruff/issues/14743 110 | "PT011", # don't require match when using pytest.raises 111 | "S", # allow asserts 112 | "SIM117", # allow nested with statements because it's more readable sometimes 113 | "SLF001", # allow private attribute access 114 | ] 115 | "examples/*" = [ 116 | "S", # allow asserts 117 | "T", # allow prints 118 | ] 119 | "src/marshmallow/orderedset.py" = [ 120 | "FBT002", # allow boolean positional argument 121 | "T", # allow prints 122 | ] 123 | 124 | [tool.mypy] 125 | files = ["src", "tests", "examples"] 126 | ignore_missing_imports = true 127 | warn_unreachable = true 128 | warn_unused_ignores = true 129 | warn_redundant_casts = true 130 | no_implicit_optional = true 131 | 132 | [[tool.mypy.overrides]] 133 | module = "tests.*" 134 | check_untyped_defs = true 135 | disable_error_code = ["call-overload", "index"] 136 | 137 | [tool.pytest.ini_options] 138 | norecursedirs = ".git .ropeproject .tox docs env venv tests/mypy_test_cases" 139 | -------------------------------------------------------------------------------- /src/marshmallow/__init__.py: -------------------------------------------------------------------------------- 1 | from marshmallow.constants import EXCLUDE, INCLUDE, RAISE, missing 2 | from marshmallow.decorators import ( 3 | post_dump, 4 | post_load, 5 | pre_dump, 6 | pre_load, 7 | validates, 8 | validates_schema, 9 | ) 10 | from marshmallow.exceptions import ValidationError 11 | from marshmallow.schema import Schema, SchemaOpts 12 | 13 | from . import fields 14 | 15 | __all__ = [ 16 | "EXCLUDE", 17 | "INCLUDE", 18 | "RAISE", 19 | "Schema", 20 | "SchemaOpts", 21 | "ValidationError", 22 | "fields", 23 | "missing", 24 | "post_dump", 25 | "post_load", 26 | "pre_dump", 27 | "pre_load", 28 | "validates", 29 | "validates_schema", 30 | ] 31 | -------------------------------------------------------------------------------- /src/marshmallow/class_registry.py: -------------------------------------------------------------------------------- 1 | """A registry of :class:`Schema ` classes. This allows for string 2 | lookup of schemas, which may be used with 3 | class:`fields.Nested `. 4 | 5 | .. warning:: 6 | 7 | This module is treated as private API. 8 | Users should not need to use this module directly. 9 | """ 10 | # ruff: noqa: ERA001 11 | 12 | from __future__ import annotations 13 | 14 | import typing 15 | 16 | from marshmallow.exceptions import RegistryError 17 | 18 | if typing.TYPE_CHECKING: 19 | from marshmallow import Schema 20 | 21 | SchemaType = type[Schema] 22 | 23 | # { 24 | # : 25 | # : 26 | # } 27 | _registry = {} # type: dict[str, list[SchemaType]] 28 | 29 | 30 | def register(classname: str, cls: SchemaType) -> None: 31 | """Add a class to the registry of serializer classes. When a class is 32 | registered, an entry for both its classname and its full, module-qualified 33 | path are added to the registry. 34 | 35 | Example: :: 36 | 37 | class MyClass: 38 | pass 39 | 40 | 41 | register("MyClass", MyClass) 42 | # Registry: 43 | # { 44 | # 'MyClass': [path.to.MyClass], 45 | # 'path.to.MyClass': [path.to.MyClass], 46 | # } 47 | 48 | """ 49 | # Module where the class is located 50 | module = cls.__module__ 51 | # Full module path to the class 52 | # e.g. user.schemas.UserSchema 53 | fullpath = f"{module}.{classname}" 54 | # If the class is already registered; need to check if the entries are 55 | # in the same module as cls to avoid having multiple instances of the same 56 | # class in the registry 57 | if classname in _registry and not any( 58 | each.__module__ == module for each in _registry[classname] 59 | ): 60 | _registry[classname].append(cls) 61 | elif classname not in _registry: 62 | _registry[classname] = [cls] 63 | 64 | # Also register the full path 65 | if fullpath not in _registry: 66 | _registry.setdefault(fullpath, []).append(cls) 67 | else: 68 | # If fullpath does exist, replace existing entry 69 | _registry[fullpath] = [cls] 70 | 71 | 72 | @typing.overload 73 | def get_class(classname: str, *, all: typing.Literal[False] = ...) -> SchemaType: ... 74 | 75 | 76 | @typing.overload 77 | def get_class( 78 | classname: str, *, all: typing.Literal[True] = ... 79 | ) -> list[SchemaType]: ... 80 | 81 | 82 | def get_class(classname: str, *, all: bool = False) -> list[SchemaType] | SchemaType: # noqa: A002 83 | """Retrieve a class from the registry. 84 | 85 | :raises: `marshmallow.exceptions.RegistryError` if the class cannot be found 86 | or if there are multiple entries for the given class name. 87 | """ 88 | try: 89 | classes = _registry[classname] 90 | except KeyError as error: 91 | raise RegistryError( 92 | f"Class with name {classname!r} was not found. You may need " 93 | "to import the class." 94 | ) from error 95 | if len(classes) > 1: 96 | if all: 97 | return _registry[classname] 98 | raise RegistryError( 99 | f"Multiple classes with name {classname!r} " 100 | "were found. Please use the full, " 101 | "module-qualified path." 102 | ) 103 | return _registry[classname][0] 104 | -------------------------------------------------------------------------------- /src/marshmallow/constants.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | EXCLUDE: typing.Final = "exclude" 4 | INCLUDE: typing.Final = "include" 5 | RAISE: typing.Final = "raise" 6 | 7 | 8 | class _Missing: 9 | def __bool__(self): 10 | return False 11 | 12 | def __copy__(self): 13 | return self 14 | 15 | def __deepcopy__(self, _): 16 | return self 17 | 18 | def __repr__(self): 19 | return "" 20 | 21 | 22 | missing: typing.Final = _Missing() 23 | -------------------------------------------------------------------------------- /src/marshmallow/decorators.py: -------------------------------------------------------------------------------- 1 | """Decorators for registering schema pre-processing and post-processing methods. 2 | These should be imported from the top-level `marshmallow` module. 3 | 4 | Methods decorated with 5 | `pre_load `, `post_load `, 6 | `pre_dump `, `post_dump `, 7 | and `validates_schema ` receive 8 | ``many`` as a keyword argument. In addition, `pre_load `, 9 | `post_load `, 10 | and `validates_schema ` receive 11 | ``partial``. If you don't need these arguments, add ``**kwargs`` to your method 12 | signature. 13 | 14 | 15 | Example: :: 16 | 17 | from marshmallow import ( 18 | Schema, 19 | pre_load, 20 | pre_dump, 21 | post_load, 22 | validates_schema, 23 | validates, 24 | fields, 25 | ValidationError, 26 | ) 27 | 28 | 29 | class UserSchema(Schema): 30 | email = fields.Str(required=True) 31 | age = fields.Integer(required=True) 32 | 33 | @post_load 34 | def lowerstrip_email(self, item, many, **kwargs): 35 | item["email"] = item["email"].lower().strip() 36 | return item 37 | 38 | @pre_load(pass_collection=True) 39 | def remove_envelope(self, data, many, **kwargs): 40 | namespace = "results" if many else "result" 41 | return data[namespace] 42 | 43 | @post_dump(pass_collection=True) 44 | def add_envelope(self, data, many, **kwargs): 45 | namespace = "results" if many else "result" 46 | return {namespace: data} 47 | 48 | @validates_schema 49 | def validate_email(self, data, **kwargs): 50 | if len(data["email"]) < 3: 51 | raise ValidationError("Email must be more than 3 characters", "email") 52 | 53 | @validates("age") 54 | def validate_age(self, data, **kwargs): 55 | if data < 14: 56 | raise ValidationError("Too young!") 57 | 58 | .. note:: 59 | These decorators only work with instance methods. Class and static 60 | methods are not supported. 61 | 62 | .. warning:: 63 | The invocation order of decorated methods of the same type is not guaranteed. 64 | If you need to guarantee order of different processing steps, you should put 65 | them in the same processing method. 66 | """ 67 | 68 | from __future__ import annotations 69 | 70 | import functools 71 | from collections import defaultdict 72 | from typing import Any, Callable, cast 73 | 74 | PRE_DUMP = "pre_dump" 75 | POST_DUMP = "post_dump" 76 | PRE_LOAD = "pre_load" 77 | POST_LOAD = "post_load" 78 | VALIDATES = "validates" 79 | VALIDATES_SCHEMA = "validates_schema" 80 | 81 | 82 | class MarshmallowHook: 83 | __marshmallow_hook__: dict[str, list[tuple[bool, Any]]] | None = None 84 | 85 | 86 | def validates(*field_names: str) -> Callable[..., Any]: 87 | """Register a validator method for field(s). 88 | 89 | :param field_names: Names of the fields that the method validates. 90 | 91 | .. versionchanged:: 4.0.0 Accepts multiple field names as positional arguments. 92 | .. versionchanged:: 4.0.0 Decorated methods receive ``data_key`` as a keyword argument. 93 | """ 94 | return set_hook(None, VALIDATES, field_names=field_names) 95 | 96 | 97 | def validates_schema( 98 | fn: Callable[..., Any] | None = None, 99 | *, 100 | pass_collection: bool = False, 101 | pass_original: bool = False, 102 | skip_on_field_errors: bool = True, 103 | ) -> Callable[..., Any]: 104 | """Register a schema-level validator. 105 | 106 | By default it receives a single object at a time, transparently handling the ``many`` 107 | argument passed to the `Schema `'s :func:`~marshmallow.Schema.validate` call. 108 | If ``pass_collection=True``, the raw data (which may be a collection) is passed. 109 | 110 | If ``pass_original=True``, the original data (before unmarshalling) will be passed as 111 | an additional argument to the method. 112 | 113 | If ``skip_on_field_errors=True``, this validation method will be skipped whenever 114 | validation errors have been detected when validating fields. 115 | 116 | .. versionchanged:: 3.0.0b1 ``skip_on_field_errors`` defaults to `True`. 117 | .. versionchanged:: 3.0.0 ``partial`` and ``many`` are always passed as keyword arguments to 118 | the decorated method. 119 | .. versionchanged:: 4.0.0 ``unknown`` is passed as a keyword argument to the decorated method. 120 | .. versionchanged:: 4.0.0 ``pass_many`` is renamed to ``pass_collection``. 121 | .. versionchanged:: 4.0.0 ``pass_collection``, ``pass_original``, and ``skip_on_field_errors`` 122 | are keyword-only arguments. 123 | """ 124 | return set_hook( 125 | fn, 126 | VALIDATES_SCHEMA, 127 | many=pass_collection, 128 | pass_original=pass_original, 129 | skip_on_field_errors=skip_on_field_errors, 130 | ) 131 | 132 | 133 | def pre_dump( 134 | fn: Callable[..., Any] | None = None, 135 | *, 136 | pass_collection: bool = False, 137 | ) -> Callable[..., Any]: 138 | """Register a method to invoke before serializing an object. The method 139 | receives the object to be serialized and returns the processed object. 140 | 141 | By default it receives a single object at a time, transparently handling the ``many`` 142 | argument passed to the `Schema `'s :func:`~marshmallow.Schema.dump` call. 143 | If ``pass_collection=True``, the raw data (which may be a collection) is passed. 144 | 145 | .. versionchanged:: 3.0.0 ``many`` is always passed as a keyword arguments to the decorated method. 146 | .. versionchanged:: 4.0.0 ``pass_many`` is renamed to ``pass_collection``. 147 | .. versionchanged:: 4.0.0 ``pass_collection`` is a keyword-only argument. 148 | """ 149 | return set_hook(fn, PRE_DUMP, many=pass_collection) 150 | 151 | 152 | def post_dump( 153 | fn: Callable[..., Any] | None = None, 154 | *, 155 | pass_collection: bool = False, 156 | pass_original: bool = False, 157 | ) -> Callable[..., Any]: 158 | """Register a method to invoke after serializing an object. The method 159 | receives the serialized object and returns the processed object. 160 | 161 | By default it receives a single object at a time, transparently handling the ``many`` 162 | argument passed to the `Schema `'s :func:`~marshmallow.Schema.dump` call. 163 | If ``pass_collection=True``, the raw data (which may be a collection) is passed. 164 | 165 | If ``pass_original=True``, the original data (before serializing) will be passed as 166 | an additional argument to the method. 167 | 168 | .. versionchanged:: 3.0.0 ``many`` is always passed as a keyword arguments to the decorated method. 169 | .. versionchanged:: 4.0.0 ``pass_many`` is renamed to ``pass_collection``. 170 | .. versionchanged:: 4.0.0 ``pass_collection`` and ``pass_original`` are keyword-only arguments. 171 | """ 172 | return set_hook(fn, POST_DUMP, many=pass_collection, pass_original=pass_original) 173 | 174 | 175 | def pre_load( 176 | fn: Callable[..., Any] | None = None, 177 | *, 178 | pass_collection: bool = False, 179 | ) -> Callable[..., Any]: 180 | """Register a method to invoke before deserializing an object. The method 181 | receives the data to be deserialized and returns the processed data. 182 | 183 | By default it receives a single object at a time, transparently handling the ``many`` 184 | argument passed to the `Schema `'s :func:`~marshmallow.Schema.load` call. 185 | If ``pass_collection=True``, the raw data (which may be a collection) is passed. 186 | 187 | .. versionchanged:: 3.0.0 ``partial`` and ``many`` are always passed as keyword arguments to 188 | the decorated method. 189 | .. versionchanged:: 4.0.0 ``pass_many`` is renamed to ``pass_collection``. 190 | .. versionchanged:: 4.0.0 ``pass_collection`` is a keyword-only argument. 191 | .. versionchanged:: 4.0.0 ``unknown`` is passed as a keyword argument to the decorated method. 192 | """ 193 | return set_hook(fn, PRE_LOAD, many=pass_collection) 194 | 195 | 196 | def post_load( 197 | fn: Callable[..., Any] | None = None, 198 | *, 199 | pass_collection: bool = False, 200 | pass_original: bool = False, 201 | ) -> Callable[..., Any]: 202 | """Register a method to invoke after deserializing an object. The method 203 | receives the deserialized data and returns the processed data. 204 | 205 | By default it receives a single object at a time, transparently handling the ``many`` 206 | argument passed to the `Schema `'s :func:`~marshmallow.Schema.load` call. 207 | If ``pass_collection=True``, the raw data (which may be a collection) is passed. 208 | 209 | If ``pass_original=True``, the original data (before deserializing) will be passed as 210 | an additional argument to the method. 211 | 212 | .. versionchanged:: 3.0.0 ``partial`` and ``many`` are always passed as keyword arguments to 213 | the decorated method. 214 | .. versionchanged:: 4.0.0 ``pass_many`` is renamed to ``pass_collection``. 215 | .. versionchanged:: 4.0.0 ``pass_collection`` and ``pass_original`` are keyword-only arguments. 216 | .. versionchanged:: 4.0.0 ``unknown`` is passed as a keyword argument to the decorated method. 217 | """ 218 | return set_hook(fn, POST_LOAD, many=pass_collection, pass_original=pass_original) 219 | 220 | 221 | def set_hook( 222 | fn: Callable[..., Any] | None, 223 | tag: str, 224 | *, 225 | many: bool = False, 226 | **kwargs: Any, 227 | ) -> Callable[..., Any]: 228 | """Mark decorated function as a hook to be picked up later. 229 | You should not need to use this method directly. 230 | 231 | .. note:: 232 | Currently only works with functions and instance methods. Class and 233 | static methods are not supported. 234 | 235 | :return: Decorated function if supplied, else this decorator with its args 236 | bound. 237 | """ 238 | # Allow using this as either a decorator or a decorator factory. 239 | if fn is None: 240 | return functools.partial(set_hook, tag=tag, many=many, **kwargs) 241 | 242 | # Set a __marshmallow_hook__ attribute instead of wrapping in some class, 243 | # because I still want this to end up as a normal (unbound) method. 244 | function = cast("MarshmallowHook", fn) 245 | try: 246 | hook_config = function.__marshmallow_hook__ 247 | except AttributeError: 248 | function.__marshmallow_hook__ = hook_config = defaultdict(list) 249 | # Also save the kwargs for the tagged function on 250 | # __marshmallow_hook__, keyed by 251 | if hook_config is not None: 252 | hook_config[tag].append((many, kwargs)) 253 | 254 | return fn 255 | -------------------------------------------------------------------------------- /src/marshmallow/error_store.py: -------------------------------------------------------------------------------- 1 | """Utilities for storing collections of error messages. 2 | 3 | .. warning:: 4 | 5 | This module is treated as private API. 6 | Users should not need to use this module directly. 7 | """ 8 | 9 | from marshmallow.exceptions import SCHEMA 10 | 11 | 12 | class ErrorStore: 13 | def __init__(self): 14 | #: Dictionary of errors stored during serialization 15 | self.errors = {} 16 | 17 | def store_error(self, messages, field_name=SCHEMA, index=None): 18 | # field error -> store/merge error messages under field name key 19 | # schema error -> if string or list, store/merge under _schema key 20 | # -> if dict, store/merge with other top-level keys 21 | if field_name != SCHEMA or not isinstance(messages, dict): 22 | messages = {field_name: messages} 23 | if index is not None: 24 | messages = {index: messages} 25 | self.errors = merge_errors(self.errors, messages) 26 | 27 | 28 | def merge_errors(errors1, errors2): # noqa: PLR0911 29 | """Deeply merge two error messages. 30 | 31 | The format of ``errors1`` and ``errors2`` matches the ``message`` 32 | parameter of :exc:`marshmallow.exceptions.ValidationError`. 33 | """ 34 | if not errors1: 35 | return errors2 36 | if not errors2: 37 | return errors1 38 | if isinstance(errors1, list): 39 | if isinstance(errors2, list): 40 | return errors1 + errors2 41 | if isinstance(errors2, dict): 42 | return dict(errors2, **{SCHEMA: merge_errors(errors1, errors2.get(SCHEMA))}) 43 | return [*errors1, errors2] 44 | if isinstance(errors1, dict): 45 | if isinstance(errors2, list): 46 | return dict(errors1, **{SCHEMA: merge_errors(errors1.get(SCHEMA), errors2)}) 47 | if isinstance(errors2, dict): 48 | errors = dict(errors1) 49 | for key, val in errors2.items(): 50 | if key in errors: 51 | errors[key] = merge_errors(errors[key], val) 52 | else: 53 | errors[key] = val 54 | return errors 55 | return dict(errors1, **{SCHEMA: merge_errors(errors1.get(SCHEMA), errors2)}) 56 | if isinstance(errors2, list): 57 | return [errors1, *errors2] 58 | if isinstance(errors2, dict): 59 | return dict(errors2, **{SCHEMA: merge_errors(errors1, errors2.get(SCHEMA))}) 60 | return [errors1, errors2] 61 | -------------------------------------------------------------------------------- /src/marshmallow/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exception classes for marshmallow-related errors.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing 6 | 7 | # Key used for schema-level validation errors 8 | SCHEMA = "_schema" 9 | 10 | 11 | class MarshmallowError(Exception): 12 | """Base class for all marshmallow-related errors.""" 13 | 14 | 15 | class ValidationError(MarshmallowError): 16 | """Raised when validation fails on a field or schema. 17 | 18 | Validators and custom fields should raise this exception. 19 | 20 | :param message: An error message, list of error messages, or dict of 21 | error messages. If a dict, the keys are subitems and the values are error messages. 22 | :param field_name: Field name to store the error on. 23 | If `None`, the error is stored as schema-level error. 24 | :param data: Raw input data. 25 | :param valid_data: Valid (de)serialized data. 26 | """ 27 | 28 | def __init__( 29 | self, 30 | message: str | list | dict, 31 | field_name: str = SCHEMA, 32 | data: typing.Mapping[str, typing.Any] 33 | | typing.Iterable[typing.Mapping[str, typing.Any]] 34 | | None = None, 35 | valid_data: list[typing.Any] | dict[str, typing.Any] | None = None, 36 | **kwargs, 37 | ): 38 | self.messages = [message] if isinstance(message, (str, bytes)) else message 39 | self.field_name = field_name 40 | self.data = data 41 | self.valid_data = valid_data 42 | self.kwargs = kwargs 43 | super().__init__(message) 44 | 45 | def normalized_messages(self): 46 | if self.field_name == SCHEMA and isinstance(self.messages, dict): 47 | return self.messages 48 | return {self.field_name: self.messages} 49 | 50 | @property 51 | def messages_dict(self) -> dict[str, typing.Any]: 52 | if not isinstance(self.messages, dict): 53 | raise TypeError( 54 | "cannot access 'messages_dict' when 'messages' is of type " 55 | + type(self.messages).__name__ 56 | ) 57 | return self.messages 58 | 59 | 60 | class RegistryError(NameError): 61 | """Raised when an invalid operation is performed on the serializer 62 | class registry. 63 | """ 64 | 65 | 66 | class StringNotCollectionError(MarshmallowError, TypeError): 67 | """Raised when a string is passed when a list of strings is expected.""" 68 | 69 | 70 | class _FieldInstanceResolutionError(MarshmallowError, TypeError): 71 | """Raised when an argument is passed to a field class that cannot be resolved to a Field instance.""" 72 | -------------------------------------------------------------------------------- /src/marshmallow/experimental/__init__.py: -------------------------------------------------------------------------------- 1 | """Experimental features. 2 | 3 | The features in this subpackage are experimental. Breaking changes may be 4 | introduced in minor marshmallow versions. 5 | """ 6 | -------------------------------------------------------------------------------- /src/marshmallow/experimental/context.py: -------------------------------------------------------------------------------- 1 | """Helper API for setting serialization/deserialization context. 2 | 3 | Example usage: 4 | 5 | .. code-block:: python 6 | 7 | import typing 8 | 9 | from marshmallow import Schema, fields 10 | from marshmallow.experimental.context import Context 11 | 12 | 13 | class UserContext(typing.TypedDict): 14 | suffix: str 15 | 16 | 17 | UserSchemaContext = Context[UserContext] 18 | 19 | 20 | class UserSchema(Schema): 21 | name_suffixed = fields.Function( 22 | lambda user: user["name"] + UserSchemaContext.get()["suffix"] 23 | ) 24 | 25 | 26 | with UserSchemaContext({"suffix": "bar"}): 27 | print(UserSchema().dump({"name": "foo"})) 28 | # {'name_suffixed': 'foobar'} 29 | """ 30 | 31 | from __future__ import annotations 32 | 33 | import contextlib 34 | import contextvars 35 | import typing 36 | 37 | try: 38 | from types import EllipsisType 39 | except ImportError: # Python<3.10 40 | EllipsisType = type(Ellipsis) # type: ignore[misc] 41 | 42 | _ContextT = typing.TypeVar("_ContextT") 43 | _DefaultT = typing.TypeVar("_DefaultT") 44 | _CURRENT_CONTEXT: contextvars.ContextVar = contextvars.ContextVar("context") 45 | 46 | 47 | class Context(contextlib.AbstractContextManager, typing.Generic[_ContextT]): 48 | """Context manager for setting and retrieving context. 49 | 50 | :param context: The context to use within the context manager scope. 51 | """ 52 | 53 | def __init__(self, context: _ContextT) -> None: 54 | self.context = context 55 | self.token: contextvars.Token | None = None 56 | 57 | def __enter__(self) -> Context[_ContextT]: 58 | self.token = _CURRENT_CONTEXT.set(self.context) 59 | return self 60 | 61 | def __exit__(self, *args, **kwargs) -> None: 62 | _CURRENT_CONTEXT.reset(typing.cast("contextvars.Token", self.token)) 63 | 64 | @classmethod 65 | def get(cls, default: _DefaultT | EllipsisType = ...) -> _ContextT | _DefaultT: 66 | """Get the current context. 67 | 68 | :param default: Default value to return if no context is set. 69 | If not provided and no context is set, a :exc:`LookupError` is raised. 70 | """ 71 | if default is not ...: 72 | return _CURRENT_CONTEXT.get(default) 73 | return _CURRENT_CONTEXT.get() 74 | -------------------------------------------------------------------------------- /src/marshmallow/orderedset.py: -------------------------------------------------------------------------------- 1 | # OrderedSet 2 | # Copyright (c) 2009 Raymond Hettinger 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation files 6 | # (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, 8 | # publish, distribute, sublicense, and/or sell copies of the Software, 9 | # and to permit persons to whom the Software is furnished to do so, 10 | # subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | from collections.abc import MutableSet 24 | 25 | 26 | class OrderedSet(MutableSet): 27 | def __init__(self, iterable=None): 28 | self.end = end = [] 29 | end += [None, end, end] # sentinel node for doubly linked list 30 | self.map = {} # key --> [key, prev, next] 31 | if iterable is not None: 32 | self |= iterable 33 | 34 | def __len__(self): 35 | return len(self.map) 36 | 37 | def __contains__(self, key): 38 | return key in self.map 39 | 40 | def add(self, key): 41 | if key not in self.map: 42 | end = self.end 43 | curr = end[1] 44 | curr[2] = end[1] = self.map[key] = [key, curr, end] 45 | 46 | def discard(self, key): 47 | if key in self.map: 48 | key, prev, next = self.map.pop(key) # noqa: A001 49 | prev[2] = next 50 | next[1] = prev 51 | 52 | def __iter__(self): 53 | end = self.end 54 | curr = end[2] 55 | while curr is not end: 56 | yield curr[0] 57 | curr = curr[2] 58 | 59 | def __reversed__(self): 60 | end = self.end 61 | curr = end[1] 62 | while curr is not end: 63 | yield curr[0] 64 | curr = curr[1] 65 | 66 | def pop(self, last=True): 67 | if not self: 68 | raise KeyError("set is empty") 69 | key = self.end[1][0] if last else self.end[2][0] 70 | self.discard(key) 71 | return key 72 | 73 | def __repr__(self): 74 | if not self: 75 | return f"{self.__class__.__name__}()" 76 | return f"{self.__class__.__name__}({list(self)!r})" 77 | 78 | def __eq__(self, other): 79 | if isinstance(other, OrderedSet): 80 | return len(self) == len(other) and list(self) == list(other) 81 | return set(self) == set(other) 82 | 83 | 84 | if __name__ == "__main__": 85 | s = OrderedSet("abracadaba") 86 | t = OrderedSet("simsalabim") 87 | print(s | t) 88 | print(s & t) 89 | print(s - t) 90 | -------------------------------------------------------------------------------- /src/marshmallow/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/marshmallow/7266de0c42e26c521801b7c01417d1f738e8a314/src/marshmallow/py.typed -------------------------------------------------------------------------------- /src/marshmallow/types.py: -------------------------------------------------------------------------------- 1 | """Type aliases. 2 | 3 | .. warning:: 4 | 5 | This module is provisional. Types may be modified, added, and removed between minor releases. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | try: 11 | from typing import TypeAlias 12 | except ImportError: # Remove when dropping Python 3.9 13 | from typing_extensions import TypeAlias 14 | 15 | import typing 16 | 17 | #: A type that can be either a sequence of strings or a set of strings 18 | StrSequenceOrSet: TypeAlias = typing.Union[ 19 | typing.Sequence[str], typing.AbstractSet[str] 20 | ] 21 | 22 | #: Type for validator functions 23 | Validator: TypeAlias = typing.Callable[[typing.Any], typing.Any] 24 | 25 | #: A valid option for the ``unknown`` schema option and argument 26 | UnknownOption: TypeAlias = typing.Literal["exclude", "include", "raise"] 27 | 28 | 29 | class SchemaValidator(typing.Protocol): 30 | def __call__( 31 | self, 32 | output: typing.Any, 33 | original_data: typing.Any = ..., 34 | *, 35 | partial: bool | StrSequenceOrSet | None = None, 36 | unknown: UnknownOption | None = None, 37 | many: bool = False, 38 | ) -> None: ... 39 | 40 | 41 | class RenderModule(typing.Protocol): 42 | def dumps( 43 | self, obj: typing.Any, *args: typing.Any, **kwargs: typing.Any 44 | ) -> str: ... 45 | 46 | def loads( 47 | self, s: str | bytes | bytearray, *args: typing.Any, **kwargs: typing.Any 48 | ) -> typing.Any: ... 49 | -------------------------------------------------------------------------------- /src/marshmallow/utils.py: -------------------------------------------------------------------------------- 1 | """Utility methods for marshmallow.""" 2 | 3 | # ruff: noqa: T201, T203 4 | from __future__ import annotations 5 | 6 | import datetime as dt 7 | import inspect 8 | import typing 9 | from collections.abc import Mapping, Sequence 10 | 11 | # Remove when we drop Python 3.9 12 | try: 13 | from typing import TypeGuard 14 | except ImportError: 15 | from typing_extensions import TypeGuard 16 | 17 | from marshmallow.constants import missing 18 | 19 | 20 | def is_generator(obj) -> TypeGuard[typing.Generator]: 21 | """Return True if ``obj`` is a generator""" 22 | return inspect.isgeneratorfunction(obj) or inspect.isgenerator(obj) 23 | 24 | 25 | def is_iterable_but_not_string(obj) -> TypeGuard[typing.Iterable]: 26 | """Return True if ``obj`` is an iterable object that isn't a string.""" 27 | return (hasattr(obj, "__iter__") and not hasattr(obj, "strip")) or is_generator(obj) 28 | 29 | 30 | def is_sequence_but_not_string(obj) -> TypeGuard[Sequence]: 31 | """Return True if ``obj`` is a sequence that isn't a string.""" 32 | return isinstance(obj, Sequence) and not isinstance(obj, (str, bytes)) 33 | 34 | 35 | def is_collection(obj) -> TypeGuard[typing.Iterable]: 36 | """Return True if ``obj`` is a collection type, e.g list, tuple, queryset.""" 37 | return is_iterable_but_not_string(obj) and not isinstance(obj, Mapping) 38 | 39 | 40 | # https://stackoverflow.com/a/27596917 41 | def is_aware(datetime: dt.datetime) -> bool: 42 | return ( 43 | datetime.tzinfo is not None and datetime.tzinfo.utcoffset(datetime) is not None 44 | ) 45 | 46 | 47 | def from_timestamp(value: typing.Any) -> dt.datetime: 48 | if value is True or value is False: 49 | raise ValueError("Not a valid POSIX timestamp") 50 | value = float(value) 51 | if value < 0: 52 | raise ValueError("Not a valid POSIX timestamp") 53 | 54 | # Load a timestamp with utc as timezone to prevent using system timezone. 55 | # Then set timezone to None, to let the Field handle adding timezone info. 56 | try: 57 | return dt.datetime.fromtimestamp(value, tz=dt.timezone.utc).replace(tzinfo=None) 58 | except OverflowError as exc: 59 | raise ValueError("Timestamp is too large") from exc 60 | except OSError as exc: 61 | raise ValueError("Error converting value to datetime") from exc 62 | 63 | 64 | def from_timestamp_ms(value: typing.Any) -> dt.datetime: 65 | value = float(value) 66 | return from_timestamp(value / 1000) 67 | 68 | 69 | def timestamp( 70 | value: dt.datetime, 71 | ) -> float: 72 | if not is_aware(value): 73 | # When a date is naive, use UTC as zone info to prevent using system timezone. 74 | value = value.replace(tzinfo=dt.timezone.utc) 75 | return value.timestamp() 76 | 77 | 78 | def timestamp_ms(value: dt.datetime) -> float: 79 | return timestamp(value) * 1000 80 | 81 | 82 | def ensure_text_type(val: str | bytes) -> str: 83 | if isinstance(val, bytes): 84 | val = val.decode("utf-8") 85 | return str(val) 86 | 87 | 88 | def pluck(dictlist: list[dict[str, typing.Any]], key: str): 89 | """Extracts a list of dictionary values from a list of dictionaries. 90 | :: 91 | 92 | >>> dlist = [{'id': 1, 'name': 'foo'}, {'id': 2, 'name': 'bar'}] 93 | >>> pluck(dlist, 'id') 94 | [1, 2] 95 | """ 96 | return [d[key] for d in dictlist] 97 | 98 | 99 | # Various utilities for pulling keyed values from objects 100 | 101 | 102 | def get_value(obj, key: int | str, default=missing): 103 | """Helper for pulling a keyed value off various types of objects. Fields use 104 | this method by default to access attributes of the source object. For object `x` 105 | and attribute `i`, this method first tries to access `x[i]`, and then falls back to 106 | `x.i` if an exception is raised. 107 | 108 | .. warning:: 109 | If an object `x` does not raise an exception when `x[i]` does not exist, 110 | `get_value` will never check the value `x.i`. Consider overriding 111 | `marshmallow.fields.Field.get_value` in this case. 112 | """ 113 | if not isinstance(key, int) and "." in key: 114 | return _get_value_for_keys(obj, key.split("."), default) 115 | return _get_value_for_key(obj, key, default) 116 | 117 | 118 | def _get_value_for_keys(obj, keys, default): 119 | if len(keys) == 1: 120 | return _get_value_for_key(obj, keys[0], default) 121 | return _get_value_for_keys( 122 | _get_value_for_key(obj, keys[0], default), keys[1:], default 123 | ) 124 | 125 | 126 | def _get_value_for_key(obj, key, default): 127 | if not hasattr(obj, "__getitem__"): 128 | return getattr(obj, key, default) 129 | 130 | try: 131 | return obj[key] 132 | except (KeyError, IndexError, TypeError, AttributeError): 133 | return getattr(obj, key, default) 134 | 135 | 136 | def set_value(dct: dict[str, typing.Any], key: str, value: typing.Any): 137 | """Set a value in a dict. If `key` contains a '.', it is assumed 138 | be a path (i.e. dot-delimited string) to the value's location. 139 | 140 | :: 141 | 142 | >>> d = {} 143 | >>> set_value(d, 'foo.bar', 42) 144 | >>> d 145 | {'foo': {'bar': 42}} 146 | """ 147 | if "." in key: 148 | head, rest = key.split(".", 1) 149 | target = dct.setdefault(head, {}) 150 | if not isinstance(target, dict): 151 | raise ValueError( 152 | f"Cannot set {key} in {head} due to existing value: {target}" 153 | ) 154 | set_value(target, rest, value) 155 | else: 156 | dct[key] = value 157 | 158 | 159 | def callable_or_raise(obj): 160 | """Check that an object is callable, else raise a :exc:`TypeError`.""" 161 | if not callable(obj): 162 | raise TypeError(f"Object {obj!r} is not callable.") 163 | return obj 164 | 165 | 166 | def timedelta_to_microseconds(value: dt.timedelta) -> int: 167 | """Compute the total microseconds of a timedelta. 168 | 169 | https://github.com/python/cpython/blob/v3.13.1/Lib/_pydatetime.py#L805-L807 170 | """ 171 | return (value.days * (24 * 3600) + value.seconds) * 1000000 + value.microseconds 172 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/marshmallow/7266de0c42e26c521801b7c01417d1f738e8a314/tests/__init__.py -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | """Test utilities and fixtures.""" 2 | 3 | import datetime as dt 4 | import functools 5 | import typing 6 | import uuid 7 | from enum import Enum, IntEnum 8 | from zoneinfo import ZoneInfo 9 | 10 | import simplejson 11 | 12 | from marshmallow import Schema, fields, missing, post_load, validate 13 | from marshmallow.exceptions import ValidationError 14 | 15 | central = ZoneInfo("America/Chicago") 16 | 17 | 18 | class GenderEnum(IntEnum): 19 | male = 1 20 | female = 2 21 | non_binary = 3 22 | 23 | 24 | class HairColorEnum(Enum): 25 | black = "black hair" 26 | brown = "brown hair" 27 | blond = "blond hair" 28 | red = "red hair" 29 | 30 | 31 | class DateEnum(Enum): 32 | date_1 = dt.date(2004, 2, 29) 33 | date_2 = dt.date(2008, 2, 29) 34 | date_3 = dt.date(2012, 2, 29) 35 | 36 | 37 | ALL_FIELDS = [ 38 | fields.String, 39 | fields.Integer, 40 | fields.Boolean, 41 | fields.Float, 42 | fields.DateTime, 43 | fields.Time, 44 | fields.Date, 45 | fields.TimeDelta, 46 | fields.Dict, 47 | fields.Url, 48 | fields.Email, 49 | fields.UUID, 50 | fields.Decimal, 51 | fields.IP, 52 | fields.IPv4, 53 | fields.IPv6, 54 | fields.IPInterface, 55 | fields.IPv4Interface, 56 | fields.IPv6Interface, 57 | functools.partial(fields.Enum, GenderEnum), 58 | functools.partial(fields.Enum, HairColorEnum, by_value=fields.String), 59 | functools.partial(fields.Enum, GenderEnum, by_value=fields.Integer), 60 | ] 61 | 62 | 63 | ##### Custom asserts ##### 64 | 65 | 66 | def assert_date_equal(d1: dt.date, d2: dt.date) -> None: 67 | assert d1.year == d2.year 68 | assert d1.month == d2.month 69 | assert d1.day == d2.day 70 | 71 | 72 | def assert_time_equal(t1: dt.time, t2: dt.time) -> None: 73 | assert t1.hour == t2.hour 74 | assert t1.minute == t2.minute 75 | assert t1.second == t2.second 76 | assert t1.microsecond == t2.microsecond 77 | 78 | 79 | ##### Validation ##### 80 | 81 | 82 | def predicate( 83 | func: typing.Callable[[typing.Any], bool], 84 | ) -> typing.Callable[[typing.Any], None]: 85 | def validate(value: typing.Any) -> None: 86 | if func(value) is False: 87 | raise ValidationError("Invalid value.") 88 | 89 | return validate 90 | 91 | 92 | ##### Models ##### 93 | 94 | 95 | class User: 96 | SPECIES = "Homo sapiens" 97 | 98 | def __init__( 99 | self, 100 | name, 101 | *, 102 | age=0, 103 | id_=None, 104 | homepage=None, 105 | email=None, 106 | registered=True, 107 | time_registered=None, 108 | birthdate=None, 109 | birthtime=None, 110 | balance=100, 111 | sex=GenderEnum.male, 112 | hair_color=HairColorEnum.black, 113 | employer=None, 114 | various_data=None, 115 | ): 116 | self.name = name 117 | self.age = age 118 | # A naive datetime 119 | self.created = dt.datetime(2013, 11, 10, 14, 20, 58) 120 | # A TZ-aware datetime 121 | self.updated = dt.datetime(2013, 11, 10, 14, 20, 58, tzinfo=central) 122 | self.id = id_ 123 | self.homepage = homepage 124 | self.email = email 125 | self.balance = balance 126 | self.registered = registered 127 | self.hair_colors = list(HairColorEnum.__members__) 128 | self.sex_choices = list(GenderEnum.__members__) 129 | self.finger_count = 10 130 | self.uid = uuid.uuid1() 131 | self.time_registered = time_registered or dt.time(1, 23, 45, 6789) 132 | self.birthdate = birthdate or dt.date(2013, 1, 23) 133 | self.birthtime = birthtime or dt.time(0, 1, 2, 3333) 134 | self.activation_date = dt.date(2013, 12, 11) 135 | self.sex = sex 136 | self.hair_color = hair_color 137 | self.employer = employer 138 | self.relatives = [] 139 | self.various_data = various_data or { 140 | "pets": ["cat", "dog"], 141 | "address": "1600 Pennsylvania Ave\nWashington, DC 20006", 142 | } 143 | 144 | @property 145 | def since_created(self): 146 | return dt.datetime(2013, 11, 24) - self.created 147 | 148 | def __repr__(self): 149 | return f"" 150 | 151 | 152 | class Blog: 153 | def __init__(self, title, user, collaborators=None, categories=None, id_=None): 154 | self.title = title 155 | self.user = user 156 | self.collaborators = collaborators or [] # List/tuple of users 157 | self.categories = categories 158 | self.id = id_ 159 | 160 | def __contains__(self, item): 161 | return item.name in [each.name for each in self.collaborators] 162 | 163 | 164 | class DummyModel: 165 | def __init__(self, foo): 166 | self.foo = foo 167 | 168 | def __eq__(self, other): 169 | return self.foo == other.foo 170 | 171 | def __str__(self): 172 | return f"bar {self.foo}" 173 | 174 | 175 | ###### Schemas ##### 176 | 177 | 178 | class Uppercased(fields.String): 179 | """Custom field formatting example.""" 180 | 181 | def _serialize(self, value, attr, obj, **kwargs): 182 | if value: 183 | return value.upper() 184 | return None 185 | 186 | 187 | def get_lowername(obj): 188 | if obj is None: 189 | return missing 190 | if isinstance(obj, dict): 191 | return obj.get("name", "").lower() 192 | return obj.name.lower() 193 | 194 | 195 | class UserSchema(Schema): 196 | name = fields.String() 197 | age: fields.Field = fields.Float() 198 | created = fields.DateTime() 199 | created_formatted = fields.DateTime( 200 | format="%Y-%m-%d", attribute="created", dump_only=True 201 | ) 202 | created_iso = fields.DateTime(format="iso", attribute="created", dump_only=True) 203 | updated = fields.DateTime() 204 | species = fields.String(attribute="SPECIES") 205 | id = fields.String(dump_default="no-id") 206 | uppername = Uppercased(attribute="name", dump_only=True) 207 | homepage = fields.Url() 208 | email = fields.Email() 209 | balance = fields.Decimal() 210 | is_old: fields.Field = fields.Method("get_is_old") 211 | lowername = fields.Function(get_lowername) 212 | registered = fields.Boolean() 213 | hair_colors = fields.List(fields.Raw) 214 | sex_choices = fields.List(fields.Raw) 215 | finger_count = fields.Integer() 216 | uid = fields.UUID() 217 | time_registered = fields.Time() 218 | birthdate = fields.Date() 219 | birthtime = fields.Time() 220 | activation_date = fields.Date() 221 | since_created = fields.TimeDelta() 222 | sex = fields.Str(validate=validate.OneOf(list(GenderEnum.__members__))) 223 | various_data = fields.Dict() 224 | 225 | class Meta: 226 | render_module = simplejson 227 | 228 | def get_is_old(self, obj): 229 | if obj is None: 230 | return missing 231 | if isinstance(obj, dict): 232 | age = obj.get("age", 0) 233 | else: 234 | age = obj.age 235 | try: 236 | return age > 80 237 | except TypeError as te: 238 | raise ValidationError(str(te)) from te 239 | 240 | @post_load 241 | def make_user(self, data, **kwargs): 242 | return User(**data) 243 | 244 | 245 | class UserExcludeSchema(UserSchema): 246 | class Meta: 247 | exclude = ("created", "updated") 248 | 249 | 250 | class UserIntSchema(UserSchema): 251 | age = fields.Integer() 252 | 253 | 254 | class UserFloatStringSchema(UserSchema): 255 | age = fields.Float(as_string=True) 256 | 257 | 258 | class ExtendedUserSchema(UserSchema): 259 | is_old = fields.Boolean() 260 | 261 | 262 | class UserRelativeUrlSchema(UserSchema): 263 | homepage = fields.Url(relative=True) 264 | 265 | 266 | class BlogSchema(Schema): 267 | title = fields.String() 268 | user = fields.Nested(UserSchema) 269 | collaborators = fields.List(fields.Nested(UserSchema())) 270 | categories = fields.List(fields.String) 271 | id = fields.String() 272 | 273 | 274 | class BlogOnlySchema(Schema): 275 | title = fields.String() 276 | user = fields.Nested(UserSchema) 277 | collaborators = fields.List(fields.Nested(UserSchema(only=("id",)))) 278 | 279 | 280 | class BlogSchemaExclude(BlogSchema): 281 | user = fields.Nested(UserSchema, exclude=("uppername", "species")) 282 | 283 | 284 | class BlogSchemaOnlyExclude(BlogSchema): 285 | user = fields.Nested(UserSchema, only=("name",), exclude=("name", "species")) 286 | 287 | 288 | class mockjson: # noqa: N801 289 | @staticmethod 290 | def dumps(val): 291 | return b"{'foo': 42}" 292 | 293 | @staticmethod 294 | def loads(val): 295 | return {"foo": 42} 296 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Pytest fixtures that are available in all test modules.""" 2 | 3 | import pytest 4 | 5 | from tests.base import Blog, User, UserSchema 6 | 7 | 8 | @pytest.fixture 9 | def user(): 10 | return User(name="Monty", age=42.3, homepage="http://monty.python.org/") 11 | 12 | 13 | @pytest.fixture 14 | def blog(user): 15 | col1 = User(name="Mick", age=123) 16 | col2 = User(name="Keith", age=456) 17 | return Blog( 18 | "Monty's blog", 19 | user=user, 20 | categories=["humor", "violence"], 21 | collaborators=[col1, col2], 22 | ) 23 | 24 | 25 | @pytest.fixture 26 | def serialized_user(user): 27 | return UserSchema().dump(user) 28 | -------------------------------------------------------------------------------- /tests/foo_serializer.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class FooSerializer(Schema): 5 | _id = fields.Integer() 6 | -------------------------------------------------------------------------------- /tests/mypy_test_cases/test_class_registry.py: -------------------------------------------------------------------------------- 1 | from marshmallow import class_registry 2 | 3 | # Works without passing `all` 4 | class_registry.get_class("MySchema") 5 | -------------------------------------------------------------------------------- /tests/mypy_test_cases/test_schema.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from marshmallow import EXCLUDE, Schema 4 | from marshmallow.fields import Integer, String 5 | 6 | 7 | # Test that valid `Meta` class attributes pass type checking 8 | class MySchema(Schema): 9 | foo = String() 10 | bar = Integer() 11 | 12 | class Meta(Schema.Meta): 13 | fields = ("foo", "bar") 14 | additional = ("baz", "qux") 15 | include = { 16 | "foo2": String(), 17 | } 18 | exclude = ("bar", "baz") 19 | many = True 20 | dateformat = "%Y-%m-%d" 21 | datetimeformat = "%Y-%m-%dT%H:%M:%S" 22 | timeformat = "%H:%M:%S" 23 | render_module = json 24 | ordered = False 25 | index_errors = True 26 | load_only = ("foo", "bar") 27 | dump_only = ("baz", "qux") 28 | unknown = EXCLUDE 29 | register = False 30 | -------------------------------------------------------------------------------- /tests/mypy_test_cases/test_validation_error.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import marshmallow as ma 4 | 5 | # OK types for 'message' 6 | ma.ValidationError("foo") 7 | ma.ValidationError(["foo"]) 8 | ma.ValidationError({"foo": "bar"}) 9 | 10 | # non-OK types for 'message' 11 | ma.ValidationError(0) # type: ignore[arg-type] 12 | 13 | # 'messages' is a dict|list 14 | err = ma.ValidationError("foo") 15 | a: dict | list = err.messages 16 | # union type can't assign to non-union type 17 | b: str = err.messages # type: ignore[assignment] 18 | c: dict = err.messages # type: ignore[assignment] 19 | # 'messages_dict' is a dict, so that it can assign to a dict 20 | d: dict = err.messages_dict 21 | -------------------------------------------------------------------------------- /tests/test_context.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | import pytest 4 | 5 | from marshmallow import ( 6 | Schema, 7 | fields, 8 | post_dump, 9 | post_load, 10 | pre_dump, 11 | pre_load, 12 | validates, 13 | validates_schema, 14 | ) 15 | from marshmallow.exceptions import ValidationError 16 | from marshmallow.experimental.context import Context 17 | from tests.base import Blog, User 18 | 19 | 20 | class UserContextSchema(Schema): 21 | is_owner = fields.Method("get_is_owner") 22 | is_collab = fields.Function( 23 | lambda user: user in Context[dict[str, typing.Any]].get()["blog"] 24 | ) 25 | 26 | def get_is_owner(self, user): 27 | return Context.get()["blog"].user.name == user.name 28 | 29 | 30 | class TestContext: 31 | def test_context_load_dump(self): 32 | class ContextField(fields.Integer): 33 | def _serialize(self, value, attr, obj, **kwargs): 34 | if (context := Context[dict].get(None)) is not None: 35 | value *= context.get("factor", 1) 36 | return super()._serialize(value, attr, obj, **kwargs) 37 | 38 | def _deserialize(self, value, attr, data, **kwargs): 39 | val = super()._deserialize(value, attr, data, **kwargs) 40 | if (context := Context[dict].get(None)) is not None: 41 | val *= context.get("factor", 1) 42 | return val 43 | 44 | class ContextSchema(Schema): 45 | ctx_fld = ContextField() 46 | 47 | ctx_schema = ContextSchema() 48 | 49 | assert ctx_schema.load({"ctx_fld": 1}) == {"ctx_fld": 1} 50 | assert ctx_schema.dump({"ctx_fld": 1}) == {"ctx_fld": 1} 51 | with Context({"factor": 2}): 52 | assert ctx_schema.load({"ctx_fld": 1}) == {"ctx_fld": 2} 53 | assert ctx_schema.dump({"ctx_fld": 1}) == {"ctx_fld": 2} 54 | 55 | def test_context_method(self): 56 | owner = User("Joe") 57 | blog = Blog(title="Joe Blog", user=owner) 58 | serializer = UserContextSchema() 59 | with Context({"blog": blog}): 60 | data = serializer.dump(owner) 61 | assert data["is_owner"] is True 62 | nonowner = User("Fred") 63 | data = serializer.dump(nonowner) 64 | assert data["is_owner"] is False 65 | 66 | def test_context_function(self): 67 | owner = User("Fred") 68 | blog = Blog("Killer Queen", user=owner) 69 | collab = User("Brian") 70 | blog.collaborators.append(collab) 71 | with Context({"blog": blog}): 72 | serializer = UserContextSchema() 73 | data = serializer.dump(collab) 74 | assert data["is_collab"] is True 75 | noncollab = User("Foo") 76 | data = serializer.dump(noncollab) 77 | assert data["is_collab"] is False 78 | 79 | def test_function_field_handles_bound_serializer(self): 80 | class SerializeA: 81 | def __call__(self, value): 82 | return "value" 83 | 84 | serialize = SerializeA() 85 | 86 | # only has a function field 87 | class UserFunctionContextSchema(Schema): 88 | is_collab = fields.Function(serialize) 89 | 90 | owner = User("Joe") 91 | serializer = UserFunctionContextSchema() 92 | data = serializer.dump(owner) 93 | assert data["is_collab"] == "value" 94 | 95 | def test_nested_fields_inherit_context(self): 96 | class InnerSchema(Schema): 97 | likes_bikes = fields.Function(lambda obj: "bikes" in Context.get()["info"]) 98 | 99 | class CSchema(Schema): 100 | inner = fields.Nested(InnerSchema) 101 | 102 | ser = CSchema() 103 | with Context[dict]({"info": "i like bikes"}): 104 | obj: dict[str, dict] = {"inner": {}} 105 | result = ser.dump(obj) 106 | assert result["inner"]["likes_bikes"] is True 107 | 108 | # Regression test for https://github.com/marshmallow-code/marshmallow/issues/820 109 | def test_nested_list_fields_inherit_context(self): 110 | class InnerSchema(Schema): 111 | foo = fields.Raw() 112 | 113 | @validates("foo") 114 | def validate_foo(self, value, **kwargs): 115 | if "foo_context" not in Context[dict].get(): 116 | raise ValidationError("Missing context") 117 | 118 | class OuterSchema(Schema): 119 | bars = fields.List(fields.Nested(InnerSchema())) 120 | 121 | inner = InnerSchema() 122 | with Context({"foo_context": "foo"}): 123 | assert inner.load({"foo": 42}) 124 | 125 | outer = OuterSchema() 126 | with Context({"foo_context": "foo"}): 127 | assert outer.load({"bars": [{"foo": 42}]}) 128 | 129 | # Regression test for https://github.com/marshmallow-code/marshmallow/issues/820 130 | def test_nested_dict_fields_inherit_context(self): 131 | class InnerSchema(Schema): 132 | foo = fields.Raw() 133 | 134 | @validates("foo") 135 | def validate_foo(self, value, **kwargs): 136 | if "foo_context" not in Context[dict].get(): 137 | raise ValidationError("Missing context") 138 | 139 | class OuterSchema(Schema): 140 | bars = fields.Dict(values=fields.Nested(InnerSchema())) 141 | 142 | inner = InnerSchema() 143 | with Context({"foo_context": "foo"}): 144 | assert inner.load({"foo": 42}) 145 | 146 | outer = OuterSchema() 147 | with Context({"foo_context": "foo"}): 148 | assert outer.load({"bars": {"test": {"foo": 42}}}) 149 | 150 | # Regression test for https://github.com/marshmallow-code/marshmallow/issues/1404 151 | def test_nested_field_with_unpicklable_object_in_context(self): 152 | class Unpicklable: 153 | def __deepcopy__(self, _): 154 | raise NotImplementedError 155 | 156 | class InnerSchema(Schema): 157 | foo = fields.Raw() 158 | 159 | class OuterSchema(Schema): 160 | inner = fields.Nested(InnerSchema()) 161 | 162 | outer = OuterSchema() 163 | obj = {"inner": {"foo": 42}} 164 | with Context({"unp": Unpicklable()}): 165 | assert outer.dump(obj) 166 | 167 | def test_function_field_passed_serialize_with_context(self, user): 168 | class Parent(Schema): 169 | pass 170 | 171 | field = fields.Function( 172 | serialize=lambda obj: obj.name.upper() + Context.get()["key"] 173 | ) 174 | field.parent = Parent() 175 | with Context({"key": "BAR"}): 176 | assert field.serialize("key", user) == "MONTYBAR" 177 | 178 | def test_function_field_deserialization_with_context(self): 179 | class Parent(Schema): 180 | pass 181 | 182 | field = fields.Function( 183 | lambda x: None, 184 | deserialize=lambda val: val.upper() + Context.get()["key"], 185 | ) 186 | field.parent = Parent() 187 | with Context({"key": "BAR"}): 188 | assert field.deserialize("foo") == "FOOBAR" 189 | 190 | def test_decorated_processors_with_context(self): 191 | NumDictContext = Context[dict[int, int]] 192 | 193 | class MySchema(Schema): 194 | f_1 = fields.Integer() 195 | f_2 = fields.Integer() 196 | f_3 = fields.Integer() 197 | f_4 = fields.Integer() 198 | 199 | @pre_dump 200 | def multiply_f_1(self, item, **kwargs): 201 | item["f_1"] *= NumDictContext.get()[1] 202 | return item 203 | 204 | @pre_load 205 | def multiply_f_2(self, data, **kwargs): 206 | data["f_2"] *= NumDictContext.get()[2] 207 | return data 208 | 209 | @post_dump 210 | def multiply_f_3(self, item, **kwargs): 211 | item["f_3"] *= NumDictContext.get()[3] 212 | return item 213 | 214 | @post_load 215 | def multiply_f_4(self, data, **kwargs): 216 | data["f_4"] *= NumDictContext.get()[4] 217 | return data 218 | 219 | schema = MySchema() 220 | 221 | with NumDictContext({1: 2, 2: 3, 3: 4, 4: 5}): 222 | assert schema.dump({"f_1": 1, "f_2": 1, "f_3": 1, "f_4": 1}) == { 223 | "f_1": 2, 224 | "f_2": 1, 225 | "f_3": 4, 226 | "f_4": 1, 227 | } 228 | assert schema.load({"f_1": 1, "f_2": 1, "f_3": 1, "f_4": 1}) == { 229 | "f_1": 1, 230 | "f_2": 3, 231 | "f_3": 1, 232 | "f_4": 5, 233 | } 234 | 235 | def test_validates_schema_with_context(self): 236 | class MySchema(Schema): 237 | f_1 = fields.Integer() 238 | f_2 = fields.Integer() 239 | 240 | @validates_schema 241 | def validate_schema(self, data, **kwargs): 242 | if data["f_2"] != data["f_1"] * Context.get(): 243 | raise ValidationError("Fail") 244 | 245 | schema = MySchema() 246 | 247 | with Context(2): 248 | schema.load({"f_1": 1, "f_2": 2}) 249 | with pytest.raises(ValidationError) as excinfo: 250 | schema.load({"f_1": 1, "f_2": 3}) 251 | assert excinfo.value.messages["_schema"] == ["Fail"] 252 | -------------------------------------------------------------------------------- /tests/test_error_store.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | 3 | from marshmallow import missing 4 | from marshmallow.error_store import merge_errors 5 | 6 | 7 | def test_missing_is_falsy(): 8 | assert bool(missing) is False 9 | 10 | 11 | class CustomError(NamedTuple): 12 | code: int 13 | message: str 14 | 15 | 16 | class TestMergeErrors: 17 | def test_merging_none_and_string(self): 18 | assert merge_errors(None, "error1") == "error1" 19 | 20 | def test_merging_none_and_custom_error(self): 21 | assert CustomError(123, "error1") == merge_errors( 22 | None, CustomError(123, "error1") 23 | ) 24 | 25 | def test_merging_none_and_list(self): 26 | assert merge_errors(None, ["error1", "error2"]) == ["error1", "error2"] 27 | 28 | def test_merging_none_and_dict(self): 29 | assert merge_errors(None, {"field1": "error1"}) == {"field1": "error1"} 30 | 31 | def test_merging_string_and_none(self): 32 | assert merge_errors("error1", None) == "error1" 33 | 34 | def test_merging_custom_error_and_none(self): 35 | assert CustomError(123, "error1") == merge_errors( 36 | CustomError(123, "error1"), None 37 | ) 38 | 39 | def test_merging_list_and_none(self): 40 | assert merge_errors(["error1", "error2"], None) == ["error1", "error2"] 41 | 42 | def test_merging_dict_and_none(self): 43 | assert merge_errors({"field1": "error1"}, None) == {"field1": "error1"} 44 | 45 | def test_merging_string_and_string(self): 46 | assert merge_errors("error1", "error2") == ["error1", "error2"] 47 | 48 | def test_merging_custom_error_and_string(self): 49 | assert [CustomError(123, "error1"), "error2"] == merge_errors( 50 | CustomError(123, "error1"), "error2" 51 | ) 52 | 53 | def test_merging_string_and_custom_error(self): 54 | assert ["error1", CustomError(123, "error2")] == merge_errors( 55 | "error1", CustomError(123, "error2") 56 | ) 57 | 58 | def test_merging_custom_error_and_custom_error(self): 59 | assert [CustomError(123, "error1"), CustomError(456, "error2")] == merge_errors( 60 | CustomError(123, "error1"), CustomError(456, "error2") 61 | ) 62 | 63 | def test_merging_string_and_list(self): 64 | assert merge_errors("error1", ["error2"]) == ["error1", "error2"] 65 | 66 | def test_merging_string_and_dict(self): 67 | assert merge_errors("error1", {"field1": "error2"}) == { 68 | "_schema": "error1", 69 | "field1": "error2", 70 | } 71 | 72 | def test_merging_string_and_dict_with_schema_error(self): 73 | assert merge_errors("error1", {"_schema": "error2", "field1": "error3"}) == { 74 | "_schema": ["error1", "error2"], 75 | "field1": "error3", 76 | } 77 | 78 | def test_merging_custom_error_and_list(self): 79 | assert [CustomError(123, "error1"), "error2"] == merge_errors( 80 | CustomError(123, "error1"), ["error2"] 81 | ) 82 | 83 | def test_merging_custom_error_and_dict(self): 84 | assert { 85 | "_schema": CustomError(123, "error1"), 86 | "field1": "error2", 87 | } == merge_errors(CustomError(123, "error1"), {"field1": "error2"}) 88 | 89 | def test_merging_custom_error_and_dict_with_schema_error(self): 90 | assert { 91 | "_schema": [CustomError(123, "error1"), "error2"], 92 | "field1": "error3", 93 | } == merge_errors( 94 | CustomError(123, "error1"), {"_schema": "error2", "field1": "error3"} 95 | ) 96 | 97 | def test_merging_list_and_string(self): 98 | assert merge_errors(["error1"], "error2") == ["error1", "error2"] 99 | 100 | def test_merging_list_and_custom_error(self): 101 | assert ["error1", CustomError(123, "error2")] == merge_errors( 102 | ["error1"], CustomError(123, "error2") 103 | ) 104 | 105 | def test_merging_list_and_list(self): 106 | assert merge_errors(["error1"], ["error2"]) == ["error1", "error2"] 107 | 108 | def test_merging_list_and_dict(self): 109 | assert merge_errors(["error1"], {"field1": "error2"}) == { 110 | "_schema": ["error1"], 111 | "field1": "error2", 112 | } 113 | 114 | def test_merging_list_and_dict_with_schema_error(self): 115 | assert merge_errors(["error1"], {"_schema": "error2", "field1": "error3"}) == { 116 | "_schema": ["error1", "error2"], 117 | "field1": "error3", 118 | } 119 | 120 | def test_merging_dict_and_string(self): 121 | assert merge_errors({"field1": "error1"}, "error2") == { 122 | "_schema": "error2", 123 | "field1": "error1", 124 | } 125 | 126 | def test_merging_dict_and_custom_error(self): 127 | assert { 128 | "_schema": CustomError(123, "error2"), 129 | "field1": "error1", 130 | } == merge_errors({"field1": "error1"}, CustomError(123, "error2")) 131 | 132 | def test_merging_dict_and_list(self): 133 | assert merge_errors({"field1": "error1"}, ["error2"]) == { 134 | "_schema": ["error2"], 135 | "field1": "error1", 136 | } 137 | 138 | def test_merging_dict_and_dict(self): 139 | assert merge_errors( 140 | {"field1": "error1", "field2": "error2"}, 141 | {"field2": "error3", "field3": "error4"}, 142 | ) == { 143 | "field1": "error1", 144 | "field2": ["error2", "error3"], 145 | "field3": "error4", 146 | } 147 | 148 | def test_deep_merging_dicts(self): 149 | assert merge_errors( 150 | {"field1": {"field2": "error1"}}, {"field1": {"field2": "error2"}} 151 | ) == {"field1": {"field2": ["error1", "error2"]}} 152 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from marshmallow.exceptions import ValidationError 4 | 5 | 6 | class TestValidationError: 7 | def test_stores_message_in_list(self): 8 | err = ValidationError("foo") 9 | assert err.messages == ["foo"] 10 | 11 | def test_can_pass_list_of_messages(self): 12 | err = ValidationError(["foo", "bar"]) 13 | assert err.messages == ["foo", "bar"] 14 | 15 | def test_stores_dictionaries(self): 16 | messages = {"user": {"email": ["email is invalid"]}} 17 | err = ValidationError(messages) 18 | assert err.messages == messages 19 | 20 | def test_can_store_field_name(self): 21 | err = ValidationError("invalid email", field_name="email") 22 | assert err.field_name == "email" 23 | 24 | def test_str(self): 25 | err = ValidationError("invalid email") 26 | assert str(err) == "invalid email" 27 | 28 | err2 = ValidationError("invalid email", "email") 29 | assert str(err2) == "invalid email" 30 | 31 | def test_stores_dictionaries_in_messages_dict(self): 32 | messages = {"user": {"email": ["email is invalid"]}} 33 | err = ValidationError(messages) 34 | assert err.messages_dict == messages 35 | 36 | def test_messages_dict_type_error_on_badval(self): 37 | err = ValidationError("foo") 38 | with pytest.raises(TypeError) as excinfo: 39 | err.messages_dict # noqa: B018 40 | assert "cannot access 'messages_dict' when 'messages' is of type list" in str( 41 | excinfo.value 42 | ) 43 | -------------------------------------------------------------------------------- /tests/test_options.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | from marshmallow import EXCLUDE, Schema, fields 4 | 5 | 6 | class UserSchema(Schema): 7 | name = fields.String(allow_none=True) 8 | email = fields.Email(allow_none=True) 9 | age = fields.Integer() 10 | created = fields.DateTime() 11 | id = fields.Integer(allow_none=True) 12 | homepage = fields.Url() 13 | birthdate = fields.Date() 14 | 15 | 16 | class ProfileSchema(Schema): 17 | user = fields.Nested(UserSchema) 18 | 19 | 20 | class TestFieldOrdering: 21 | def test_declared_field_order_is_maintained_on_dump(self, user): 22 | ser = UserSchema() 23 | data = ser.dump(user) 24 | keys = list(data) 25 | assert keys == [ 26 | "name", 27 | "email", 28 | "age", 29 | "created", 30 | "id", 31 | "homepage", 32 | "birthdate", 33 | ] 34 | 35 | def test_declared_field_order_is_maintained_on_load(self, serialized_user): 36 | schema = UserSchema(unknown=EXCLUDE) 37 | data = schema.load(serialized_user) 38 | keys = list(data) 39 | assert keys == [ 40 | "name", 41 | "email", 42 | "age", 43 | "created", 44 | "id", 45 | "homepage", 46 | "birthdate", 47 | ] 48 | 49 | def test_nested_field_order_with_only_arg_is_maintained_on_dump(self, user): 50 | schema = ProfileSchema() 51 | data = schema.dump({"user": user}) 52 | user_data = data["user"] 53 | keys = list(user_data) 54 | assert keys == [ 55 | "name", 56 | "email", 57 | "age", 58 | "created", 59 | "id", 60 | "homepage", 61 | "birthdate", 62 | ] 63 | 64 | def test_nested_field_order_with_only_arg_is_maintained_on_load(self): 65 | schema = ProfileSchema() 66 | data = schema.load( 67 | { 68 | "user": { 69 | "name": "Foo", 70 | "email": "Foo@bar.com", 71 | "age": 42, 72 | "created": dt.datetime.now().isoformat(), 73 | "id": 123, 74 | "homepage": "http://foo.com", 75 | "birthdate": dt.datetime.now().date().isoformat(), 76 | } 77 | } 78 | ) 79 | user_data = data["user"] 80 | keys = list(user_data) 81 | assert keys == [ 82 | "name", 83 | "email", 84 | "age", 85 | "created", 86 | "id", 87 | "homepage", 88 | "birthdate", 89 | ] 90 | 91 | def test_nested_field_order_with_exclude_arg_is_maintained(self, user): 92 | class HasNestedExclude(Schema): 93 | user = fields.Nested(UserSchema, exclude=("birthdate",)) 94 | 95 | ser = HasNestedExclude() 96 | data = ser.dump({"user": user}) 97 | user_data = data["user"] 98 | keys = list(user_data) 99 | assert keys == ["name", "email", "age", "created", "id", "homepage"] 100 | 101 | 102 | class TestIncludeOption: 103 | class AddFieldsSchema(Schema): 104 | name = fields.Str() 105 | 106 | class Meta: 107 | include = {"from": fields.Str()} 108 | 109 | def test_fields_are_added(self): 110 | s = self.AddFieldsSchema() 111 | in_data = {"name": "Steve", "from": "Oskosh"} 112 | result = s.load({"name": "Steve", "from": "Oskosh"}) 113 | assert result == in_data 114 | 115 | def test_included_fields_ordered_after_declared_fields(self): 116 | class AddFieldsOrdered(Schema): 117 | name = fields.Str() 118 | email = fields.Str() 119 | 120 | class Meta: 121 | include = { 122 | "from": fields.Str(), 123 | "in": fields.Str(), 124 | "@at": fields.Str(), 125 | } 126 | 127 | s = AddFieldsOrdered() 128 | in_data = { 129 | "name": "Steve", 130 | "from": "Oskosh", 131 | "email": "steve@steve.steve", 132 | "in": "VA", 133 | "@at": "Charlottesville", 134 | } 135 | # declared fields, then "included" fields 136 | expected_fields = ["name", "email", "from", "in", "@at"] 137 | assert list(AddFieldsOrdered._declared_fields.keys()) == expected_fields 138 | 139 | result = s.load(in_data) 140 | assert list(result.keys()) == expected_fields 141 | 142 | def test_added_fields_are_inherited(self): 143 | class AddFieldsChild(self.AddFieldsSchema): # type: ignore[name-defined] 144 | email = fields.Str() 145 | 146 | s = AddFieldsChild() 147 | assert "email" in s._declared_fields 148 | assert "from" in s._declared_fields 149 | assert isinstance(s._declared_fields["from"], fields.Str) 150 | 151 | 152 | class TestManyOption: 153 | class ManySchema(Schema): 154 | foo = fields.Str() 155 | 156 | class Meta: 157 | many = True 158 | 159 | def test_many_by_default(self): 160 | test = self.ManySchema() 161 | assert test.load([{"foo": "bar"}]) == [{"foo": "bar"}] 162 | 163 | def test_explicit_single(self): 164 | test = self.ManySchema(many=False) 165 | assert test.load({"foo": "bar"}) == {"foo": "bar"} 166 | -------------------------------------------------------------------------------- /tests/test_registry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from marshmallow import Schema, class_registry, fields 4 | from marshmallow.exceptions import RegistryError 5 | 6 | 7 | def test_serializer_has_class_registry(): 8 | class MySchema(Schema): 9 | pass 10 | 11 | class MySubSchema(Schema): 12 | pass 13 | 14 | assert "MySchema" in class_registry._registry 15 | assert "MySubSchema" in class_registry._registry 16 | 17 | # by fullpath 18 | assert "tests.test_registry.MySchema" in class_registry._registry 19 | assert "tests.test_registry.MySubSchema" in class_registry._registry 20 | 21 | 22 | def test_register_class_meta_option(): 23 | class UnregisteredSchema(Schema): 24 | class Meta: 25 | register = False 26 | 27 | class RegisteredSchema(Schema): 28 | class Meta: 29 | register = True 30 | 31 | class RegisteredOverrideSchema(UnregisteredSchema): 32 | class Meta: 33 | register = True 34 | 35 | class UnregisteredOverrideSchema(RegisteredSchema): 36 | class Meta: 37 | register = False 38 | 39 | assert "UnregisteredSchema" not in class_registry._registry 40 | assert "tests.test_registry.UnregisteredSchema" not in class_registry._registry 41 | 42 | assert "RegisteredSchema" in class_registry._registry 43 | assert "tests.test_registry.RegisteredSchema" in class_registry._registry 44 | 45 | assert "RegisteredOverrideSchema" in class_registry._registry 46 | assert "tests.test_registry.RegisteredOverrideSchema" in class_registry._registry 47 | 48 | assert "UnregisteredOverrideSchema" not in class_registry._registry 49 | assert ( 50 | "tests.test_registry.UnregisteredOverrideSchema" not in class_registry._registry 51 | ) 52 | 53 | 54 | def test_serializer_class_registry_register_same_classname_different_module(): 55 | reglen = len(class_registry._registry) 56 | 57 | type("MyTestRegSchema", (Schema,), {"__module__": "modA"}) 58 | 59 | assert "MyTestRegSchema" in class_registry._registry 60 | result = class_registry._registry.get("MyTestRegSchema") 61 | assert isinstance(result, list) 62 | assert len(result) == 1 63 | assert "modA.MyTestRegSchema" in class_registry._registry 64 | # storing for classname and fullpath 65 | assert len(class_registry._registry) == reglen + 2 66 | 67 | type("MyTestRegSchema", (Schema,), {"__module__": "modB"}) 68 | 69 | assert "MyTestRegSchema" in class_registry._registry 70 | # aggregating classes with same name from different modules 71 | result = class_registry._registry.get("MyTestRegSchema") 72 | assert isinstance(result, list) 73 | assert len(result) == 2 74 | assert "modB.MyTestRegSchema" in class_registry._registry 75 | # storing for same classname (+0) and different module (+1) 76 | assert len(class_registry._registry) == reglen + 2 + 1 77 | 78 | type("MyTestRegSchema", (Schema,), {"__module__": "modB"}) 79 | 80 | assert "MyTestRegSchema" in class_registry._registry 81 | # only the class with matching module has been replaced 82 | result = class_registry._registry.get("MyTestRegSchema") 83 | assert isinstance(result, list) 84 | assert len(result) == 2 85 | assert "modB.MyTestRegSchema" in class_registry._registry 86 | # only the class with matching module has been replaced (+0) 87 | assert len(class_registry._registry) == reglen + 2 + 1 88 | 89 | 90 | def test_serializer_class_registry_override_if_same_classname_same_module(): 91 | reglen = len(class_registry._registry) 92 | 93 | type("MyTestReg2Schema", (Schema,), {"__module__": "SameModulePath"}) 94 | 95 | assert "MyTestReg2Schema" in class_registry._registry 96 | result = class_registry._registry.get("MyTestReg2Schema") 97 | assert isinstance(result, list) 98 | assert len(result) == 1 99 | assert "SameModulePath.MyTestReg2Schema" in class_registry._registry 100 | result = class_registry._registry.get("SameModulePath.MyTestReg2Schema") 101 | assert isinstance(result, list) 102 | assert len(result) == 1 103 | # storing for classname and fullpath 104 | assert len(class_registry._registry) == reglen + 2 105 | 106 | type("MyTestReg2Schema", (Schema,), {"__module__": "SameModulePath"}) 107 | 108 | assert "MyTestReg2Schema" in class_registry._registry 109 | # overriding same class name and same module 110 | result = class_registry._registry.get("MyTestReg2Schema") 111 | assert isinstance(result, list) 112 | assert len(result) == 1 113 | 114 | assert "SameModulePath.MyTestReg2Schema" in class_registry._registry 115 | # overriding same fullpath 116 | result = class_registry._registry.get("SameModulePath.MyTestReg2Schema") 117 | assert isinstance(result, list) 118 | assert len(result) == 1 119 | # overriding for same classname (+0) and different module (+0) 120 | assert len(class_registry._registry) == reglen + 2 121 | 122 | 123 | class A: 124 | def __init__(self, _id, b=None): 125 | self.id = _id 126 | self.b = b 127 | 128 | 129 | class B: 130 | def __init__(self, _id, a=None): 131 | self.id = _id 132 | self.a = a 133 | 134 | 135 | class C: 136 | def __init__(self, _id, bs=None): 137 | self.id = _id 138 | self.bs = bs or [] 139 | 140 | 141 | class ASchema(Schema): 142 | id = fields.Integer() 143 | b = fields.Nested("tests.test_registry.BSchema", exclude=("a",)) 144 | 145 | 146 | class BSchema(Schema): 147 | id = fields.Integer() 148 | a = fields.Nested("tests.test_registry.ASchema") 149 | 150 | 151 | class CSchema(Schema): 152 | id = fields.Integer() 153 | bs = fields.Nested("tests.test_registry.BSchema", many=True) 154 | 155 | 156 | def test_two_way_nesting(): 157 | a_obj = A(1) 158 | b_obj = B(2, a=a_obj) 159 | a_obj.b = b_obj 160 | 161 | a_serialized = ASchema().dump(a_obj) 162 | b_serialized = BSchema().dump(b_obj) 163 | assert a_serialized["b"]["id"] == b_obj.id 164 | assert b_serialized["a"]["id"] == a_obj.id 165 | 166 | 167 | def test_nesting_with_class_name_many(): 168 | c_obj = C(1, bs=[B(2), B(3), B(4)]) 169 | 170 | c_serialized = CSchema().dump(c_obj) 171 | 172 | assert len(c_serialized["bs"]) == len(c_obj.bs) 173 | assert c_serialized["bs"][0]["id"] == c_obj.bs[0].id 174 | 175 | 176 | def test_invalid_class_name_in_nested_field_raises_error(user): 177 | class MySchema(Schema): 178 | nf = fields.Nested("notfound") 179 | 180 | sch = MySchema() 181 | msg = "Class with name {!r} was not found".format("notfound") 182 | with pytest.raises(RegistryError, match=msg): 183 | sch.dump({"nf": None}) 184 | 185 | 186 | class FooSerializer(Schema): 187 | _id = fields.Integer() 188 | 189 | 190 | def test_multiple_classes_with_same_name_raises_error(): 191 | # Import a class with the same name 192 | from .foo_serializer import FooSerializer as FooSerializer1 # noqa: F401 193 | 194 | class MySchema(Schema): 195 | foo = fields.Nested("FooSerializer") 196 | 197 | # Using a nested field with the class name fails because there are 198 | # two defined classes with the same name 199 | sch = MySchema() 200 | msg = "Multiple classes with name {!r} were found.".format("FooSerializer") 201 | with pytest.raises(RegistryError, match=msg): 202 | sch.dump({"foo": {"_id": 1}}) 203 | 204 | 205 | def test_multiple_classes_with_all(): 206 | # Import a class with the same name 207 | from .foo_serializer import FooSerializer as FooSerializer1 # noqa: F401 208 | 209 | classes = class_registry.get_class("FooSerializer", all=True) 210 | assert len(classes) == 2 211 | 212 | 213 | def test_can_use_full_module_path_to_class(): 214 | from .foo_serializer import FooSerializer as FooSerializer1 # noqa: F401 215 | 216 | # Using full paths is ok 217 | 218 | class Schema1(Schema): 219 | foo = fields.Nested("tests.foo_serializer.FooSerializer") 220 | 221 | sch = Schema1() 222 | 223 | # Note: The arguments here don't matter. What matters is that no 224 | # error is raised 225 | assert sch.dump({"foo": {"_id": 42}}) 226 | 227 | class Schema2(Schema): 228 | foo = fields.Nested("tests.test_registry.FooSerializer") 229 | 230 | sch2 = Schema2() 231 | assert sch2.dump({"foo": {"_id": 42}}) 232 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import datetime as dt 4 | from copy import copy, deepcopy 5 | from typing import NamedTuple 6 | 7 | import pytest 8 | 9 | from marshmallow import Schema, fields, utils 10 | 11 | 12 | def test_missing_singleton_copy(): 13 | assert copy(utils.missing) is utils.missing 14 | assert deepcopy(utils.missing) is utils.missing 15 | 16 | 17 | class PointNT(NamedTuple): 18 | x: int | None 19 | y: int | None 20 | 21 | 22 | class PointClass: 23 | def __init__(self, x, y): 24 | self.x = x 25 | self.y = y 26 | 27 | 28 | class PointDict(dict): 29 | def __init__(self, x, y): 30 | super().__init__({"x": x}) 31 | self.y = y 32 | 33 | 34 | @pytest.mark.parametrize( 35 | "obj", [PointNT(24, 42), PointClass(24, 42), PointDict(24, 42), {"x": 24, "y": 42}] 36 | ) 37 | def test_get_value_from_object(obj): 38 | assert utils.get_value(obj, "x") == 24 39 | assert utils.get_value(obj, "y") == 42 40 | 41 | 42 | def test_get_value_from_namedtuple_with_default(): 43 | p = PointNT(x=42, y=None) 44 | # Default is only returned if key is not found 45 | assert utils.get_value(p, "z", default=123) == 123 46 | # since 'y' is an attribute, None is returned instead of the default 47 | assert utils.get_value(p, "y", default=123) is None 48 | 49 | 50 | class Triangle: 51 | def __init__(self, p1, p2, p3): 52 | self.p1 = p1 53 | self.p2 = p2 54 | self.p3 = p3 55 | self.points = [p1, p2, p3] 56 | 57 | 58 | def test_get_value_for_nested_object(): 59 | tri = Triangle(p1=PointClass(1, 2), p2=PointNT(3, 4), p3={"x": 5, "y": 6}) 60 | assert utils.get_value(tri, "p1.x") == 1 61 | assert utils.get_value(tri, "p2.x") == 3 62 | assert utils.get_value(tri, "p3.x") == 5 63 | 64 | 65 | # regression test for https://github.com/marshmallow-code/marshmallow/issues/62 66 | def test_get_value_from_dict(): 67 | d = dict(items=["foo", "bar"], keys=["baz", "quux"]) 68 | assert utils.get_value(d, "items") == ["foo", "bar"] 69 | assert utils.get_value(d, "keys") == ["baz", "quux"] 70 | 71 | 72 | def test_get_value(): 73 | lst = [1, 2, 3] 74 | assert utils.get_value(lst, 1) == 2 75 | 76 | class MyInt(int): 77 | pass 78 | 79 | assert utils.get_value(lst, MyInt(1)) == 2 80 | 81 | 82 | def test_set_value(): 83 | d: dict[str, int | dict] = {} 84 | utils.set_value(d, "foo", 42) 85 | assert d == {"foo": 42} 86 | 87 | d = {} 88 | utils.set_value(d, "foo.bar", 42) 89 | assert d == {"foo": {"bar": 42}} 90 | 91 | d = {"foo": {}} 92 | utils.set_value(d, "foo.bar", 42) 93 | assert d == {"foo": {"bar": 42}} 94 | 95 | d = {"foo": 42} 96 | with pytest.raises(ValueError): 97 | utils.set_value(d, "foo.bar", 42) 98 | 99 | 100 | def test_is_collection(): 101 | assert utils.is_collection([1, "foo", {}]) is True 102 | assert utils.is_collection(("foo", 2.3)) is True 103 | assert utils.is_collection({"foo": "bar"}) is False 104 | 105 | 106 | @pytest.mark.parametrize( 107 | ("value", "expected"), 108 | [ 109 | (1676386740, dt.datetime(2023, 2, 14, 14, 59, 00)), 110 | (1676386740.58, dt.datetime(2023, 2, 14, 14, 59, 00, 580000)), 111 | ], 112 | ) 113 | def test_from_timestamp(value, expected): 114 | result = utils.from_timestamp(value) 115 | assert type(result) is dt.datetime 116 | assert result == expected 117 | 118 | 119 | def test_from_timestamp_with_negative_value(): 120 | value = -10 121 | with pytest.raises(ValueError, match=r"Not a valid POSIX timestamp"): 122 | utils.from_timestamp(value) 123 | 124 | 125 | def test_from_timestamp_with_overflow_value(): 126 | value = 9223372036854775 127 | with pytest.raises(ValueError, match="out of range"): 128 | utils.from_timestamp(value) 129 | 130 | 131 | # Regression test for https://github.com/marshmallow-code/marshmallow/issues/540 132 | def test_function_field_using_type_annotation(): 133 | def get_split_words(value: str): 134 | return value.split(";") 135 | 136 | class MySchema(Schema): 137 | friends = fields.Function(deserialize=get_split_words) 138 | 139 | data = {"friends": "Clark;Alfred;Robin"} 140 | result = MySchema().load(data) 141 | assert result == {"friends": ["Clark", "Alfred", "Robin"]} 142 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = lint,mypy,py{39,310,311,312,313},docs 3 | 4 | [testenv] 5 | extras = tests 6 | commands = pytest {posargs} 7 | 8 | [testenv:lint] 9 | deps = pre-commit>=3.5,<5.0 10 | skip_install = true 11 | commands = pre-commit run --all-files 12 | 13 | [testenv:mypy] 14 | deps = 15 | mypy>=1.14.1 16 | types-simplejson 17 | commands = mypy --show-error-codes 18 | 19 | [testenv:docs] 20 | extras = docs 21 | commands = sphinx-build --fresh-env docs/ docs/_build {posargs} 22 | 23 | ; Below tasks are for development only (not run in CI) 24 | 25 | [testenv:docs-serve] 26 | deps = sphinx-autobuild 27 | extras = docs 28 | commands = sphinx-autobuild --port=0 --open-browser --delay=2 docs/ docs/_build {posargs} --watch src --watch CONTRIBUTING.rst --watch README.rst 29 | 30 | [testenv:readme-serve] 31 | deps = restview 32 | skip_install = true 33 | commands = restview README.rst 34 | 35 | [testenv:benchmark] 36 | usedevelop = true 37 | commands = python performance/benchmark.py --iterations=100 --repeat=3 38 | --------------------------------------------------------------------------------