├── .all-contributorsrc ├── .coveragerc ├── .gitattributes ├── .github ├── dependabot.yml ├── release.yml └── workflows │ ├── prerelease.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── bench ├── __init__.py ├── attrs_class.py ├── bench.py ├── cattrs_class.py ├── charts │ ├── asdict-medium.png │ ├── asdict-small.png │ ├── astuple-medium.png │ ├── astuple-small.png │ ├── de-medium.png │ ├── de-small.png │ ├── se-medium.png │ └── se-small.png ├── dacite_class.py ├── data.py ├── dataclasses_class.py ├── marshmallow_class.py ├── mashumaro_class.py ├── pyproject.toml ├── pyserde_class.py ├── raw.py └── runner.py ├── conftest.py ├── docs ├── en │ ├── SUMMARY.md │ ├── book.toml │ ├── class-attributes.md │ ├── data-formats.md │ ├── decorators.md │ ├── extension.md │ ├── faq.md │ ├── field-attributes.md │ ├── getting-started.md │ ├── introduction.md │ ├── type-check.md │ ├── types.md │ └── union.md └── ja │ ├── SUMMARY.md │ ├── book.toml │ ├── class-attributes.md │ ├── data-formats.md │ ├── decorators.md │ ├── extension.md │ ├── faq.md │ ├── field-attributes.md │ ├── getting-started.md │ ├── introduction.md │ ├── type-check.md │ ├── types.md │ └── union.md ├── examples ├── Pipfile ├── __init__.py ├── alias.py ├── any.py ├── app.yml ├── class_var.py ├── collection.py ├── custom_class_serializer.py ├── custom_field_serializer.py ├── custom_legacy_class_serializer.py ├── default.py ├── default_dict.py ├── deny_unknown_fields.py ├── enum34.py ├── env.py ├── field_order.py ├── flatten.py ├── forward_reference.py ├── frozen_set.py ├── generics.py ├── generics_nested.py ├── generics_pep695.py ├── global_custom_class_serializer.py ├── imported.py ├── init_var.py ├── jsonfile.py ├── kw_only.py ├── lazy_type_evaluation.py ├── literal.py ├── msg_pack.py ├── nested.py ├── newtype.py ├── pep681.py ├── plain_dataclass.py ├── plain_dataclass_class_attribute.py ├── primitive_subclass.py ├── python_pickle.py ├── recursive.py ├── recursive_list.py ├── recursive_union.py ├── rename.py ├── rename_all.py ├── runner.py ├── simple.py ├── skip.py ├── swagger.yml ├── tomlfile.py ├── type_alias_pep695.py ├── type_check_coerce.py ├── type_check_disabled.py ├── type_datetime.py ├── type_decimal.py ├── type_ipaddress.py ├── type_numpy.py ├── type_numpy_jaxtyping.py ├── type_pathlib.py ├── type_sqlalchemy.py ├── type_uuid.py ├── union.py ├── union_directly.py ├── union_tagging.py ├── user_exception.py ├── variable_length_tuple.py └── yamlfile.py ├── profile_codegen.py ├── pyproject.toml ├── serde ├── __init__.py ├── compat.py ├── core.py ├── de.py ├── inspect.py ├── json.py ├── msgpack.py ├── numpy.py ├── pickle.py ├── py.typed ├── se.py ├── sqlalchemy.py ├── toml.py └── yaml.py ├── setup.cfg └── tests ├── __init__.py ├── common.py ├── data.py ├── imported.py ├── test_basics.py ├── test_beartype.py ├── test_code_completion.py ├── test_compat.py ├── test_core.py ├── test_custom.py ├── test_de.py ├── test_flatten.py ├── test_json.py ├── test_kwonly.py ├── test_lazy_type_evaluation.py ├── test_legacy_custom.py ├── test_literal.py ├── test_numpy.py ├── test_se.py ├── test_sqlalchemy.py ├── test_toml.py ├── test_type_alias.py ├── test_type_check.py ├── test_union.py └── test_yaml.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = serde 4 | 5 | [report] 6 | exclude_also = 7 | def __repr__ 8 | if self.debug: 9 | if settings.DEBUG 10 | raise AssertionError 11 | raise NotImplementedError 12 | if 0: 13 | if __name__ == .__main__.: 14 | if TYPE_CHECKING: 15 | class .*\bProtocol\): 16 | @(abc\.)?abstractmethod 17 | @overload 18 | ignore_errors = True 19 | omit = 20 | tests/* 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | Makefile linguist-vendored 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | labels: 11 | - "build" 12 | assignees: 13 | - "yukinarit" 14 | schedule: 15 | interval: "weekly" 16 | time: "09:00" 17 | timezone: "Asia/Tokyo" 18 | - package-ecosystem: "github-actions" 19 | directory: "/" 20 | labels: 21 | - "build" 22 | assignees: 23 | - "yukinarit" 24 | schedule: 25 | interval: "weekly" 26 | time: "09:00" 27 | timezone: "Asia/Tokyo" 28 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - octocat 7 | categories: 8 | - title: New features 9 | labels: 10 | - enhancement 11 | - title: Bug fixes 12 | labels: 13 | - bug 14 | - title: Breaking changes 15 | labels: 16 | - breaking-change 17 | - title: Style fixes 18 | labels: 19 | - style 20 | - title: CI 21 | labels: 22 | - ci 23 | - title: Build 24 | labels: 25 | - build 26 | - title: Refactoring 27 | labels: 28 | - refactoring 29 | - title: Test 30 | labels: 31 | - test 32 | - title: Documentation 33 | labels: 34 | - documentation 35 | - title: Performance improvements 36 | labels: 37 | - performance 38 | - title: Other changes 39 | labels: 40 | - "*" 41 | -------------------------------------------------------------------------------- /.github/workflows/prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Build package 2 | 3 | on: 4 | release: 5 | types: [prereleased] 6 | 7 | jobs: 8 | prerelease: 9 | name: Pre Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | - name: Install poetry 16 | run: pip install poetry poetry-dynamic-versioning 17 | - name: Set up Python 3.10 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.10" 21 | cache: poetry 22 | cache-dependency-path: pyproject.toml 23 | - name: Install dependencies 24 | run: make setup 25 | - name: Build package to make sure dynamic versioning works 26 | run: | 27 | poetry publish --build --dry-run 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Install poetry 15 | run: pip install poetry poetry-dynamic-versioning 16 | - name: Set up Python 3.10 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.10" 20 | cache: poetry 21 | cache-dependency-path: pyproject.toml 22 | - name: Install dependencies 23 | run: make setup 24 | - name: Publish to PyPI 25 | run: | 26 | poetry publish --build -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} 27 | release-docs: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | with: 32 | persist-credentials: false 33 | - name: Install poetry 34 | run: pip install poetry 35 | - name: Set up Python 3.12 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: "3.12" 39 | cache: poetry 40 | cache-dependency-path: pyproject.toml 41 | - name: Install dependencies 42 | run: | 43 | poetry install 44 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable 45 | source $HOME/.cargo/env 46 | cargo install mdbook 47 | - name: Get PR number 48 | uses: jwalton/gh-find-current-pr@v1 49 | id: findPr 50 | with: 51 | state: all 52 | - name: Set commit message 53 | run: | 54 | if [ -z "$PR" ] 55 | then 56 | echo "COMMIT_MESSAGE=:octocat: Update docs" >> $GITHUB_ENV 57 | else 58 | echo "COMMIT_MESSAGE=:octocat: Update docs for #$PR" >> $GITHUB_ENV 59 | fi 60 | env: 61 | PR: ${{ steps.findPr.outputs.pr }} 62 | - name: Build docs 63 | run: make docs 64 | - name: Deploy on gh-pages 65 | uses: JamesIves/github-pages-deploy-action@v4 66 | with: 67 | folder: out 68 | commit-message: ${{ env.COMMIT_MESSAGE }} 69 | clean: true 70 | git-config-name: github-actions 71 | git-config-email: github-actions@github.com 72 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | 7 | test: 8 | name: Python ${{ matrix.python-version }} 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Install poetry 16 | run: pip install poetry 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | allow-prereleases: true 22 | cache: poetry 23 | cache-dependency-path: pyproject.toml 24 | - name: Install dependencies 25 | run: make setup 26 | - name: Run tests 27 | run: | 28 | make test 29 | - name: Run examples 30 | run: | 31 | make examples 32 | if: matrix.python-version == '3.12' 33 | - name: Run tests (orjson) 34 | run: | 35 | pip install orjson 36 | make test 37 | - name: Run examples (orjson) 38 | run: | 39 | make examples 40 | if: matrix.python-version == '3.12' 41 | - name: Run tests (sqlalchemy) 42 | run: | 43 | poetry install --extras sqlalchemy 44 | make test 45 | - name: Run examples (sqlalchemy) 46 | run: | 47 | make examples 48 | if: matrix.python-version == '3.12' 49 | 50 | check_formatting: 51 | name: Check formatting 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v4 55 | - name: Set up Python 3.12 56 | uses: actions/setup-python@v5 57 | with: 58 | python-version: "3.12" 59 | cache: pip 60 | cache-dependency-path: .pre-commit-config.yaml 61 | - name: Install poetry 62 | run: pip install poetry 63 | - name: Check formatting 64 | run: make setup check 65 | - name: Comment PR 66 | if: ${{ failure() && github.event_name == 'pull_request' }} 67 | uses: thollander/actions-comment-pull-request@v3.0.1 68 | with: 69 | message: 'Please consider formatting your code according to the standards described here: https://github.com/yukinarit/pyserde/blob/main/CONTRIBUTING.md' 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | 72 | check_coverage: 73 | name: Check coverage 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v4 77 | - name: Install poetry 78 | run: pip install poetry 79 | - name: Set up Python 3.12 80 | uses: actions/setup-python@v5 81 | with: 82 | python-version: "3.12" 83 | cache: poetry 84 | cache-dependency-path: pyproject.toml 85 | - name: Install dependencies 86 | run: poetry install --extras sqlalchemy 87 | - name: Check coverage 88 | run: make coverage 89 | - name: Upload coverage report to codecov.io 90 | uses: codecov/codecov-action@v4 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | _build 3 | dist 4 | __pycache__ 5 | .mypy_cache 6 | .pytest_cache 7 | .eggs 8 | *.egg-info 9 | *.pyc 10 | htmlcov 11 | Pipfile.lock 12 | coverage.xml 13 | .coverage 14 | html/ 15 | pip-wheel-metadata 16 | .DS_Store 17 | venv/ 18 | .venv/ 19 | .idea/ 20 | out/ 21 | poetry.lock 22 | **/book 23 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: '.git|.tox' 2 | files: 'serde/|tests/|examples/|bench/bench.py' 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | 10 | - repo: https://github.com/psf/black 11 | rev: 24.3.0 12 | hooks: 13 | - id: black 14 | args: 15 | - . 16 | - repo: https://github.com/RobertCraigie/pyright-python 17 | rev: v1.1.356 18 | hooks: 19 | - id: pyright 20 | additional_dependencies: ['pyyaml', 'msgpack', 'msgpack-types'] 21 | - repo: https://github.com/astral-sh/ruff-pre-commit 22 | rev: v0.3.4 23 | hooks: 24 | - id: ruff 25 | args: 26 | - --fix 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to pyserde 2 | 3 | Thank you for considering contributing to pyserde! 4 | 5 | ## Reporting issues 6 | - Describe what you expected to happen. 7 | - If possible, include a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) to help us identify the issue. This also helps check that the issue is not with your own code. 8 | - Describe what actually happened. Include the full traceback if there was an exception. 9 | - List your Python and pyserde versions. If possible, check if this issue is already fixed in the repository. 10 | 11 | ## Submitting patches 12 | - pyserde uses [black](https://github.com/psf/black) to autoformat your code. This should be done for you as a git pre-commit hook, which gets installed when you run `make setup` but you can do it manually via `make fmt` or `make check`. 13 | - Include tests if your patch is supposed to solve a bug, and explain clearly under which circumstances the bug happens. Make sure the test fails without your patch. 14 | - Include a string like “Fixes #123” in your commit message (where 123 is the issue you fixed). See [Closing issues using keywords](https://help.github.com/articles/creating-a-pull-request/). 15 | 16 | ### First time setup 17 | - Download and install the latest version of [git](https://git-scm.com/downloads). 18 | - Configure git with your [username](https://help.github.com/articles/setting-your-username-in-git/) and [email](https://help.github.com/articles/setting-your-email-in-git/): 19 | ```bash 20 | git config --global user.name 'your name' 21 | git config --global user.email 'your email' 22 | ``` 23 | - Make sure you have a [GitHub account](https://github.com/join). 24 | - Fork pyserde to your GitHub account by clicking the [Fork](https://github.com/yukinarit/pyserde/fork) button. 25 | - [Clone](https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork) your GitHub fork locally: 26 | ```bash 27 | git clone https://github.com/{your-github-username}/pyserde 28 | cd pyserde 29 | ``` 30 | - Add the main repository as a remote to update later: 31 | ```bash 32 | git remote add upstream https://github.com/yukinarit/pyserde 33 | git fetch upstream 34 | ``` 35 | - Install Poetry (used for dependency management and packaging): 36 | 37 | For macOS / Linux / WSL 38 | ```bash 39 | curl -sSL https://install.python-poetry.org/ | python - 40 | ``` 41 | For Windows Powershell 42 | ```bash 43 | (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/main/get-poetry.py -UseBasicParsing).Content | python - 44 | ``` 45 | - Run setup script: 46 | ```bash 47 | make setup 48 | ``` 49 | 50 | ### Start coding 51 | - Create a branch to identify the issue you would like to work on: 52 | ```bash 53 | git checkout -b your-branch-name origin/main 54 | ``` 55 | - Using your favorite editor, make your changes, committing as you go. 56 | - Include tests that cover any code changes you make. Make sure the test fails without your patch. Run the tests via `make test`. 57 | - Push your commits to GitHub and [create a pull request](https://help.github.com/en/articles/creating-a-pull-request) by using: 58 | ```bash 59 | git push --set-upstream origin your-branch-name 60 | ``` 61 | 62 | ### Linters & Type checkers 63 | pyserde uses the following tools. mypy and pyright are enabled for `/examples` directory at the moment. 64 | * black 65 | * pyright 66 | * ruff 67 | * mypy 68 | 69 | If pre-commit is configured, `black`, `pyright` and `ruff` are automatically triggered when making a git commit. If you want to run mypy, run 70 | ``` 71 | make check # this also runs black, pyright and ruff 72 | ``` 73 | or 74 | ``` 75 | mypy . 76 | ``` 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 yukinarit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL:=/bin/bash 2 | PYTHON ?= python 3 | POETRY ?= poetry 4 | 5 | .PHONY: all setup build test examples coverage fmt check docs open-docs bench 6 | 7 | all: setup pep8 mypy docs test examples 8 | 9 | setup: 10 | $(POETRY) install 11 | $(POETRY) run pip list 12 | $(POETRY) run pre-commit install 13 | 14 | setup-bench: 15 | pushd bench && $(POETRY) install && popd 16 | 17 | build: 18 | $(POETRY) build 19 | 20 | test: 21 | $(POETRY) run pytest tests --doctest-modules serde -v -nauto 22 | 23 | examples: 24 | pushd examples && $(POETRY) run $(PYTHON) runner.py && popd 25 | 26 | coverage: 27 | $(POETRY) run pytest tests --doctest-modules serde -v -nauto --cov=serde --cov-report term --cov-report xml --cov-report html 28 | 29 | fmt: 30 | $(POETRY) run pre-commit run black -a 31 | 32 | check: 33 | $(POETRY) run pre-commit run -a 34 | $(POETRY) run mypy . 35 | 36 | docs: 37 | mkdir -p docs out/api out/guide/en 38 | $(POETRY) run pdoc -e serde=https://github.com/yukinarit/pyserde/tree/main/serde/ serde -o out/api 39 | mdbook build -d ../../out/guide/en ./docs/en 40 | mdbook build -d ../../out/guide/ja ./docs/ja 41 | 42 | open-docs: 43 | $(POETRY) run pdoc -e serde=https://github.com/yukinarit/pyserde/tree/main/serde serde 44 | 45 | bench: 46 | pushd bench && $(POETRY) run $(PYTHON) bench.py && popd 47 | -------------------------------------------------------------------------------- /bench/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/bench/__init__.py -------------------------------------------------------------------------------- /bench/attrs_class.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import partial 3 | from typing import Union 4 | 5 | import attr 6 | import data 7 | from runner import Runner, Size 8 | 9 | 10 | @attr.s(auto_attribs=True) 11 | class Small: 12 | i: int 13 | s: str 14 | f: float 15 | b: bool 16 | 17 | 18 | @attr.s(auto_attribs=True) 19 | class Medium: 20 | inner: list[Small] = attr.Factory(list) 21 | 22 | 23 | SMALL = Small(**data.args_sm) 24 | 25 | MEDIUM = Medium([Small(**d) for d in data.args_md]) 26 | 27 | 28 | def new(size: Size) -> Runner: 29 | name = "attrs" 30 | if size == Size.Small: 31 | unp = SMALL 32 | elif size == Size.Medium: 33 | unp = MEDIUM 34 | return Runner(name, unp, partial(se, unp), None, partial(astuple, unp), partial(asdict, unp)) 35 | 36 | 37 | def se(obj: Union[Small, Medium]): 38 | return json.dumps(attr.asdict(obj)) 39 | 40 | 41 | def astuple(obj: Union[Small, Medium]): 42 | return attr.astuple(obj) 43 | 44 | 45 | def asdict(obj: Union[Small, Medium]): 46 | return attr.asdict(obj) 47 | -------------------------------------------------------------------------------- /bench/cattrs_class.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import partial 3 | from typing import Type, Union 4 | 5 | import cattr 6 | import data 7 | from attrs_class import MEDIUM, SMALL, Medium, Small 8 | from runner import Runner, Size 9 | 10 | 11 | def new(size: Size) -> Runner: 12 | name = "cattrs" 13 | if size == Size.Small: 14 | unp = SMALL 15 | pac = data.SMALL 16 | cls = Small 17 | elif size == Size.Medium: 18 | unp = MEDIUM 19 | pac = data.MEDIUM 20 | cls = Medium 21 | return Runner(name, unp, partial(se, unp), partial(de, cls, pac), None, partial(asdict, unp)) 22 | 23 | 24 | def se(obj: Union[Small, Medium]): 25 | return json.dumps(cattr.unstructure(obj)) 26 | 27 | 28 | def de(cls: Type, data: str): 29 | return cattr.structure(json.loads(data), cls) 30 | 31 | 32 | def asdict(obj: Union[Small, Medium]): 33 | return cattr.unstructure(obj) 34 | -------------------------------------------------------------------------------- /bench/charts/asdict-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/bench/charts/asdict-medium.png -------------------------------------------------------------------------------- /bench/charts/asdict-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/bench/charts/asdict-small.png -------------------------------------------------------------------------------- /bench/charts/astuple-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/bench/charts/astuple-medium.png -------------------------------------------------------------------------------- /bench/charts/astuple-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/bench/charts/astuple-small.png -------------------------------------------------------------------------------- /bench/charts/de-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/bench/charts/de-medium.png -------------------------------------------------------------------------------- /bench/charts/de-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/bench/charts/de-small.png -------------------------------------------------------------------------------- /bench/charts/se-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/bench/charts/se-medium.png -------------------------------------------------------------------------------- /bench/charts/se-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/bench/charts/se-small.png -------------------------------------------------------------------------------- /bench/dacite_class.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import partial 3 | from typing import Type 4 | 5 | import dacite 6 | import data 7 | from dataclasses_class import MEDIUM, SMALL, Medium, Small 8 | from runner import Runner, Size 9 | 10 | 11 | def de(cls: Type, data: str): 12 | return dacite.from_dict(data_class=cls, data=json.loads(data)) 13 | 14 | 15 | def new(size: Size) -> Runner: 16 | name = "attrs" 17 | if size == Size.Small: 18 | unp = SMALL 19 | pac = data.SMALL 20 | cls = Small 21 | elif size == Size.Medium: 22 | unp = MEDIUM 23 | pac = data.MEDIUM 24 | cls = Medium 25 | return Runner(name, unp, None, partial(de, cls, pac), None, None) 26 | -------------------------------------------------------------------------------- /bench/data.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | SMALL = '{"i": 10, "s": "foo", "f": 100.0, "b": true}' 4 | 5 | SMALL_TUPLE = (10, "foo", 100.0, True) 6 | 7 | SMALL_DICT = json.loads(SMALL) 8 | 9 | MEDIUM = f'{{"inner": [{",".join([SMALL] * 50)}]}}' 10 | 11 | MEDIUM_TUPLE = tuple([[SMALL_TUPLE] * 50]) 12 | 13 | json_md = ( 14 | '{"i": 10, "s": "foo", "f": 100.0, "b": true,' 15 | '"i2": 10, "s2": "foo", "f2": 100.0, "b2": true,' 16 | '"i3": 10, "s3": "foo", "f3": 100.0, "b3": true,' 17 | '"i4": 10, "s4": "foo", "f4": 100.0, "b4": true,' 18 | '"i5": 10, "s5": "foo", "f5": 100.0, "b5": true}' 19 | ) 20 | 21 | json_pri_container = ( 22 | '{"v": [1, 2, 3, 4, 5], "d": {"foo": 10, "fuga": 20}, "foo": 30, "t": [true, false, true]}' 23 | ) 24 | 25 | args_sm = {"i": 10, "s": "foo", "f": 100.0, "b": True} 26 | 27 | args_md = [args_sm] * 50 28 | -------------------------------------------------------------------------------- /bench/dataclasses_class.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import json 3 | from dataclasses import dataclass, field 4 | from functools import partial 5 | from typing import List, Union 6 | 7 | import data 8 | from runner import Runner, Size 9 | 10 | 11 | @dataclass 12 | class Small: 13 | i: int 14 | s: str 15 | f: float 16 | b: bool 17 | 18 | 19 | @dataclass 20 | class Medium: 21 | inner: List[Small] = field(default_factory=list) 22 | 23 | 24 | SMALL = Small(**data.args_sm) 25 | 26 | MEDIUM = Medium([Small(**d) for d in data.args_md]) 27 | 28 | 29 | def new(size: Size) -> Runner: 30 | name = "dataclass" 31 | if size == Size.Small: 32 | unp = SMALL 33 | elif size == Size.Medium: 34 | unp = MEDIUM 35 | return Runner(name, unp, partial(se, unp), None, partial(astuple, unp), partial(asdict, unp)) 36 | 37 | 38 | def se(obj: Union[Small, Medium]): 39 | return json.dumps(asdict(obj)) 40 | 41 | 42 | def astuple(d): 43 | return dataclasses.astuple(d) 44 | 45 | 46 | def asdict(d): 47 | return dataclasses.asdict(d) 48 | -------------------------------------------------------------------------------- /bench/marshmallow_class.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import partial 3 | from typing import Union 4 | 5 | import data 6 | import marshmallow as ms 7 | from dataclasses_class import MEDIUM, SMALL, Medium, Small 8 | from runner import Runner, Size 9 | 10 | 11 | class SmallSchema(ms.Schema): 12 | class Meta: 13 | render_module = json 14 | 15 | i = ms.fields.Int() 16 | s = ms.fields.Str() 17 | f = ms.fields.Float() 18 | b = ms.fields.Boolean() 19 | 20 | @ms.post_load 21 | def make_small(self, data, **kwargs): 22 | return Small(**data) 23 | 24 | 25 | class MediumSchema(ms.Schema): 26 | class Meta: 27 | render_module = json 28 | 29 | inner = ms.fields.List(ms.fields.Nested(SmallSchema)) 30 | 31 | @ms.post_load 32 | def make_medium(self, data, **kwargs): 33 | return Medium([s for s in data["inner"]]) 34 | 35 | 36 | def new(size: Size) -> Runner: 37 | name = "marshmallow" 38 | if size == Size.Small: 39 | unp = SMALL 40 | pac = data.SMALL 41 | schema = SmallSchema() 42 | elif size == Size.Medium: 43 | unp = MEDIUM 44 | pac = data.MEDIUM 45 | schema = MediumSchema() 46 | return Runner( 47 | name, 48 | unp, 49 | partial(se, schema, unp), 50 | partial(de, schema, pac), 51 | None, 52 | partial(asdict, schema, unp), 53 | ) 54 | 55 | 56 | def se(schema: ms.Schema, obj: Union[Small, Medium]): 57 | return schema.dumps(obj) 58 | 59 | 60 | def de(schema: ms.Schema, data: str): 61 | return schema.loads(data) 62 | 63 | 64 | def asdict(schema: ms.Schema, data: str): 65 | return schema.dump(data) 66 | -------------------------------------------------------------------------------- /bench/mashumaro_class.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from functools import partial 3 | from typing import List, Type, Union 4 | 5 | import data 6 | import mashumaro 7 | from runner import Runner, Size 8 | 9 | 10 | @dataclass 11 | class Small(mashumaro.DataClassJSONMixin): 12 | i: int 13 | s: str 14 | f: float 15 | b: bool 16 | 17 | 18 | @dataclass 19 | class Medium(mashumaro.DataClassJSONMixin): 20 | inner: List[Small] = field(default_factory=list) 21 | 22 | 23 | SMALL = Small(**data.args_sm) 24 | 25 | MEDIUM = Medium([Small(**d) for d in data.args_md]) 26 | 27 | 28 | def new(size: Size) -> Runner: 29 | name = "mashmaro" 30 | if size == Size.Small: 31 | unp = SMALL 32 | pac = data.SMALL 33 | cls = Small 34 | elif size == Size.Medium: 35 | unp = MEDIUM 36 | pac = data.MEDIUM 37 | cls = Medium 38 | return Runner(name, unp, partial(se, unp), partial(de, cls, pac), None, None) 39 | 40 | 41 | def se(obj: Union[Small, Medium]): 42 | return obj.to_json() 43 | 44 | 45 | def de(cls: Type, data: str): 46 | return cls.from_json(data) 47 | -------------------------------------------------------------------------------- /bench/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pyserde_bench" 3 | version = "0.1.0" 4 | description = "Benchmark package for pyserde" 5 | authors = ["yukinarit "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.7.0" 9 | click = "*" 10 | pyserde = {path = "../"} 11 | 12 | [tool.poetry.dev-dependencies] 13 | # dacite = "*" 14 | # mashumaro = "^2.11.0" 15 | # marshmallow = "~=3.2" 16 | # attrs = "*" 17 | # cattrs = "*" 18 | # seaborn = "*" 19 | # matplotlib = "*" 20 | # numpy = "*" 21 | -------------------------------------------------------------------------------- /bench/pyserde_class.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from functools import partial 3 | from typing import List, Type, Union 4 | 5 | import data 6 | from runner import Runner, Size 7 | 8 | import serde 9 | import serde.json 10 | 11 | 12 | @serde.serde 13 | @dataclass(unsafe_hash=True) 14 | class Small: 15 | i: int 16 | s: str 17 | f: float 18 | b: bool 19 | 20 | 21 | @serde.serde 22 | @dataclass 23 | class Medium: 24 | inner: List[Small] = field(default_factory=list) 25 | 26 | 27 | SMALL = Small(**data.args_sm) 28 | 29 | MEDIUM = Medium([Small(**d) for d in data.args_md]) 30 | 31 | 32 | def new(size: Size) -> Runner: 33 | name = "pyserde" 34 | if size == Size.Small: 35 | unp = SMALL 36 | pac = data.SMALL 37 | cls = Small 38 | elif size == Size.Medium: 39 | unp = MEDIUM 40 | pac = data.MEDIUM 41 | cls = Medium 42 | return Runner( 43 | name, 44 | unp, 45 | partial(se, unp), 46 | partial(de, cls, pac), 47 | partial(astuple, unp), 48 | partial(asdict, unp), 49 | ) 50 | 51 | 52 | def se(obj: Union[Small, Medium]): 53 | return serde.json.to_json(obj) 54 | 55 | 56 | def de(cls: Type, data: str): 57 | return serde.json.from_json(cls, data) 58 | 59 | 60 | def astuple(data): 61 | return serde.to_tuple(data) 62 | 63 | 64 | def asdict(data): 65 | return serde.to_dict(data) 66 | -------------------------------------------------------------------------------- /bench/raw.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import partial 3 | from typing import List, Tuple 4 | 5 | import data 6 | from dataclasses_class import MEDIUM, SMALL, Medium, Small 7 | from runner import Runner, Size 8 | 9 | 10 | def new(size: Size) -> Runner: 11 | name = "raw" 12 | if size == Size.Small: 13 | unp = SMALL 14 | pac = data.SMALL 15 | se = se_small 16 | de = de_small 17 | astuple = astuple_small 18 | asdict = asdict_small 19 | elif size == Size.Medium: 20 | unp = MEDIUM 21 | pac = data.MEDIUM 22 | se = se_medium 23 | de = de_medium 24 | astuple = astuple_medium 25 | asdict = asdict_medium 26 | return Runner( 27 | name, unp, partial(se, unp), partial(de, pac), partial(astuple, unp), partial(asdict, unp) 28 | ) 29 | 30 | 31 | def se_small(s: Small): 32 | return json.dumps(asdict_small(s)) 33 | 34 | 35 | def se_medium(m: Medium): 36 | return json.dumps({"inner": [{"i": s.i, "s": s.s, "f": s.f, "b": s.b} for s in m.inner]}) 37 | 38 | 39 | def de_small(data: str) -> Small: 40 | return _de_small(json.loads(data)) 41 | 42 | 43 | def _de_small(dct) -> Small: 44 | return Small(dct["i"], dct["s"], dct["f"], dct["b"]) 45 | 46 | 47 | def de_medium(data: str) -> Medium: 48 | lst = json.loads(data) 49 | return Medium([_de_small(v) for v in lst["inner"]]) 50 | 51 | 52 | def astuple_small(sm: Small) -> Tuple[int, str, float, bool]: 53 | return (sm.i, sm.s, sm.f, sm.b) 54 | 55 | 56 | def astuple_medium(md: Medium) -> Tuple[List[Tuple[int, str, float, bool]]]: 57 | return ([astuple_small(sm) for sm in md.inner],) 58 | 59 | 60 | def asdict_small(s: Small): 61 | return {"i": s.i, "s": s.s, "f": s.f, "b": s.b} 62 | 63 | 64 | def asdict_medium(m: Medium): 65 | return {"inner": [asdict_small(s) for s in m.inner]} 66 | -------------------------------------------------------------------------------- /bench/runner.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from dataclasses import dataclass 3 | from typing import Any, Callable 4 | 5 | 6 | class Size(enum.Enum): 7 | Small = "Small" 8 | Medium = "Medium" 9 | Large = "Large" 10 | 11 | 12 | @dataclass 13 | class Runner: 14 | name: Size 15 | data: Any 16 | se: Callable 17 | de: Callable 18 | astuple: Callable 19 | asdict: Callable 20 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | # see https://docs.pytest.org/en/latest/example/pythoncollection.html#customizing-test-collection 2 | import sys 3 | 4 | collect_ignore = ["setup.py"] 5 | 6 | if sys.version_info[:2] < (3, 12): 7 | collect_ignore.append("tests/test_type_alias.py") 8 | -------------------------------------------------------------------------------- /docs/en/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](introduction.md) 4 | - [Getting Started](getting-started.md) 5 | - [Data Formats](data-formats.md) 6 | - [Types](types.md) 7 | - [Decorators](decorators.md) 8 | - [Class Attributes](class-attributes.md) 9 | - [Field Attributes](field-attributes.md) 10 | - [Union](union.md) 11 | - [Type Checking](type-check.md) 12 | - [Extension](extension.md) 13 | - [FAQ](faq.md) 14 | -------------------------------------------------------------------------------- /docs/en/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["yukinarit"] 3 | language = "en" 4 | multilingual = true 5 | src = "./" 6 | title = "pyserde documentation" 7 | 8 | [output.html] 9 | git-repository-url = "https://github.com/yukinarit/pyserde" 10 | edit-url-template = "https://github.com/yukinarit/pyserde/edit/main/docs/en/{path}" 11 | -------------------------------------------------------------------------------- /docs/en/data-formats.md: -------------------------------------------------------------------------------- 1 | # Data Formats 2 | 3 | pyserde supports several data formats for serialization and deserialization, including `dict`, `tuple`, `JSON`, `YAML`, `TOML`, `MsgPack`, and `Pickle`. Each API can take additional keyword arguments, which are forwarded to the underlying packages used by pyserde. 4 | 5 | e.g. If you want to preserve the field order in YAML, you can pass `sort_key=True` in `serde.yaml.to_yaml` 6 | 7 | ```python 8 | serde.yaml.to_yaml(foo, sort_key=True) 9 | ``` 10 | 11 | `sort_key=True` will be passed in the [yaml.safedump](https://github.com/yukinarit/pyserde/blob/a9f44d52d109144a4c3bb93965f831e91d13960b/serde/yaml.py#L18) 12 | 13 | ## dict 14 | 15 | ```python 16 | >>> from serde import to_dict, from_dict 17 | 18 | >>> to_dict(Foo(i=10, s='foo', f=100.0, b=True)) 19 | {"i": 10, "s": "foo", "f": 100.0, "b": True} 20 | 21 | >>> from_dict(Foo, {"i": 10, "s": "foo", "f": 100.0, "b": True}) 22 | Foo(i=10, s='foo', f=100.0, b=True) 23 | ``` 24 | 25 | See [serde.to_dict](https://yukinarit.github.io/pyserde/api/serde/se.html#to_dict) / [serde.from_dict](https://yukinarit.github.io/pyserde/api/serde/de.html#from_dict) for more information. 26 | 27 | ## tuple 28 | 29 | ```python 30 | >>> from serde import to_tuple, from_tuple 31 | 32 | >>> to_tuple(Foo(i=10, s='foo', f=100.0, b=True)) 33 | (10, 'foo', 100.0, True) 34 | 35 | >>> from_tuple(Foo, (10, 'foo', 100.0, True)) 36 | Foo(i=10, s='foo', f=100.0, b=True) 37 | ``` 38 | 39 | See [serde.to_tuple](https://yukinarit.github.io/pyserde/api/serde/se.html#to_tuple) / [serde.from_tuple](https://yukinarit.github.io/pyserde/api/serde/de.html#from_tuple) for more information. 40 | 41 | ## JSON 42 | 43 | ```python 44 | >>> from serde.json import to_json, from_json 45 | 46 | >>> to_json(Foo(i=10, s='foo', f=100.0, b=True)) 47 | '{"i":10,"s":"foo","f":100.0,"b":true}' 48 | 49 | >>> from_json(Foo, '{"i": 10, "s": "foo", "f": 100.0, "b": true}') 50 | Foo(i=10, s='foo', f=100.0, b=True) 51 | ``` 52 | 53 | See [serde.json.to_json](https://yukinarit.github.io/pyserde/api/serde/json.html#to_json) / [serde.json.from_json](https://yukinarit.github.io/pyserde/api/serde/json.html#from_json) for more information. 54 | 55 | ## Yaml 56 | 57 | ```python 58 | >>> from serde.yaml import from_yaml, to_yaml 59 | 60 | >>> to_yaml(Foo(i=10, s='foo', f=100.0, b=True)) 61 | b: true 62 | f: 100.0 63 | i: 10 64 | s: foo 65 | 66 | >>> from_yaml(Foo, 'b: true\nf: 100.0\ni: 10\ns: foo') 67 | Foo(i=10, s='foo', f=100.0, b=True) 68 | ``` 69 | 70 | See [serde.yaml.to_yaml](https://yukinarit.github.io/pyserde/api/serde/yaml.html#to_yaml) / [serde.yaml.from_yaml](https://yukinarit.github.io/pyserde/api/serde/yaml.html#from_yaml) for more information. 71 | 72 | ## Toml 73 | 74 | ```python 75 | >>> from serde.toml import from_toml, to_toml 76 | 77 | >>> to_toml(Foo(i=10, s='foo', f=100.0, b=True)) 78 | i = 10 79 | s = "foo" 80 | f = 100.0 81 | b = true 82 | 83 | >>> from_toml(Foo, 'i = 10\ns = "foo"\nf = 100.0\nb = true') 84 | Foo(i=10, s='foo', f=100.0, b=True) 85 | ``` 86 | 87 | See [serde.toml.to_toml](https://yukinarit.github.io/pyserde/api/serde/toml.html#to_toml) / [serde.toml.from_toml](https://yukinarit.github.io/pyserde/api/serde/toml.html#from_toml) for more information. 88 | 89 | ## MsgPack 90 | 91 | ```python 92 | 93 | >>> from serde.msgpack import from_msgpack, to_msgpack 94 | 95 | >>> to_msgpack(Foo(i=10, s='foo', f=100.0, b=True)) 96 | b'\x84\xa1i\n\xa1s\xa3foo\xa1f\xcb@Y\x00\x00\x00\x00\x00\x00\xa1b\xc3' 97 | 98 | >>> from_msgpack(Foo, b'\x84\xa1i\n\xa1s\xa3foo\xa1f\xcb@Y\x00\x00\x00\x00\x00\x00\xa1b\xc3') 99 | Foo(i=10, s='foo', f=100.0, b=True) 100 | ``` 101 | 102 | See [serde.msgpack.to_msgpack](https://yukinarit.github.io/pyserde/api/serde/msgpack.html#to_msgpack) / [serde.msgpack.from_msgpack](https://yukinarit.github.io/pyserde/api/serde/msgpack.html#from_msgpack) for more information. 103 | 104 | ## Pickle 105 | 106 | New in v0.9.6 107 | 108 | ```python 109 | >>> from serde.pickle import from_pickle, to_pickle 110 | 111 | >>> to_pickle(Foo(i=10, s='foo', f=100.0, b=True)) 112 | b"\x80\x04\x95'\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x01i\x94K\n\x8c\x01s\x94\x8c\x03foo\x94\x8c\x01f\x94G@Y\x00\x00\x00\x00\x00\x00\x8c\x01b\x94\x88u." 113 | 114 | >>> from_pickle(Foo, b"\x80\x04\x95'\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x01i\x94K\n\x8c\x01s\x94\x8c\x03foo\x94\x8c\x01f\x94G@Y\x00\x00\x00\x00\x00\x00\x8c\x01b\x94\x88u.") 115 | Foo(i=10, s='foo', f=100.0, b=True) 116 | ``` 117 | 118 | See [serde.pickle.to_pickle](https://yukinarit.github.io/pyserde/api/serde/pickle.html#to_pickle) / [serde.pickle.from_pickle](https://yukinarit.github.io/pyserde/api/serde/pickle.html#from_pickle) for more information. 119 | 120 | ## Needs a new data format support? 121 | 122 | We don't plan to supprot a data format such as XML to keep pyserde as simple as possible. If you need a new data format support, we recommend to create a separate python package. If the data format is interchangable to dict or tuple, implementing the serialize/desrialize API is not that difficult. See [YAML's implementation](https://github.com/yukinarit/pyserde/blob/main/serde/yaml.py) to know how to implement. 123 | -------------------------------------------------------------------------------- /docs/en/decorators.md: -------------------------------------------------------------------------------- 1 | # Decorators 2 | 3 | ## `@serde` 4 | 5 | `@serde` is a wrapper of `@serialize` and `@deserialize` decorators. 6 | 7 | This code 8 | ```python 9 | @serde 10 | class Foo: 11 | ... 12 | ``` 13 | 14 | is equivalent to the following code. 15 | 16 | ```python 17 | @deserialize 18 | @serialize 19 | @dataclass 20 | class Foo: 21 | ... 22 | ``` 23 | 24 | `@serde` decorator does these for you: 25 | * Add `@serialize` and `@deserialize` decorators to a class 26 | * Add `@dataclass` decorator to a class if a class doesn't have `@dataclass` 27 | * You can pass both (de)serialize attributes to the decorator 28 | * `serializer` attribute is ignored in `@deserialize` and `deserializer` attribute is ignored in `@serialize` 29 | 30 | ```python 31 | @serde(serializer=serializer, deserializer=deserializer) 32 | @dataclass 33 | class Foo: 34 | ... 35 | ``` 36 | 37 | > **NOTE:** `@serde` actually works without @dataclass decorator, because it detects and add @dataclass to the declared class automatically. However, mypy will produce `Too many arguments` or `Unexpected keyword argument` error. This is due to [the mypy limitation](https://mypy.readthedocs.io/en/stable/additional_features.html#caveats-known-issues). 38 | > 39 | > ```python 40 | > @serde 41 | > class Foo: 42 | > ... 43 | > ``` 44 | > 45 | > But if you use a PEP681 compliant type checker (e.g. pyright), you don't get the type error because pyserde supports [PEP681 dataclass_transform](https://peps.python.org/pep-0681/) 46 | 47 | 48 | ## `@serialize`/`@deserialize` 49 | 50 | `@serialize` and `@deserialize` are used by `@serde` under the hood. We recommend to use those two decorators in the following cases. Otherwise `@serde` is recommended. 51 | * You only need either serialization or deserialization functionality 52 | * You want to have different class attributes as below 53 | 54 | ```python 55 | @deserialize(rename_all = "snakecase") 56 | @serialize(rename_all = "camelcase") 57 | class Foo: 58 | ... 59 | ``` 60 | 61 | ## (de)serializing class without `@serde` 62 | 63 | pyserde can (de)serialize dataclasses without `@serde` since v0.10.0. This feature is convenient when you want to use classes declared in external libraries or a type checker doesn't work with `@serde` decorator. See [this example](https://github.com/yukinarit/pyserde/blob/main/examples/plain_dataclass.py). 64 | 65 | How does it work? It's quite simple that pyserde add `@serde` decorator if a class doesn't has it. It may take for a while for the first API call, but the generated code will be cached internally. So it won't be a problem. See the following example to deserialize a class in a third party package. 66 | 67 | ```python 68 | @dataclass 69 | class External: 70 | ... 71 | 72 | to_dict(External()) # works without @serde 73 | ``` 74 | 75 | Note that you can't specify class attributes e.g. `rename_all` in this case. If you want to add a class attribute to an external dataclass, there is a technique to do that by extending dataclass. See [this example](https://github.com/yukinarit/pyserde/blob/main/examples/plain_dataclass_class_attribute.py). 76 | 77 | ```python 78 | @dataclass 79 | class External: 80 | some_value: int 81 | 82 | @serde(rename_all="kebabcase") 83 | @dataclass 84 | class Wrapper(External): 85 | pass 86 | ``` 87 | 88 | ## How can I use forward references? 89 | 90 | pyserde supports forward references. If you replace a nested class name with with string, pyserde looks up and evaluate the decorator after nested class is defined. 91 | 92 | ```python 93 | from __future__ import annotations # make sure to import annotations 94 | 95 | @dataclass 96 | class Foo: 97 | i: int 98 | s: str 99 | bar: Bar # Bar can be specified although it's declared afterward. 100 | 101 | @serde 102 | @dataclass 103 | class Bar: 104 | f: float 105 | b: bool 106 | 107 | # Evaluate pyserde decorators after `Bar` is defined. 108 | serde(Foo) 109 | ``` 110 | 111 | ## PEP563 Postponed Evaluation of Annotations 112 | 113 | pyserde supports [PEP563 Postponed evaluation of annotation](https://peps.python.org/pep-0563/). 114 | 115 | ```python 116 | from __future__ import annotations 117 | from serde import serde 118 | 119 | @serde 120 | class Foo: 121 | i: int 122 | s: str 123 | f: float 124 | b: bool 125 | 126 | def foo(self, cls: Foo): # You can use "Foo" type before it's defined. 127 | print('foo') 128 | ``` 129 | 130 | See [examples/lazy_type_evaluation.py](https://github.com/yukinarit/pyserde/blob/main/examples/lazy_type_evaluation.py) for complete example. 131 | -------------------------------------------------------------------------------- /docs/en/extension.md: -------------------------------------------------------------------------------- 1 | # Extending pyserde 2 | 3 | pyserde offers three ways to extend pyserde to support non builtin types. 4 | 5 | ## Custom field (de)serializer 6 | 7 | See [custom field serializer](./field-attributes.md#serializerdeserializer). 8 | 9 | > 💡 Tip: wrapping `serde.field` with your own field function makes 10 | > 11 | > ```python 12 | > import serde 13 | > 14 | > def field(*args, **kwargs): 15 | > serde.field(*args, **kwargs, serializer=str) 16 | > 17 | > @serde 18 | > class Foo: 19 | > a: int = field(default=0) # Configuring field serializer 20 | > ``` 21 | 22 | ## Custom class (de)serializer 23 | 24 | See [custom class serializer](./class-attributes.md#class_serializer--class_deserializer). 25 | 26 | ## Custom global (de)serializer 27 | 28 | You apply the custom (de)serialization for entire codebase by registering class (de)serializer by `add_serializer` and `add_deserializer`. Registered class (de)serializers are stacked in pyserde's global space and automatically used for all the pyserde classes. 29 | 30 | e.g. Implementing custom (de)serialization for `datetime.timedelta` using [isodate](https://pypi.org/project/isodate/) package. 31 | 32 | Here is the code of registering class (de)serializer for `datetime.timedelta`. This package is actually published in PyPI as [pyserde-timedelta](https://pypi.org/project/pyserde-timedelta/). 33 | 34 | ```python 35 | from datetime import timedelta 36 | from plum import dispatch 37 | from typing import Type, Any 38 | import isodate 39 | import serde 40 | 41 | class Serializer: 42 | @dispatch 43 | def serialize(self, value: timedelta) -> Any: 44 | return isodate.duration_isoformat(value) 45 | 46 | class Deserializer: 47 | @dispatch 48 | def deserialize(self, cls: Type[timedelta], value: Any) -> timedelta: 49 | return isodate.parse_duration(value) 50 | 51 | def init() -> None: 52 | serde.add_serializer(Serializer()) 53 | serde.add_deserializer(Deserializer()) 54 | ``` 55 | 56 | Users of this package can reuse custom (de)serialization functionality for `datetime.timedelta` just by calling `serde_timedelta.init()`. 57 | 58 | ```python 59 | import serde_timedelta 60 | from serde import serde 61 | from serde.json import to_json, from_json 62 | from datetime import timedelta 63 | 64 | serde_timedelta.init() 65 | 66 | @serde 67 | class Foo: 68 | a: timedelta 69 | 70 | f = Foo(timedelta(hours=10)) 71 | json = to_json(f) 72 | print(json) 73 | print(from_json(Foo, json)) 74 | ``` 75 | and you get `datetime.timedelta` to be serialized in ISO 8601 duration format! 76 | ```bash 77 | {"a":"PT10H"} 78 | Foo(a=datetime.timedelta(seconds=36000)) 79 | ``` 80 | 81 | > 💡 Tip: You can register as many class (de)serializer as you want. This means you can use as many pyserde extensions as you want. 82 | > Registered (de)serializers are stacked in the memory. A (de)serializer can be overridden by another (de)serializer. 83 | > 84 | > e.g. If you register 3 custom serializers in this order, the first serializer will completely overridden by the 3rd one. 2nd one works because it is implemented for a different type. 85 | > 1. Register Serializer for `int` 86 | > 2. Register Serializer for `float` 87 | > 3. Register Serializer for `int` 88 | 89 | New in v0.13.0. 90 | -------------------------------------------------------------------------------- /docs/en/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## How can I see the code generated by pyserde? 4 | 5 | pyserde provides `inspect` submodule that works as commandline: 6 | ``` 7 | python -m serde.inspect 8 | ``` 9 | 10 | e.g. in pyserde project 11 | 12 | ``` 13 | cd pyserde 14 | poetry shell 15 | python -m serde.inspect examples/simple.py Foo 16 | ``` 17 | 18 | Output 19 | ```python 20 | Loading simple.Foo from examples. 21 | 22 | ================================================== 23 | Foo 24 | ================================================== 25 | 26 | -------------------------------------------------- 27 | Functions generated by pyserde 28 | -------------------------------------------------- 29 | def to_iter(obj, reuse_instances=True, convert_sets=False): 30 | if reuse_instances is Ellipsis: 31 | reuse_instances = True 32 | if convert_sets is Ellipsis: 33 | convert_sets = False 34 | if not is_dataclass(obj): 35 | return copy.deepcopy(obj) 36 | 37 | Foo = serde_scope.types["Foo"] 38 | res = [] 39 | res.append(obj.i) 40 | res.append(obj.s) 41 | res.append(obj.f) 42 | res.append(obj.b) 43 | return tuple(res) 44 | ... 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/en/field-attributes.md: -------------------------------------------------------------------------------- 1 | # Field Attributes 2 | 3 | Field attributes are options to customize (de)serialization behaviour for a field of a dataclass. 4 | 5 | ## Attributes offered by dataclasses 6 | 7 | ### **`default`** / **`default_factory`** 8 | 9 | `default` and `default_factory` work as expected. If a field has `default` or `default_factory` attribute, it behaves like an optional field. If the field is found in the data, the value is fetched from the data and set in the deserialized object. If the field is not found in the data, the specified default value is set in the deserialized object. 10 | 11 | ```python 12 | class Foo: 13 | a: int = 10 14 | b: int = field(default=10) # same as field "a" 15 | c: dict[str, int] = field(default_factory=dict) 16 | 17 | print(from_dict(Foo, {})) # prints Foo(a=10, b=10, c={}) 18 | ``` 19 | 20 | See [examples/default.py](https://github.com/yukinarit/pyserde/blob/main/examples/default.py) for the complete example. 21 | 22 | ### **`ClassVar`** 23 | 24 | `dataclasses.ClassVar` is a class variable for the dataclasses. Since dataclass treats ClassVar as pseudo-field and `dataclasses.field` doesn't pick ClassVar, pyserde doesn't (de)serialize ClassVar fields as a default behaviour. If you want to serialize ClassVar fields, consider using [serialize_class_var](class-attributes.md#serialize_class_var) class attribute. 25 | 26 | See [examples/class_var.py](https://github.com/yukinarit/pyserde/blob/main/examples/class_var.py) for the complete example. 27 | 28 | ## Attributes offered by pyserde 29 | 30 | Field attributes can be specified through `serde.field` or `dataclasses.field`. We recommend to use `serde.field` because it's shorter and type check works. 31 | Here is an example specifying `rename` attribute in both `serde.field` and `dataclasses.field`. 32 | 33 | ```python 34 | @serde.serde 35 | class Foo: 36 | a: str = serde.field(rename="A") 37 | b: str = dataclasses.field(metadata={"serde_rename"="B"}) 38 | ``` 39 | 40 | ### **`rename`** 41 | 42 | `rename` is used to rename field name during (de)serialization. This attribute is convenient when you want to use a python keyword in field name. For example, this code renames field name `id` to `ID`. 43 | 44 | ```python 45 | @serde 46 | class Foo: 47 | id: int = field(rename="ID") 48 | ``` 49 | 50 | See [examples/rename.py](https://github.com/yukinarit/pyserde/blob/main/examples/rename.py) for the complete example. 51 | 52 | ### **`skip`** 53 | 54 | `skip` is used to skip (de)serialization of the field with this attribute. 55 | 56 | ```python 57 | @serde 58 | class Resource: 59 | name: str 60 | hash: str 61 | metadata: dict[str, str] = field(default_factory=dict, skip=True) 62 | ``` 63 | 64 | See [examples/skip.py](https://github.com/yukinarit/pyserde/blob/main/examples/skip.py) for the complete example. 65 | 66 | ### **`skip_if`** 67 | 68 | `skip` is used to skip (de)serialization of the field if the predicate function returns `True`. 69 | 70 | ```python 71 | @serde 72 | class World: 73 | buddy: str = field(default='', skip_if=lambda v: v == 'Pikachu') 74 | ``` 75 | 76 | See [examples/skip.py](https://github.com/yukinarit/pyserde/blob/main/examples/skip.py) for the complete example. 77 | 78 | ### **`skip_if_false`** 79 | 80 | `skip` is used to skip (de)serialization of the field if the field evaluates to `False`. For example, this code skip (de)serializing if `enemies` is empty. 81 | 82 | ```python 83 | @serde 84 | class World: 85 | enemies: list[str] = field(default_factory=list, skip_if_false=True) 86 | ``` 87 | 88 | See [examples/skip.py](https://github.com/yukinarit/pyserde/blob/main/examples/skip.py) for the complete example. 89 | 90 | ### **`skip_if_default`** 91 | 92 | `skip` is used to skip (de)serialization of the field if the field is equivalent to its default value. For example, this code skip (de)serializing if `town` is `Masara Town`. 93 | 94 | ```python 95 | @serde 96 | class World: 97 | town: str = field(default='Masara Town', skip_if_default=True) 98 | ``` 99 | 100 | See [examples/skip.py](https://github.com/yukinarit/pyserde/blob/main/examples/skip.py) for the complete example. 101 | 102 | ### **`alias`** 103 | 104 | You can set aliases for field names. Alias only works for deserialization. 105 | 106 | ```python 107 | @serde 108 | class Foo: 109 | a: str = field(alias=["b", "c"]) 110 | ``` 111 | 112 | `Foo` can be deserialized from either `{"a": "..."}`, `{"b": "..."}` or `{"c": "..."}`. 113 | 114 | See [examples/alias.py](https://github.com/yukinarit/pyserde/blob/main/examples/alias.py) for complete example. 115 | 116 | ## **`serializer`**/**`deserializer`** 117 | 118 | Sometimes you want to customize (de)serializer for a particular field, such as 119 | * You want to serialize datetime into a different format 120 | * You want to serialize a type in a third party package 121 | 122 | In the following example, field `a` is serialized into `"2021-01-01T00:00:00"` by the default serializer for `datetime`, whereas field `b` is serialized into `"01/01/21"` by the custom serializer. 123 | 124 | ```python 125 | @serde 126 | class Foo: 127 | a: datetime 128 | b: datetime = field(serializer=lambda x: x.strftime('%d/%m/%y'), deserializer=lambda x: datetime.strptime(x, '%d/%m/%y')) 129 | ``` 130 | 131 | See [examples/custom_field_serializer.py](https://github.com/yukinarit/pyserde/blob/main/examples/custom_field_serializer.py) for the complete example. 132 | 133 | ## **`flatten`** 134 | 135 | You can flatten the fields of the nested structure. 136 | 137 | ```python 138 | @serde 139 | class Bar: 140 | c: float 141 | d: bool 142 | 143 | @serde 144 | class Foo: 145 | a: int 146 | b: str 147 | bar: Bar = field(flatten=True) 148 | ``` 149 | 150 | Bar's c, d fields are deserialized as if they are defined in Foo. So you will get `{"a":10,"b":"foo","c":100.0,"d":true}` if you serialize `Foo` into JSON. 151 | 152 | See [examples/flatten.py](https://github.com/yukinarit/pyserde/blob/main/examples/flatten.py) for complete example. 153 | -------------------------------------------------------------------------------- /docs/en/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ## Installation 4 | 5 | Install pyserde from PyPI. pyserde requires Python>=3.9. 6 | 7 | ``` 8 | pip install pyserde 9 | ``` 10 | 11 | If you're using poetry, run this command. 12 | ``` 13 | poetry add pyserde 14 | ``` 15 | 16 | Additional data formats besides JSON and Pickle need additional dependencies installed. Install `msgpack`, `toml` or `yaml` extras to work with the appropriate data formats; you can skip formats that you don't plan to use. For example, if you want to use Toml and YAML: 17 | 18 | ``` 19 | pip install "pyserde[toml,yaml]" 20 | ``` 21 | 22 | With poetry 23 | ``` 24 | poetry add pyserde -E toml -E yaml 25 | ``` 26 | 27 | Or all at once: 28 | 29 | ``` 30 | pip install "pyserde[all]" 31 | ``` 32 | 33 | With poetry 34 | ``` 35 | poetry add pyserde -E all 36 | ``` 37 | 38 | Here are the available extras 39 | * `all`: Install `msgpack`, `toml`, `yaml`, `numpy`, `orjson`, and `sqlalchemy` extras 40 | * `msgpack`: Install [msgpack](https://github.com/msgpack/msgpack-python) 41 | * `toml`: Install [tomli](https://github.com/hukkin/tomli) and [tomli-w](https://github.com/hukkin/tomli-w) 42 | * NOTE: [tomllib](https://docs.python.org/3/library/tomllib.html) is used for python 3.11 onwards 43 | * `yaml`: Install [pyyaml](https://github.com/yaml/pyyaml) 44 | * `numpy`: Install [numpy](https://github.com/numpy/numpy) 45 | * `orjson`: Install [orjson](https://github.com/ijl/orjson) 46 | * `sqlalchemy`: Install [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) 47 | 48 | ## Define your first pyserde class 49 | 50 | Define your class with pyserde's `@serde` decorators. Be careful that module name is `serde`, not `pyserde`. `pyserde` heavily depends on the standard library's `dataclasses` module. If you are new to dataclass, I would recommend to read [dataclasses documentation](https://docs.python.org/3/library/dataclasses.html) first. 51 | 52 | ```python 53 | from serde import serde 54 | 55 | @serde 56 | class Foo: 57 | i: int 58 | s: str 59 | f: float 60 | b: bool 61 | ``` 62 | 63 | pyserde generates methods necessary for (de)serialization by `@serde` when a class is loaded into python interpreter. The code generation occurs only once and there is no overhead when you use the class. Now your class is serializable and deserializable in all the data formats supported by pyserde. 64 | 65 | > **NOTE:** If you need only either serialization or deserialization functionality, you can use `@serialize` or `@deserialize` instead of `@serde` decorator. 66 | > 67 | > e.g. If you do only serialization, you can use `@serialize` decorator. But calling deserialize API e.g. `from_json` for `Foo` will raise an error. 68 | > ```python 69 | > from serde import serialize 70 | > 71 | > @serialize 72 | > class Foo: 73 | > i: int 74 | > s: str 75 | > f: float 76 | > b: bool 77 | > ``` 78 | 79 | ## PEP585 and PEP604 80 | 81 | [PEP585](https://www.python.org/dev/peps/pep-0585/) style annotation is supported for python>=3.9. [PEP604](https://www.python.org/dev/peps/pep-0604/) Union operator is also supported for python>=3.10. With PEP585 and PEP604, you can write a pyserde class pretty neatly. 82 | ```python 83 | @serde 84 | class Foo: 85 | a: int 86 | b: list[str] 87 | c: tuple[int, float, str, bool] 88 | d: dict[str, list[tuple[str, int]]] 89 | e: str | None 90 | ``` 91 | 92 | ## Use pyserde class 93 | 94 | Next, import pyserde (de)serialize APIs. For JSON: 95 | 96 | ```python 97 | from serde.json import from_json, to_json 98 | ``` 99 | 100 | Use `to_json` to serialize the object into JSON. 101 | ``` 102 | f = Foo(i=10, s='foo', f=100.0, b=True) 103 | print(to_json(f)) 104 | ``` 105 | 106 | Pass `Foo` class and JSON string in `from_json` to deserialize JSON into the object. 107 | ```python 108 | s = '{"i": 10, "s": "foo", "f": 100.0, "b": true}' 109 | print(from_json(Foo, s)) 110 | ``` 111 | 112 | That's it! pyserde offers many more features. If you're interested, please read the rest of the documentation. 113 | 114 | > 💡 Tip: which type checker should I use? 115 | > pyserde depends on [PEP681 dataclass_transform](https://peps.python.org/pep-0681/). [mypy](https://github.com/python/mypy) does not fully support dataclass_transform as of Jan. 2024. My personal recommendation is [pyright](https://github.com/microsoft/pyright). 116 | -------------------------------------------------------------------------------- /docs/en/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `pyserde` is a simple yet powerful serialization library on top of [dataclasses](https://docs.python.org/3/library/dataclasses.html). It allows you to convert Python objects to and from JSON, YAML, and other formats easily and efficiently. 4 | 5 | Declare your class with `@serde` decorator and annotate fields using [PEP484](https://peps.python.org/pep-0484/) as below. 6 | 7 | ```python 8 | from serde import serde 9 | 10 | @serde 11 | class Foo: 12 | i: int 13 | s: str 14 | f: float 15 | b: bool 16 | ``` 17 | 18 | You can serialize `Foo` object into JSON. 19 | 20 | ```python 21 | >>> from serde.json import to_json 22 | >>> to_json(Foo(i=10, s='foo', f=100.0, b=True)) 23 | '{"i":10,"s":"foo","f":100.0,"b":true}' 24 | ``` 25 | 26 | You can deserialize JSON into `Foo` object. 27 | ```python 28 | >>> from serde.json import from_json 29 | >>> from_json(Foo, '{"i": 10, "s": "foo", "f": 100.0, "b": true}') 30 | Foo(i=10, s='foo', f=100.0, b=True) 31 | ``` 32 | 33 | ## Next Steps 34 | 35 | * [Getting started](getting-started.md) 36 | * [API Reference](https://yukinarit.github.io/pyserde/api/serde.html) 37 | * [Examples](https://github.com/yukinarit/pyserde/tree/main/examples) 38 | * [FAQ](faq.md) 39 | -------------------------------------------------------------------------------- /docs/en/type-check.md: -------------------------------------------------------------------------------- 1 | # Type Checking 2 | 3 | pyserde offers runtime type checking since v0.9. It was completely reworked at v0.14 using [beartype](https://github.com/beartype/beartype) and it became more sophisticated and reliable. It is highly recommended to enable type checking always as it helps writing type-safe and robust programs. 4 | 5 | ## `strict` 6 | 7 | Strict type checking is to check every field value against the declared type during (de)serialization and object construction. This is the default type check mode since v0.14. What will happen with this mode is if you declare a class with `@serde` decorator without any class attributes, `@serde(type_check=strict)` is assumed and strict type checking is enabled. 8 | 9 | ```python 10 | @serde 11 | class Foo: 12 | s: str 13 | ``` 14 | 15 | If you call `Foo` with wrong type of object, 16 | ```python 17 | foo = Foo(10) 18 | ``` 19 | 20 | you get an error 21 | ```python 22 | beartype.roar.BeartypeCallHintParamViolation: Method __main__.Foo.__init__() parameter s=10 violates type hint , as int 10 not instance of str. 23 | ``` 24 | 25 | > **NOTE:** beartype exception instead of SerdeError is raised from constructor because beartype does not provide post validation hook as of Feb. 2024. 26 | 27 | similarly, if you call (de)serialize APIs with wrong type of object, 28 | 29 | ```python 30 | print(to_json(foo)) 31 | ``` 32 | 33 | again you get an error 34 | 35 | ```python 36 | serde.compat.SerdeError: Method __main__.Foo.__init__() parameter s=10 violates type hint , as int 10 not instance of str. 37 | ``` 38 | 39 | > **NOTE:** There are several caveats regarding type checks by beartype. 40 | > 41 | > 1. beartype can not validate on mutated properties 42 | > 43 | > The following code mutates the property "s" at the bottom. beartype can not detect this case. 44 | > ```python 45 | > @serde 46 | > class Foo 47 | > s: str 48 | > 49 | > f = Foo("foo") 50 | > f.s = 100 51 | > ``` 52 | > 53 | > 2. beartype can not validate every one of elements in containers. This is not a bug. This is desgin principle of beartype. See [Does beartype actually do anything?](https://beartype.readthedocs.io/en/latest/faq/#faq-o1]. 54 | > ``` 55 | 56 | ## `coerce` 57 | 58 | Type coercing automatically converts a value into the declared type during (de)serialization. If the value is incompatible e.g. value is "foo" and type is int, pyserde raises an `SerdeError`. 59 | 60 | ```python 61 | @serde(type_check=coerce) 62 | class Foo 63 | s: str 64 | 65 | foo = Foo(10) 66 | # pyserde automatically coerce the int value 10 into "10". 67 | # {"s": "10"} will be printed. 68 | print(to_json(foo)) 69 | ``` 70 | 71 | ## `disabled` 72 | 73 | This is the default behavior until pyserde v0.8.3 and v0.9.x. No type coercion or checks are run. Even if a user puts a wrong value, pyserde doesn't complain anything. 74 | 75 | ```python 76 | @serde 77 | class Foo 78 | s: str 79 | 80 | foo = Foo(10) 81 | # pyserde doesn't complain anything. {"s": 10} will be printed. 82 | print(to_json(foo)) 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/en/union.md: -------------------------------------------------------------------------------- 1 | # Union Representation 2 | 3 | `pyserde`>=0.7 offers attributes to control how Union is (de)serialized. This concept is the very same as the one in [serde-rs](https://serde.rs/enum-representations.html). Note these representations only apply to the dataclass, non dataclass objects are (de)serialized with `Untagged` always. 4 | 5 | ## `Untagged` 6 | 7 | This is the default Union representation for pyserde<0.7. Given these dataclasses, 8 | 9 | ```python 10 | @serde 11 | class Bar: 12 | b: int 13 | 14 | @serde 15 | class Baz: 16 | b: int 17 | 18 | @serde(tagging=Untagged) 19 | class Foo: 20 | a: Union[Bar, Baz] 21 | ``` 22 | 23 | Note that `Bar` and `Baz` have the same field name and type. If you serialize `Foo(Baz(10))` into dict, you get `{"a": {"b": 10}}`. But if you deserialize `{"a": {"b": 10}}`, you get `Foo(Bar(10))` instead of `Foo(Baz(10))`. This means pyserde **can't correctly (de)serialize dataclasses of Union with `Untagged`**. This is why pyserde offers other kinds of union representation options. 24 | 25 | ## `ExternalTagging` 26 | 27 | This is the default Union representation since 0.7. A class declaration with `ExternalTagging` looks like below. If you serialize `Foo(Baz(10))` into dict, you get `{"a": {"Baz": {"b": 10}}}` and you can deserialize it back to `Foo(Baz(10))`. 28 | 29 | ```python 30 | @serde(tagging=ExternalTagging) 31 | class Foo: 32 | a: Union[Bar, Baz] 33 | ``` 34 | 35 | > **NOTE:** Non dataclass objects are alreays (de)serialized with `Untagged` regardless of `tagging` attribute because there is no information which can be used for tag. The drawback of `Untagged` is pyserde can't correctly deserialize certain types. For example, `Foo({1, 2, 3})` of below class is serialized into `{"a": [1, 2, 3]}`, but you get `Foo([1, 2, 3])` by deserializing. 36 | > 37 | > ```python 38 | > @serde(tagging=ExternalTagging) 39 | > class Foo: 40 | > a: Union[list[int], set[int]] 41 | > ``` 42 | 43 | ## `InternalTagging` 44 | 45 | A class declaration with `InternalTagging` looks like below. If you serialize `Foo(Baz(10))` into dict, you will get `{"a": {"type": "Baz", "b": 10}}` and you can deserialize it back to `Foo(Baz(10))`. `type` tag is encoded inside the `Baz`'s dictionary. 46 | 47 | ```python 48 | @serde(tagging=InternalTagging("type")) 49 | class Foo: 50 | a: Union[Bar, Baz] 51 | ``` 52 | 53 | ## `AdjacentTagging` 54 | 55 | A class declaration with `AdjacentTagging` looks like below. If you serialize `Foo(Baz(10))` into dict, you will get `{"a": {"type": "Baz", "content": {"b": 10}}}` and you can deserialize it back to `Foo(Baz(10))`. `type` tag is encoded inside `Baz`'s dictionary and `Baz`s fields are encoded inside `content`. 56 | 57 | ```python 58 | @serde(tagging=AdjacentTagging("type", "content")) 59 | class Foo: 60 | a: Union[Bar, Baz] 61 | ``` 62 | 63 | ## (de)serializing Union types directly 64 | 65 | New in v0.12.0. 66 | 67 | Passing Union types directly in (de)serialize APIs (e.g. to_json, from_json) was partially supported prior to v0.12, but the union type was always treated as untagged. Users had no way to change the union tagging. The following example code wasn't able to correctly deserialize into `Bar` due to untagged. 68 | 69 | ```python 70 | @serde 71 | class Foo: 72 | a: int 73 | 74 | @serde 75 | class Bar: 76 | a: int 77 | 78 | bar = Bar(10) 79 | s = to_json(bar) 80 | print(s) 81 | # prints {"a": 10} 82 | print(from_json(Union[Foo, Bar], s)) 83 | # prints Foo(10) 84 | ``` 85 | 86 | Since v0.12.0, pyserde can handle union that's passed in (de)serialize APIs a bit nicely. The union type is treated as externally tagged as that is the default tagging in pyserde. So the above example can correctly (de)serialize as `Bar`. 87 | 88 | ```python 89 | @serde 90 | class Foo: 91 | a: int 92 | 93 | @serde 94 | class Bar: 95 | a: int 96 | 97 | bar = Bar(10) 98 | s = to_json(bar, cls=Union[Foo, Bar]) 99 | print(s) 100 | # prints {"Bar" {"a": 10}} 101 | print(from_json(Union[Foo, Bar], s)) 102 | # prints Bar(10) 103 | ``` 104 | 105 | Also you can change the tagging using `serde.InternalTagging`, `serde.AdjacentTagging` and `serde.Untagged`. 106 | 107 | Now try to change the tagging for the above example. You need to pass a new argument `cls` in `to_json`. Also union class must be wrapped in either `InternalTagging`, `AdjacentTaging` or `Untagged` with required parameters. 108 | 109 | * InternalTagging 110 | ```python 111 | from serde import InternalTagging 112 | 113 | s = to_json(bar, cls=InternalTagging("type", Union[Foo, Bar])) 114 | print(s) 115 | # prints {"type": "Bar", "a": 10} 116 | print(from_json(InternalTagging("type", Union[Foo, Bar]), s)) 117 | # prints Bar(10) 118 | ``` 119 | * AdjacentTagging 120 | ```python 121 | from serde import AdjacentTagging 122 | 123 | s = to_json(bar, cls=AdjacentTagging("type", "content", Union[Foo, Bar])) 124 | print(s) 125 | # prints {"type": "Bar", "content": {"a": 10}} 126 | print(from_json(AdjacentTagging("type", "content", Union[Foo, Bar]), s)) 127 | # prints Bar(10) 128 | ``` 129 | * Untagged 130 | ```python 131 | from serde import Untagged 132 | 133 | s = to_json(bar, cls=Untagged(Union[Foo, Bar])) 134 | print(s) 135 | # prints {"a": 10} 136 | print(from_json(Untagged(Union[Foo, Bar]), s)) 137 | # prints Foo(10) 138 | ``` 139 | -------------------------------------------------------------------------------- /docs/ja/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](introduction.md) 4 | - [Getting Started](getting-started.md) 5 | - [Data Formats](data-formats.md) 6 | - [Types](types.md) 7 | - [Decorators](decorators.md) 8 | - [Class Attributes](class-attributes.md) 9 | - [Field Attributes](field-attributes.md) 10 | - [Union](union.md) 11 | - [Type Checking](type-check.md) 12 | - [Extension](extension.md) 13 | - [FAQ](faq.md) 14 | -------------------------------------------------------------------------------- /docs/ja/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["yukinarit"] 3 | language = "ja" 4 | multilingual = true 5 | src = "./" 6 | title = "pyserde documentation" 7 | 8 | [output.html] 9 | git-repository-url = "https://github.com/yukinarit/pyserde" 10 | edit-url-template = "https://github.com/yukinarit/pyserde/edit/main/docs/ja/{path}" 11 | -------------------------------------------------------------------------------- /docs/ja/data-formats.md: -------------------------------------------------------------------------------- 1 | # Data Formats 2 | 3 | pyserdeは、`dict`、`tuple`、`JSON`、`YAML`、`TOML`、`MsgPack`、`Pickle` などのさまざまなデータ形式をシリアライズおよびデシリアライズできます。 4 | 各APIは追加のキーワード引数を取ることができ、これらの引数はpyserdeで使用されるベースパッケージへと渡されます。 5 | 6 | 例えば、YAMLでフィールドの順序を保持したい場合、`serde.yaml.to_yaml`に`sort_key=True`を渡すことができます。 7 | 8 | ```python 9 | serde.yaml.to_yaml(foo, sort_key=True) 10 | ``` 11 | 12 | `sort_key=True`は[yaml.safedump](https://github.com/yukinarit/pyserde/blob/a9f44d52d109144a4c3bb93965f831e91d13960b/serde/yaml.py#L18)に渡されます。 13 | 14 | ## dict 15 | 16 | ```python 17 | >>> from serde import to_dict, from_dict 18 | 19 | >>> to_dict(Foo(i=10, s='foo', f=100.0, b=True)) 20 | {"i": 10, "s": "foo", "f": 100.0, "b": True} 21 | 22 | >>> from_dict(Foo, {"i": 10, "s": "foo", "f": 100.0, "b": True}) 23 | Foo(i=10, s='foo', f=100.0, b=True) 24 | ``` 25 | 26 | 詳細は[serde.to_dict](https://yukinarit.github.io/pyserde/api/serde/se.html#to_dict) / [serde.from_dict](https://yukinarit.github.io/pyserde/api/serde/de.html#from_dict)をご覧ください。 27 | 28 | ## tuple 29 | 30 | ```python 31 | >>> from serde import to_tuple, from_tuple 32 | 33 | >>> to_tuple(Foo(i=10, s='foo', f=100.0, b=True)) 34 | (10, 'foo', 100.0, True) 35 | 36 | >>> from_tuple(Foo, (10, 'foo', 100.0, True)) 37 | Foo(i=10, s='foo', f=100.0, b=True) 38 | ``` 39 | 40 | 詳細は[serde.to_tuple](https://yukinarit.github.io/pyserde/api/serde/se.html#to_tuple) / [serde.from_tuple](https://yukinarit.github.io/pyserde/api/serde/de.html#from_tuple)をご覧ください。 41 | 42 | ## JSON 43 | 44 | ```python 45 | >>> from serde.json import to_json, from_json 46 | 47 | >>> to_json(Foo(i=10, s='foo', f=100.0, b=True)) 48 | '{"i":10,"s":"foo","f":100.0,"b":true}' 49 | 50 | >>> from_json(Foo, '{"i": 10, "s": "foo", "f": 100.0, "b": true}') 51 | Foo(i=10, s='foo', f=100.0, b=True) 52 | ``` 53 | 54 | 詳細は[serde.json.to_json](https://yukinarit.github.io/pyserde/api/serde/json.html#to_json) / [serde.json.from_json](https://yukinarit.github.io/pyserde/api/serde/json.html#from_json)をご覧ください。 55 | 56 | ## Yaml 57 | 58 | ```python 59 | >>> from serde.yaml import from_yaml, to_yaml 60 | 61 | >>> to_yaml(Foo(i=10, s='foo', f=100.0, b=True)) 62 | b: true 63 | f: 100.0 64 | i: 10 65 | s: foo 66 | 67 | >>> from_yaml(Foo, 'b: true\nf: 100.0\ni: 10\ns: foo') 68 | Foo(i=10, s='foo', f=100.0, b=True) 69 | ``` 70 | 71 | 詳細は[serde.yaml.to_yaml](https://yukinarit.github.io/pyserde/api/serde/yaml.html#to_yaml) / [serde.yaml.from_yaml](https://yukinarit.github.io/pyserde/api/serde/yaml.html#from_yaml)をご覧ください。 72 | 73 | ## Toml 74 | 75 | ```python 76 | >>> from serde.toml from_toml, to_toml 77 | 78 | >>> to_toml(Foo(i=10, s='foo', f=100.0, b=True)) 79 | i = 10 80 | s = "foo" 81 | f = 100.0 82 | b = true 83 | 84 | >>> from_toml(Foo, 'i = 10\ns = "foo"\nf = 100.0\n 85 | 86 | b = true') 87 | Foo(i=10, s='foo', f=100.0, b=True) 88 | ``` 89 | 90 | 詳細は[serde.toml.to_toml](https://yukinarit.github.io/pyserde/api/serde/toml.html#to_toml) / [serde.toml.from_toml](https://yukinarit.github.io/pyserde/api/serde/toml.html#from_toml)をご覧ください。 91 | 92 | ## MsgPack 93 | 94 | ```python 95 | >>> from serde.msgpack import from_msgpack, to_msgpack 96 | 97 | >>> to_msgpack(Foo(i=10, s='foo', f=100.0, b=True)) 98 | b'\x84\xa1i\n\xa1s\xa3foo\xa1f\xcb@Y\x00\x00\x00\x00\x00\x00\xa1b\xc3' 99 | 100 | >>> from_msgpack(Foo, b'\x84\xa1i\n\xa1s\xa3foo\xa1f\xcb@Y\x00\x00\x00\x00\x00\x00\xa1b\xc3') 101 | Foo(i=10, s='foo', f=100.0, b=True) 102 | ``` 103 | 104 | 詳細は[serde.msgpack.to_msgpack](https://yukinarit.github.io/pyserde/api/serde/msgpack.html#to_msgpack) / [serde.msgpack.from_msgpack](https://yukinarit.github.io/pyserde/api/serde/msgpack.html#from_msgpack)をご覧ください。 105 | 106 | ## Pickle 107 | 108 | v0.9.6で新しく追加されました 109 | 110 | ```python 111 | >>> from serde.pickle import from_pickle, to_pickle 112 | 113 | >>> to_pickle(Foo(i=10, s='foo', f=100.0, b=True)) 114 | b"\x80\x04\x95'\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x01i\x94K\n\x8c\x01s\x94\x8c\x03foo\x94\x8c\x01f\x94G@Y\x00\x00\x00\x00\x00\x00\x8c\x01b\x94\x88u." 115 | 116 | >>> from_pickle(Foo, b"\x80\x04\x95'\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x01i\x94K\n\x8c\x01s\x94\x8c\x03foo\x94\x8c\x01f\x94G@Y\x00\x00\x00\x00\x00\x00\x8c\x01b\x94\x88u.") 117 | Foo(i=10, s='foo', f=100.0, b=True) 118 | ``` 119 | 120 | 詳細は[serde.pickle.to_pickle](https://yukinarit.github.io/pyserde/api/serde/pickle.html#to_pickle) / [serde.pickle.from_pickle](https://yukinarit.github.io/pyserde/api/serde/pickle.html#from_pickle)をご覧ください。 121 | 122 | ## 新しいデータ形式のサポートが必要ですか? 123 | 124 | pyserde をできるだけシンプルに保つため、XML のようなデータフォーマットをサポートする予定はありません。 125 | 新しいデータ形式のサポートが必要な場合は、別のPythonパッケージを作成することをお勧めします。 126 | データ形式がdictやtupleに変換可能であれば、シリアライズ/デシリアライズAPIの実装はそれほど難しくありません。 127 | 実装方法を知りたい場合は、[YAMLの実装](https://github.com/yukinarit/pyserde/blob/main/serde/yaml.py)をご覧ください。 128 | -------------------------------------------------------------------------------- /docs/ja/decorators.md: -------------------------------------------------------------------------------- 1 | # Decorators 2 | 3 | ## `@serde` 4 | 5 | `@serde` は `@serialize` と `@deserialize` デコレータのラッパーです。 6 | 7 | 以下のコードがあるとします。 8 | ```python 9 | @serde 10 | class Foo: 11 | ... 12 | ``` 13 | 14 | 上記のコードは、以下のコードと同等です。 15 | 16 | ```python 17 | @deserialize 18 | @serialize 19 | @dataclass 20 | class Foo: 21 | ... 22 | ``` 23 | 24 | `@serde` デコレータは以下を行います。 25 | * クラスに `@serialize` と `@deserialize` デコレータを追加します。 26 | * クラスが `@dataclass` を持っていない場合、`@dataclass` デコレータを追加します。 27 | * 両方の(デ)シリアライズ属性をデコレータに渡すことができます。 28 | * `serializer` 属性は `@deserialize` で無視され、`deserializer` 属性は `@serialize` で無視されます。 29 | 30 | ```python 31 | @serde(serializer=serializer, deserializer=deserializer) 32 | @dataclass 33 | class Foo: 34 | ... 35 | ``` 36 | 37 | > **注記:** `@serde` は `@dataclass` デコレータなしで動作します。 38 | > これはserdeが `@dataclass` を自動的に検出し、宣言されたクラスに追加するからです。 39 | > しかし `@dataclass` を定義しない場合、mypy では `Too many arguments` または `Unexpected keyword argument` というエラーが発生します。これは [mypy の制限](https://mypy.readthedocs.io/en/stable/additional_features.html#caveats-known-issues)によるものです。 40 | > 41 | > ```python 42 | > @serde 43 | > class Foo: 44 | > ... 45 | > ``` 46 | > 47 | > しかし、PEP681に準拠した型チェッカー(例:pyright)を使用すると、pyserdeが [PEP681 dataclass_transform](https://peps.python.org/pep-0681/) をサポートしているため、型エラーは発生しません。 48 | 49 | ## `@serialize` と `@deserialize` 50 | 51 | `@serialize` および `@deserialize` は内部的に `@serde` によって使用されます。 52 | 以下に該当する場合、これら2つのデコレータを使用することを推奨します。 53 | * シリアライズまたはデシリアライズの機能のみが必要な場合 54 | * 以下のように異なるクラス属性を持つことを望む場合 55 | 56 | ```python 57 | @deserialize(rename_all = "snakecase") 58 | @serialize(rename_all = "camelcase") 59 | class Foo: 60 | ... 61 | ``` 62 | 上記に当てはまらない場合は `@serde` の使用を推奨します。 63 | 64 | ## `@serde` なしでクラスを(デ)シリアライズする 65 | 66 | pyserdeは v0.10.0 以降、`@serde` なしでデータクラスを(デ)シリアライズできます。 67 | この機能は、外部ライブラリで宣言されたクラスを使用したい場合や、`@serde` デコレータが型チェッカーで機能しない場合に便利です。[この例](https://github.com/yukinarit/pyserde/blob/main/examples/plain_dataclass.py)を参照してください。 68 | 69 | どのように動作するのでしょうか? 70 | それは非常に単純で、クラスが`@serde`デコレータを持っていない場合、pyserdeは `@serde` デコレータを追加します。 71 | 最初のAPI呼び出しには時間がかかるかもしれませんが、生成されたコードは内部的にキャッシュされるため問題にはなりません。 72 | 73 | 以下にサードパーティパッケージのクラスをデシリアライズする例を示します。 74 | 75 | ```python 76 | @dataclass 77 | class External: 78 | ... 79 | 80 | to_dict(External()) # "@serde" なしで動作します 81 | ``` 82 | 83 | この場合、`rename_all` などのクラス属性を指定することはできません。 84 | 外部のデータクラスにクラス属性を追加したい場合は、データクラスを拡張することでそれを行う方法があります。[この例](https://github.com/yukinarit/pyserde/blob/main/examples/plain_dataclass_class_attribute.py)を参照してください。 85 | 86 | ```python 87 | @dataclass 88 | class External: 89 | some_value: int 90 | 91 | @serde(rename_all="kebabcase") 92 | @dataclass 93 | class Wrapper(External): 94 | pass 95 | ``` 96 | 97 | ## 前方参照をどのように使用するか? 98 | 99 | pyserdeは前方参照をサポートしています。 100 | ネストされたクラス名を文字列で置き換えると、pyserdeはネストされたクラスが定義された後にデコレータを探して評価します。 101 | 102 | ```python 103 | from __future__ import annotations # annotationsをインポート 104 | 105 | @dataclass 106 | class Foo: 107 | i: int 108 | s: str 109 | bar: Bar # "Bar" は後で宣言されていても指定できます 110 | 111 | @serde 112 | @dataclass 113 | class Bar: 114 | f: float 115 | b: bool 116 | 117 | # "Bar" が定義された後に pyserde デコレータを評価します 118 | serde(Foo) 119 | ``` 120 | 121 | ## PEP563 Annotationsによる遅延評価 122 | 123 | pyserdeは [PEP563 Annotationsによる遅延評価](https://peps.python.org/pep-0563/)をサポートしています。 124 | 125 | ```python 126 | from __future__ import annotations 127 | from serde import serde 128 | 129 | @serde 130 | class Foo: 131 | i: int 132 | s: str 133 | f: float 134 | b: bool 135 | 136 | def foo(self, cls: Foo): # 定義される前に "Foo" 型を使用できます 137 | print('foo') 138 | ``` 139 | 140 | 完全な例については [examples/lazy_type_evaluation.py](https://github.com/yukinarit/pyserde/blob/main/examples/lazy_type_evaluation.py) を参照してください。 141 | -------------------------------------------------------------------------------- /docs/ja/extension.md: -------------------------------------------------------------------------------- 1 | # Extending pyserde 2 | 3 | pyserde はビルトイン型以外をサポートするために pyserde を拡張する 3 つの方法を提供しています。 4 | 5 | ## カスタムフィールド(デ)シリアライザ 6 | 7 | 詳細は[カスタムフィールドシリアライザ](./field-attributes.md#serializerdeserializer)を参照してください。 8 | 9 | > 💡 ヒント:`serde.field` を独自のフィールド関数でラップすると以下のようになります。 10 | > 11 | > ```python 12 | > import serde 13 | > 14 | > def field(*args, **kwargs): 15 | > serde.field(*args, **kwargs, serializer=str) 16 | > 17 | > @serde 18 | > class Foo: 19 | > a: int = field(default=0) # フィールドシリアライザの設定 20 | > ``` 21 | 22 | ## カスタムクラス(デ)シリアライザ 23 | 24 | 詳細は[カスタムクラスシリアライザ](./class-attributes.md#class_serializer--class_deserializer)を参照してください。 25 | 26 | ## カスタムグローバル(デ)シリアライザ 27 | 28 | `add_serializer` と `add_deserializer` を使ってクラス(デ)シリアライザを登録することで、コードベース全体でカスタム(デ)シリアライザを適用できます。 29 | 30 | 登録されたクラス(デ)シリアライザは pyserde のグローバル空間にスタックされ、すべての pyserde クラスで自動的に使用されます。 31 | 32 | 例:[isodate](https://pypi.org/project/isodate/)パッケージを使用して `datetime.timedelta` のカスタム(デ)シリアライザを実装する。 33 | 34 | 以下は、`datetime.timedelta` のためのクラス(デ)シリアライザを登録するコードです。 35 | このパッケージは実際に PyPI で [pyserde-timedelta](https://pypi.org/project/pyserde-timedelta/) として公開されています。 36 | 37 | ```python 38 | from datetime import timedelta 39 | from plum import dispatch 40 | from typing import Type, Any 41 | import isodate 42 | import serde 43 | 44 | class Serializer: 45 | @dispatch 46 | def serialize(self, value: timedelta) -> Any: 47 | return isodate.duration_isoformat(value) 48 | 49 | class Deserializer: 50 | @dispatch 51 | def deserialize(self, cls: Type[timedelta], value: Any) -> timedelta: 52 | return isodate.parse_duration(value) 53 | 54 | def init() -> None: 55 | serde.add_serializer(Serializer()) 56 | serde.add_deserializer(Deserializer()) 57 | ``` 58 | 59 | このパッケージの利用者は、 `serde_timedelta.init()` を呼び出すだけで `datetime.timedelta` のカスタム(デ)シリアライザの機能を再利用できます。 60 | 61 | ```python 62 | import serde_timedelta 63 | from serde import serde 64 | from serde.json import to_json, from_json 65 | from datetime import timedelta 66 | 67 | serde_timedelta.init() 68 | 69 | @serde 70 | class Foo: 71 | a: timedelta 72 | 73 | f = Foo(timedelta(hours=10)) 74 | json = to_json(f) 75 | print(json) 76 | print(from_json(Foo, json)) 77 | ``` 78 | 79 | 以下のように `datetime.timedelta` がISO 8601 形式でシリアライズされます! 80 | 81 | ```bash 82 | {"a":"PT10H"} 83 | Foo(a=datetime.timedelta(seconds=36000)) 84 | ``` 85 | 86 | > 💡 ヒント:クラス(デ)シリアライザはいくつでも登録できます。これは、好きなだけpyserde拡張を使用できることを意味します。 87 | > 登録された(デ)シリアライザはメモリにスタックされます。 88 | > 89 | > なお、1つの(デ)シリアライザは他の(デ)シリアライザによってオーバーライドされる可能性があります。 90 | > 91 | > 例:次の順序で3つのカスタムシリアライザを登録すると、最初のシリアライザは3番目によって完全に上書きされます。2番目は異なる型用に実装されているため機能します。 92 | > 93 | > 1. `int` 用のシリアライザを登録 94 | > 2. `float` 用のシリアライザを登録 95 | > 3. `int` 用のシリアライザを登録 96 | 97 | こちらは、v0.13.0 で新しく追加されました。 98 | -------------------------------------------------------------------------------- /docs/ja/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## pyserdeによって生成されたコードをどのように確認できますか? 4 | 5 | pyserdeはコマンドラインとして動作する `inspect` サブモジュールを提供しています。 6 | ``` 7 | python -m serde.inspect 8 | ``` 9 | 10 | 例:pyserdeプロジェクト内で以下を実行します。 11 | 12 | ``` 13 | cd pyserde 14 | poetry shell 15 | python -m serde.inspect examples/simple.py Foo 16 | ``` 17 | 18 | 出力 19 | ```python 20 | Loading simple.Foo from examples. 21 | 22 | ================================================== 23 | Foo 24 | ================================================== 25 | 26 | -------------------------------------------------- 27 | Functions generated by pyserde 28 | -------------------------------------------------- 29 | def to_iter(obj, reuse_instances=True, convert_sets=False): 30 | if reuse_instances is Ellipsis: 31 | reuse_instances = True 32 | if convert_sets is Ellipsis: 33 | convert_sets = False 34 | if not is_dataclass(obj): 35 | return copy.deepcopy(obj) 36 | 37 | Foo = serde_scope.types["Foo"] 38 | res = [] 39 | res.append(obj.i) 40 | res.append(obj.s) 41 | res.append(obj.f) 42 | res.append(obj.b) 43 | return tuple(res) 44 | ... 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/ja/field-attributes.md: -------------------------------------------------------------------------------- 1 | # Field Attributes 2 | 3 | フィールド属性は、データクラスのフィールドの(デ)シリアライズ動作をカスタマイズするためのオプションです。 4 | 5 | ## dataclassesによって提供される属性 6 | 7 | ### **`default`** と **`default_factory`** 8 | 9 | dataclassesの `default` と `default_factory` は期待通りに動作します。 10 | フィールドが `default` または `default_factory` 属性を持つ場合、そのフィールドはオプショナルフィールドのように振る舞います。 11 | フィールドがデータ内に存在する場合、当該フィールドの値がデシリアライズされたオブジェクトに設定されます。 12 | フィールドがデータ内に存在しない場合、指定されたデフォルト値がデシリアライズされたオブジェクトに設定されます。 13 | 14 | ```python 15 | class Foo: 16 | a: int = 10 17 | b: int = field(default=10) # field "a"と同じ 18 | c: dict[str, int] = field(default_factory=dict) 19 | 20 | print(from_dict(Foo, {})) # Foo(a=10, b=10, c={}) を出力 21 | ``` 22 | 23 | 完全な例については、[examples/default.py](https://github.com/yukinarit/pyserde/blob/main/examples/default.py)を参照してください。 24 | 25 | ### **`ClassVar`** 26 | 27 | `dataclasses.ClassVar` はデータクラスのクラス変数であり、dataclassはClassVarを擬似的なフィールドとして扱います。 28 | `dataclasses.field` はクラス変数を含まないため、pyserdeはデフォルトの動作として `ClassVar` フィールドを(デ)シリアライズしません。 29 | ClassVarフィールドをシリアライズする場合は、[serialize_class_var](class-attributes.md#serialize_class_var)クラス属性の使用を検討してください。 30 | 31 | 完全な例については、[examples/class_var.py](https://github.com/yukinarit/pyserde/blob/main/examples/class_var.py)を参照してください。 32 | 33 | ## pyserdeによって提供される属性 34 | 35 | フィールド属性は `serde.field` または `dataclasses.field` を通じて指定することができます。 36 | 型チェックが機能するため、 `serde.field` の使用を推奨します。 37 | 38 | 以下は、`rename` 属性を `serde.field` と `dataclasses.field` の両方で指定する例です。 39 | 40 | ```python 41 | @serde.serde 42 | class Foo: 43 | a: str = serde.field(rename="A") 44 | b: str = dataclasses.field(metadata={"serde_rename"="B"}) 45 | ``` 46 | 47 | ### **`rename`** 48 | 49 | `rename` は(デ)シリアライズ中にフィールド名を変更するために使用されます。 50 | この属性は、フィールド名にPythonのキーワード(予約語)を使用したい場合に便利です。 51 | 52 | 以下のコードはフィールド名 `id` を `ID` に変更する例です。 53 | 54 | ```python 55 | @serde 56 | class Foo: 57 | id: int = field(rename="ID") 58 | ``` 59 | 60 | 完全な例については、[examples/rename.py](https://github.com/yukinarit/pyserde/blob/main/examples/rename.py)を参照してください。 61 | 62 | ### **`skip`** 63 | 64 | `skip` はこの属性を持つフィールドの(デ)シリアライズをスキップします。 65 | 66 | ```python 67 | @serde 68 | class Resource: 69 | name: str 70 | hash: str 71 | metadata: dict[str, str] = field(default_factory=dict, skip=True) 72 | ``` 73 | 74 | 完全な例については、[examples/skip.py](https://github.com/yukinarit/pyserde/blob/main/examples/skip.py)を参照してください。 75 | 76 | ### **`skip_if`** 77 | 78 | `skip_if` は条件関数が `True` を返す場合にフィールドの(デ)シリアライズをスキップします。 79 | 80 | ```python 81 | @serde 82 | class World: 83 | buddy: str = field(default='', skip_if=lambda v: v == 'Pikachu') 84 | ``` 85 | 86 | 完全な例については、[examples/skip.py](https://github.com/yukinarit/pyserde/blob/main/examples/skip.py)を参照してください。 87 | 88 | ### **`skip_if_false`** 89 | 90 | `skip_if_false` はフィールドが `False` と評価される場合にフィールドの(デ)シリアライズをスキップします。 91 | 92 | 例えば、以下のコードは `enemies` が空であれば(デ)シリアライズをスキップします。 93 | 94 | ```python 95 | @serde 96 | class World: 97 | enemies: list[str] = field(default_factory=list, skip_if_false=True) 98 | ``` 99 | 100 | 完全な例については、[examples/skip.py](https://github.com/yukinarit/pyserde/blob/main/examples/skip.py)を参照してください。 101 | 102 | ### **`skip_if_default`** 103 | 104 | `skip_if_default` はフィールドがデフォルト値とイコールである場合にフィールドの(デ)シリアライズをスキップします。 105 | 106 | 例えば、以下のコードは `town` が `Masara Town` であれば(デ)シリアライズをスキップします。 107 | 108 | ```python 109 | @serde 110 | class World: 111 | town: str = field(default='Masara Town', skip_if_default=True) 112 | ``` 113 | 114 | 完全な例については、[examples/skip.py](https://github.com/yukinarit/pyserde/blob/main/examples/skip.py)を参照してください。 115 | 116 | ### **`alias`** 117 | 118 | フィールド名に別名を設定できます。エイリアスはデシリアライズ時にのみ機能します。 119 | 120 | ```python 121 | @serde 122 | class Foo: 123 | a: str = field(alias=["b", "c"]) 124 | ``` 125 | 126 | 上記の例では、 `Foo` は `{"a": "..."}` , `{"b": "..."}` 、または `{"c": "..."}` のいずれかからデシリアライズできます。 127 | 128 | 完全な例については、[examples/alias.py](https://github.com/yukinarit/pyserde/blob/main/examples/alias.py)を参照してください。 129 | 130 | ## **`serializer`** と **`deserializer`** 131 | 132 | 特定のフィールドに対してカスタム(デ)シリアライザを使用したい場合があります。 133 | 134 | 例えば、以下のようなケースです。 135 | * datetimeを異なる形式でシリアライズしたい。 136 | * サードパーティパッケージの型をシリアライズしたい。 137 | 138 | 以下の例では、フィールド `a` はデフォルトのシリアライザで `"2021-01-01T00:00:00"` にシリアライズされますが、フィールド `b` はカスタムシリアライザで `"01/01/21"` にシリアライズされます。 139 | 140 | ```python 141 | @serde 142 | class Foo: 143 | a: datetime 144 | b: datetime = field(serializer=lambda x: x.strftime('%d/%m/%y'), deserializer=lambda x: datetime.strptime(x, '%d/%m/%y')) 145 | ``` 146 | 147 | 完全な例については、[examples/custom_field_serializer.py](https://github.com/yukinarit/pyserde/blob/main/examples/custom_field_serializer.py)を参照してください。 148 | 149 | ## **`flatten`** 150 | 151 | ネストされた構造のフィールドをフラットにできます。 152 | 153 | ```python 154 | @serde 155 | class Bar: 156 | c: float 157 | d: bool 158 | 159 | @serde 160 | class Foo: 161 | a: int 162 | b: str 163 | bar: Bar = field(flatten=True) 164 | ``` 165 | 166 | 上記の例では、`Bar` の `c` および `d` フィールドが `Foo` で定義されているかのようにデシリアライズされます。 167 | `Foo` をJSON形式にシリアライズすると `{"a":10,"b":"foo","c":100.0,"d":true}` を取得します。 168 | 169 | 完全な例については、[examples/flatten.py](https://github.com/yukinarit/pyserde/blob/main/examples/flatten.py)を参照してください。 170 | -------------------------------------------------------------------------------- /docs/ja/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## インストール 4 | 5 | PyPIからpyserdeをインストールしてください。pyserdeにはPython>=3.9が必要です。 6 | 7 | ``` 8 | pip install pyserde 9 | ``` 10 | 11 | poetryを使用している場合は、以下のコマンドを実行してください。 12 | ``` 13 | poetry add pyserde 14 | ``` 15 | 16 | JSONとPickle以外のデータ形式を扱う場合は、追加の依存関係をインストールする必要があります。 17 | 適切なデータ形式で動作するには、`msgpack`、`toml`、`yaml` の追加パッケージをインストールしてください。 18 | 使用しない場合はスキップして構いません。 19 | 例えば、TomlとYAMLを使用する場合は以下のようにします。 20 | 21 | ``` 22 | pip install "pyserde[toml,yaml]" 23 | ``` 24 | 25 | poetryを使用している場合 26 | ``` 27 | poetry add pyserde -E toml -E yaml 28 | ``` 29 | 30 | 一度にすべてをインストールする場合 31 | 32 | ``` 33 | pip install "pyserde[all]" 34 | ``` 35 | 36 | poetryを使用している場合 37 | ``` 38 | poetry add pyserde -E all 39 | ``` 40 | 41 | 利用可能な追加パッケージ 42 | * `all`:`msgpack`、`toml`、`yaml`、`numpy`、`orjson`、`sqlalchemy` をインストール 43 | * `msgpack`:[msgpack](https://github.com/msgpack/msgpack-python) をインストール 44 | * `toml`:[tomli](https://github.com/hukkin/tomli) と [tomli-w](https://github.com/hukkin/tomli-w) をインストール 45 | * 注記:python 3.11以降は [tomllib](https://docs.python.org/3/library/tomllib.html) を使用 46 | * `yaml`:[pyyaml](https://github.com/yaml/pyyaml) をインストール 47 | * `numpy`:[numpy](https://github.com/numpy/numpy) をインストール 48 | * `orjson`:[orjson](https://github.com/ijl/orjson) をインストール 49 | * `sqlalchemy`:[sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) をインストール 50 | 51 | 52 | ## 最初のpyserdeクラスを定義 53 | 54 | pyserdeの`@serde`デコレータを使用してクラスを定義します。 55 | モジュール名が `pyserde` ではなく `serde` であることに注意してください。 56 | `pyserde`は標準ライブラリの`dataclasses`モジュールに大きく依存しています。 57 | そのため、dataclassに慣れていない場合はまず[dataclassesのドキュメント](https://docs.python.org/3/library/dataclasses.html)を読むことをおすすめします。 58 | 59 | ```python 60 | from serde import serde 61 | 62 | @serde 63 | class Foo: 64 | i: int 65 | s: str 66 | f: float 67 | b: bool 68 | ``` 69 | 70 | クラスがPythonインタプリタにロードされると、pyserdeは`@serde`によって(デ)シリアライズに必要なメソッドを生成します。 71 | コード生成は一度だけ行われ、クラスを使用する際にオーバーヘッドはありません。 72 | これにより、クラスはpyserdeがサポートするすべてのデータ形式でシリアライズおよびデシリアライズ可能になります。 73 | 74 | > **注記:** シリアライズまたはデシリアライズの機能のみが必要な場合、`@serde`デコレータの代わりに`@serialize`や`@deserialize`を使用できます。 75 | > 76 | > 例:シリアライズのみを行う場合、`@serialize`デコレータを使用できます。しかし、`Foo`のデシリアライズAPI(例:`from_json`)を呼び出すとエラーが発生します。 77 | > ```python 78 | > from serde import serialize 79 | > 80 | > @serialize 81 | > class Foo: 82 | > i: int 83 | > s: str 84 | > f: float 85 | > b: bool 86 | > ``` 87 | 88 | ## PEP585とPEP604 89 | 90 | python>=3.9用の[PEP585](https://www.python.org/dev/peps/pep-0585/)スタイルのアノテーションと、python>=3.10用の[PEP604](https://www.python.org/dev/peps/pep-0604/) Unionオペレータがサポートされています。 91 | PEP585とPEP604を使用すると、pyserdeクラスをきれいに書くことができます。 92 | ```python 93 | @serde 94 | class Foo: 95 | a: int 96 | b: list[str] 97 | c: tuple[int, float, str, bool] 98 | d: dict[str, list[tuple[str, int]]] 99 | e: str | None 100 | ``` 101 | 102 | ## pyserdeクラスの使用 103 | 104 | 次に、pyserdeの(デ)シリアライズAPIをインポートします。 105 | JSONの場合は以下のようにします。 106 | 107 | ```python 108 | from serde.json import from_json, to_json 109 | ``` 110 | 111 | `to_json`を使用してオブジェクトをJSONにシリアライズします。 112 | ```python 113 | f = Foo(i=10, s='foo', f=100.0, b=True) 114 | print(to_json(f)) 115 | ``` 116 | 117 | `from_json`に`Foo`クラスとJSON文字列を渡して、JSONをオブジェクトにデシリアライズします。 118 | ```python 119 | s = '{"i": 10, "s": "foo", "f": 100.0, "b": true}' 120 | print(from_json(Foo, s)) 121 | ``` 122 | 123 | 以上です! 124 | pyserdeには他にも多くの機能があります。興味があれば、残りのドキュメントをお読みください。 125 | 126 | > 💡 ヒント:どのタイプチェッカーを使用すべきか? 127 | > pyserdeは[PEP681 dataclass_transform](https://peps.python.org/pep-0681/)に依存しています。 128 | > 2024年1月現在、[mypy](https://github.com/python/mypy)はdataclass_transformを完全にはサポートしていません。 129 | > 私の個人的なおすすめは[pyright](https://github.com/microsoft/pyright)です。 130 | -------------------------------------------------------------------------------- /docs/ja/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `pyserde`は[dataclasses](https://docs.python.org/3/library/dataclasses.html)ベースのシンプルで強力なシリアライゼーションライブラリで、クラスをJSONやYAML等の様々なデータフォーマットに簡単に効率的に変換可能になります。 4 | 5 | クラスに`@serde`デコレータを付け、[PEP484](https://peps.python.org/pep-0484/)形式でフィールドに型アノテーションを付けます。 6 | 7 | ```python 8 | from serde import serde 9 | 10 | @serde 11 | class Foo: 12 | i: int 13 | s: str 14 | f: float 15 | b: bool 16 | ``` 17 | 18 | すると、`Foo`クラスは以下のようにJSONにシリアライズ出来るようになります。 19 | 20 | ```python 21 | >>> from serde.json import to_json 22 | >>> to_json(Foo(i=10, s='foo', f=100.0, b=True)) 23 | '{"i":10,"s":"foo","f":100.0,"b":true}' 24 | ``` 25 | 26 | また、JSONから`Foo`クラスにデシリアライズも出来るようになります。 27 | ```python 28 | >>> from serde.json import from_json 29 | >>> from_json(Foo, '{"i": 10, "s": "foo", "f": 100.0, "b": true}') 30 | Foo(i=10, s='foo', f=100.0, b=True) 31 | ``` 32 | 33 | ## Next Steps 34 | 35 | * [Getting started](getting-started.md) 36 | * [API Reference](https://yukinarit.github.io/pyserde/api/serde.html) 37 | * [Examples](https://github.com/yukinarit/pyserde/tree/main/examples) 38 | * [FAQ](faq.md) 39 | -------------------------------------------------------------------------------- /docs/ja/type-check.md: -------------------------------------------------------------------------------- 1 | # Type Checking 2 | 3 | pyserdeはv0.9からランタイム型チェックを提供しています。 4 | v0.14で完全に作り直され、[beartype](https://github.com/beartype/beartype)を使用してより洗練され信頼性の高いものとなりました。 5 | 型安全で堅牢なプログラムを書くためには、型チェックを常に有効にすることを強く推奨します。 6 | 7 | ## `strict` 8 | 9 | 厳格な型チェック `strict` は、(デ)シリアライズとオブジェクト構築の際にすべてのフィールド値を宣言された型と照合します。 10 | これはv0.14以降のデフォルトの型チェックモードです。 11 | 12 | このモードでは、クラス属性を指定せずに `@serde` デコレータを使用してクラスを宣言した場合、`@serde(type_check=strict)` と見なされ、厳格な型チェックが有効になります。 13 | 14 | ```python 15 | @serde 16 | class Foo: 17 | s: str 18 | ``` 19 | 20 | 例えば、以下のように間違った型のオブジェクトで`Foo`を呼び出すと、 21 | ```python 22 | foo = Foo(10) 23 | ``` 24 | 25 | 以下のエラーが発生します。 26 | ```python 27 | beartype.roar.BeartypeCallHintParamViolation: Method __main__.Foo.__init__() parameter s=10 violates type hint , as int 10 not instance of str. 28 | ``` 29 | 30 | > **注記:** 2024年2月時点でbeartypeは検証フックを提供していないため、コンストラクターからはSerdeErrorではなくbeartypeの例外が発生します。 31 | 32 | 同様に、間違った型のオブジェクトで(デ)シリアライズAPIを呼び出すと、 33 | 34 | ```python 35 | print(to_json(foo)) 36 | ``` 37 | 38 | 再びエラーが発生します。 39 | 40 | ```python 41 | serde.compat.SerdeError: Method __main__.Foo.__init__() parameter s=10 violates type hint , as int 10 not instance of str. 42 | ``` 43 | 44 | > **注記:** beartypeによる型チェックにはいくつかの注意点があります。 45 | > 46 | > 1. beartypeは変更されたプロパティを検証できません。 47 | > 48 | > 以下のコードでは、プロパティ `s` が最後に変更されていますが、beartypeはこのケースを検出できません。 49 | > ```python 50 | > @serde 51 | > class Foo: 52 | > s: str 53 | > 54 | > f = Foo("foo") 55 | > f.s = 100 56 | > ``` 57 | > 58 | > 2. beartypeはコンテナ内の各要素を検証することはできません。これはバグではなく、beartypeの設計原則です。[Does beartype actually do anything?](https://beartype.readthedocs.io/en/latest/faq/#faq-o1)を参照してください。 59 | 60 | ## `coerce` 61 | 62 | 型強制 `coerce` は、(デ)シリアライズ中に値を宣言された型に自動的に変換します。 63 | 64 | ```python 65 | @serde(type_check=coerce) 66 | class Foo: 67 | s: str 68 | 69 | foo = Foo(10) 70 | # pyserdeは自動的に int 値の 10 を str の "10" に変換します 71 | # {"s": "10"}が出力されます 72 | print(to_json(foo)) 73 | ``` 74 | 75 | しかし、値が宣言された型に変換できない場合(例えば、値が `foo` で型が `int` の場合)、pyserde は`SerdeError` を発生させます。 76 | 77 | ## `disabled` 78 | 79 | これはpyserde v0.8.3およびv0.9.xまでのデフォルトの挙動です。 80 | 型強制またはチェックは実行されません。 81 | 82 | 利用者が間違った値を入力しても、pyserdeは型の不整合を無視して処理を続行します。 83 | 84 | ```python 85 | @serde 86 | class Foo: 87 | s: str 88 | 89 | foo = Foo(10) 90 | # pyserdeは型の整合性を確認しないため、{"s": 10} が出力されます 91 | print(to_json(foo)) 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/ja/union.md: -------------------------------------------------------------------------------- 1 | # Union Representation 2 | 3 | `pyserde>=0.7` では、Union が(デ)シリアライズされる方法を制御するための属性が提供されています。この概念は [serde-rs](https://serde.rs/enum-representations.html) にあるものと同じです。 4 | 5 | これらの表現は dataclass にのみ適用され、dataclassではないオブジェクトは常に `Untagged` で(デ)シリアライズされます。 6 | 7 | ## `Untagged` 8 | 9 | 以下は、 `pyserde<0.7` におけるデフォルトの Union 表現です。dataclass が与えられた場合を例に上げます。 10 | 11 | ```python 12 | @serde 13 | class Bar: 14 | b: int 15 | 16 | @serde 17 | class Baz: 18 | b: int 19 | 20 | @serde(tagging=Untagged) 21 | class Foo: 22 | a: Union[Bar, Baz] 23 | ``` 24 | 25 | 注意点として、`Bar` と `Baz` は同じフィールド名と型を持っています。 26 | `Foo(Baz(10))` を辞書にシリアライズすると、`{"a": {"b": 10}}` が得られます。 27 | 28 | しかし、`{"a": {"b": 10}}` をデシリアライズすると、 `Foo(Baz(10))` ではなく `Foo(Bar(10))` が得られます。 29 | これは、pyserde が `Untagged` を使った Union の dataclass を正しく(デ)シリアライズ**できない**ことを意味します。 30 | 31 | このため、pyserde は他の Union 表現オプションを提供しています。 32 | 33 | ## `ExternalTagging` 34 | 35 | これは 0.7 以降のデフォルトの Union 表現です。`ExternalTagging` を使用したクラス宣言は以下のようになります。 36 | 37 | ```python 38 | @serde(tagging=ExternalTagging) 39 | class Foo: 40 | a: Union[Bar, Baz] 41 | ``` 42 | `Foo(Baz(10))` を辞書にシリアライズすると、 `{"a": {"Baz": {"b": 10}}}` が得られ、デシリアライズすると `Foo(Baz(10))` になります。 43 | 44 | > **注意:** dataclass でないオブジェクトは、`tagging` 属性に関わらず常に `Untagged` で(デ)シリアライズされます。 45 | > これはタグに使用できる情報がないためです。`Untagged` の欠点は、特定のタイプを正しく非シリアライズできないことです。 46 | > 47 | > 例えば、以下のクラスの `Foo({1, 2, 3})` は `{"a": [1, 2, 3]}` にシリアライズされますが、デシリアライズすると `Foo([1, 2, 3])` になります。 48 | > 49 | > ```python 50 | > @serde(tagging=ExternalTagging) 51 | > class Foo: 52 | > a: Union[list[int], set[int]] 53 | > ``` 54 | 55 | ## `InternalTagging` 56 | 57 | `InternalTagging` を使用したクラス宣言は以下のようになります。 58 | 59 | ```python 60 | @serde(tagging=InternalTagging("type")) 61 | class Foo: 62 | a: Union[Bar, Baz] 63 | ``` 64 | 65 | `Foo(Baz(10))` を辞書にシリアライズすると、`{"a": {"type": "Baz", "b": 10}}` が得られ、それを `Foo(Baz(10))` にデシリアライズできます。 66 | `type` タグは `Baz` の辞書内にエンコードされます。 67 | 68 | ## `AdjacentTagging` 69 | 70 | `AdjacentTagging` を使用したクラス宣言は以下のようになります。 71 | 72 | ```python 73 | @serde(tagging=AdjacentTagging("type", "content")) 74 | class Foo: 75 | a: Union[Bar, Baz] 76 | ``` 77 | 78 | `Foo(Baz(10))` を辞書にシリアライズすると、`{"a": {"type": "Baz", "content": {"b": 10}}}` が得られ、それを `Foo(Baz(10))` にデシリアライズできます。`type` タグは `Baz` の辞書内にエンコードされ、`Baz` のフィールドは `content` 内にエンコードされます。 79 | 80 | 81 | ## Union 型の直接的な(デ)シリアライズ 82 | 83 | v0.12.0 で新しく追加されました。 84 | 85 | v0.12 以前において、(デ)シリアライズ API(例: `to_json`, `from_json`)に直接 Union 型のデータを渡すことは部分的にサポートされていましたが、Union 型は常にタグ無しとして扱われました。 86 | これは、利用者側で Union タグを変更することができなかったことを意味します。 87 | 88 | 以下の例は、タグ無しのため `Bar` に正しくデシリアライズできません。 89 | 90 | ```python 91 | @serde 92 | class Foo: 93 | a: int 94 | 95 | @serde 96 | class Bar: 97 | a: int 98 | 99 | bar = Bar(10) 100 | s = to_json(bar) 101 | print(s) 102 | # {"a": 10} を出力 103 | print(from_json(Union[Foo, Bar], s)) 104 | # Foo(10) を出力 105 | ``` 106 | 107 | v0.12.0 以降、pyserde は(デ)シリアライズ API で渡された Union 型を適切に扱えます。 108 | Union 型は、pyserde のデフォルトのタグ付けである外部タグ付けとして扱われます。 109 | 110 | 以下の例は `Bar` として正しく(デ)シリアライズできます。 111 | 112 | ```python 113 | @serde 114 | class Foo: 115 | a: int 116 | 117 | @serde 118 | class Bar: 119 | a: int 120 | 121 | bar = Bar(10) 122 | s = to_json(bar, cls=Union[Foo, Bar]) 123 | print(s) 124 | # {"Bar" {"a": 10}} を出力 125 | print(from_json(Union[Foo, Bar], s)) 126 | # Bar(10) を出力 127 | ``` 128 | 129 | また、`serde.InternalTagging`、`serde.AdjacentTagging`、および `serde.Untagged` を使用してタグ付けを変更できます。 130 | 131 | それでは、上記の例を用いてタグ付けを変更してみましょう。 132 | 133 | タグ付けを変更するには、 `to_json` に新しい引数 `cls` を渡す必要があります。 134 | また、Unionクラスは `InternalTagging`、`AdjacentTagging`、または `Untagged` で必要なパラメータと共にラップされる必要があります。 135 | 136 | * InternalTagging 137 | ```python 138 | from serde import InternalTagging 139 | 140 | s = to_json(bar, cls=InternalTagging("type", Union[Foo, Bar])) 141 | print(s) 142 | # {"type": "Bar", "a": 10} を出力 143 | print(from_json(InternalTagging("type", Union[Foo, Bar]), s)) 144 | # Bar(10) を出力 145 | ``` 146 | 147 | * AdjacentTagging 148 | ```python 149 | from serde 150 | 151 | s = to_json(bar, cls=AdjacentTagging("type", "content", Union[Foo, Bar])) 152 | print(s) 153 | # {"type": "Bar", "content": {"a": 10}} を出力 154 | print(from_json(AdjacentTagging("type", "content", Union[Foo, Bar]), s)) 155 | # Bar(10) を出力 156 | ``` 157 | 158 | * Untagged 159 | ```python 160 | from serde import Untagged 161 | 162 | s = to_json(bar, cls=Untagged(Union[Foo, Bar])) 163 | print(s) 164 | # {"a": 10} を出力 165 | print(from_json(Untagged(Union[Foo, Bar]), s)) 166 | # Foo(10) を出力 167 | ``` 168 | -------------------------------------------------------------------------------- /examples/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | pyserde = { path = "../", editable = true } 8 | toml = "*" 9 | pytoml = "*" 10 | requests = "*" 11 | envclasses = "==0.2.1" 12 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/examples/__init__.py -------------------------------------------------------------------------------- /examples/alias.py: -------------------------------------------------------------------------------- 1 | from serde import field, serde 2 | from serde.json import from_json 3 | 4 | 5 | @serde 6 | class Foo: 7 | a: int = field(alias=["b", "c", "d"]) 8 | 9 | 10 | def main() -> None: 11 | s = '{"a": 10}' 12 | print(f"From Json: {from_json(Foo, s)}") 13 | 14 | s = '{"b": 20}' 15 | print(f"From Json: {from_json(Foo, s)}") 16 | 17 | s = '{"c": 30}' 18 | print(f"From Json: {from_json(Foo, s)}") 19 | 20 | s = '{"d": 40}' 21 | print(f"From Json: {from_json(Foo, s)}") 22 | 23 | try: 24 | s = '{"e": 50}' 25 | print(f"From Json: {from_json(Foo, s)}") 26 | except Exception: 27 | pass 28 | 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /examples/any.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Bar: 9 | v: float 10 | 11 | 12 | @serde 13 | class Foo: 14 | a: Any 15 | b: Any 16 | c: Any 17 | d: Any 18 | 19 | 20 | def main() -> None: 21 | # Bar is serialized into dict. 22 | f = Foo(a=10, b=[1, 2], c={"foo": "bar"}, d=Bar(100.0)) 23 | print(f"Into Json: {to_json(f)}") 24 | 25 | # However, pyserde can't deserialize the dict into Bar. 26 | # This is because there is no "Bar" annotation in "Foo" class. 27 | s = '{"a": 10, "b": [1, 2], "c": {"foo": "bar"}, "d": {"v": 100.0}}' 28 | print(f"From Json: {from_json(Foo, s)}") 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /examples/app.yml: -------------------------------------------------------------------------------- 1 | --- 2 | addr: localhost 3 | port: 8080 4 | secret: THIS_IS_SECRET 5 | workers: 4 6 | -------------------------------------------------------------------------------- /examples/class_var.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Bar: 9 | v: int 10 | 11 | 12 | @serde(serialize_class_var=True) 13 | class Foo: 14 | a: ClassVar[int] = 10 15 | b: ClassVar[Bar] = Bar(100) 16 | c: ClassVar[list[Bar]] = [Bar(1), Bar(2)] 17 | 18 | 19 | def main() -> None: 20 | f = Foo() 21 | print(f"Into Json: {to_json(f)}") 22 | print(f"From Json: {from_json(Foo, '{}')}") 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /examples/collection.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde 6 | class Foo: 7 | a: list[str] 8 | b: tuple[str, bool] 9 | c: dict[str, list[int]] 10 | 11 | 12 | def main() -> None: 13 | h = Foo(["1", "2"], ("foo", True), {"bar": [10, 20]}) 14 | print(f"Into Json: {to_json(h)}") 15 | 16 | s = '{"a": ["1", "2"], "b": ["foo", true], "c": {"bar": [10, 20]}}' 17 | print(f"From Json: {from_json(Foo, s)}") 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /examples/custom_class_serializer.py: -------------------------------------------------------------------------------- 1 | from plum import dispatch 2 | from datetime import datetime 3 | from serde import ( 4 | serde, 5 | field, 6 | ) 7 | from serde.json import from_json, to_json 8 | from typing import Type, Any 9 | 10 | 11 | class MySerializer: 12 | @dispatch 13 | def serialize(self, value: datetime) -> str: 14 | return value.strftime("%d/%m/%y") 15 | 16 | 17 | class MyDeserializer: 18 | @dispatch 19 | def deserialize(self, cls: Type[datetime], value: Any) -> datetime: 20 | return datetime.strptime(value, "%d/%m/%y") 21 | 22 | 23 | @serde(class_serializer=MySerializer(), class_deserializer=MyDeserializer()) 24 | class Foo: 25 | a: datetime 26 | b: datetime = field( 27 | serializer=lambda x: x.strftime("%y.%m.%d"), 28 | deserializer=lambda x: datetime.strptime(x, "%y.%m.%d"), 29 | ) 30 | c: list[datetime] = field(default_factory=list) 31 | 32 | 33 | def main() -> None: 34 | dt = datetime(2021, 1, 1, 0, 0, 0) 35 | f = Foo(dt, dt, [dt]) 36 | print(f"Into Json: {to_json(f)}") 37 | 38 | s = '{"a": "01/01/21", "b": "01.01.21", "c": ["01/01/21"]}' 39 | print(f"From Json: {from_json(Foo, s)}") 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /examples/custom_field_serializer.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from serde import field, serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Foo: 9 | a: datetime 10 | b: datetime = field( 11 | serializer=lambda x: x.strftime("%d/%m/%y"), 12 | deserializer=lambda x: datetime.strptime(x, "%d/%m/%y"), 13 | ) 14 | 15 | 16 | def main() -> None: 17 | dt = datetime(2021, 1, 1, 0, 0, 0) 18 | f = Foo(dt, dt) 19 | print(f"Into Json: {to_json(f)}") 20 | 21 | s = '{"a": "2021-01-01T00:00:00", "b": "01/01/21"}' 22 | print(f"From Json: {from_json(Foo, s)}") 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /examples/custom_legacy_class_serializer.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Type, Any 3 | 4 | from serde import SerdeSkip, default_deserializer, default_serializer, field, serde 5 | from serde.json import from_json, to_json 6 | 7 | 8 | def serializer(cls: Type[Any], o: Any) -> str: 9 | """ 10 | Custom class level serializer. 11 | """ 12 | 13 | # You can provide a custom serialize/deserialize logic for certain types. 14 | if cls is datetime: 15 | # I don't know why but this is untyped in typeshed 16 | return o.strftime("%d/%m/%y") # type: ignore 17 | # Raise SerdeSkip to tell serde to use the default serializer/deserializer. 18 | else: 19 | raise SerdeSkip() 20 | 21 | 22 | def deserializer(cls: Type[Any], o: Any) -> datetime: 23 | """ 24 | Custom class level deserializer. 25 | """ 26 | if cls is datetime: 27 | return datetime.strptime(o, "%d/%m/%y") 28 | else: 29 | raise SerdeSkip() 30 | 31 | 32 | @serde(serializer=serializer, deserializer=deserializer) 33 | class Foo: 34 | a: datetime 35 | # Override by field serializer/deserializer. 36 | b: datetime = field( 37 | serializer=lambda x: x.strftime("%y.%m.%d"), 38 | deserializer=lambda x: datetime.strptime(x, "%y.%m.%d"), 39 | ) 40 | # Override by the default serializer/deserializer. 41 | c: datetime = field(serializer=default_serializer, deserializer=default_deserializer) 42 | 43 | 44 | def main() -> None: 45 | dt = datetime(2021, 1, 1, 0, 0, 0) 46 | f = Foo(dt, dt, dt) 47 | print(f"Into Json: {to_json(f)}") 48 | 49 | s = '{"a": "01/01/21", "b": "01.01.21", "c": "2021-01-01T00:00:00"}' 50 | print(f"From Json: {from_json(Foo, s)}") 51 | 52 | 53 | if __name__ == "__main__": 54 | main() 55 | -------------------------------------------------------------------------------- /examples/default.py: -------------------------------------------------------------------------------- 1 | from serde import serde, field 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde 6 | class Foo: 7 | i: int = 10 8 | s: str = "foo" 9 | f: float = field(default=100.0) # Use dataclass field. 10 | b: bool = field(default=True) 11 | d: dict[str, int] = field(default_factory=dict) 12 | 13 | 14 | def main() -> None: 15 | h = Foo() 16 | print(f"Into Json: {to_json(h)}") 17 | 18 | s = "{}" 19 | print(f"From Json: {from_json(Foo, s)}") 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /examples/default_dict.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Bar: 9 | v: int | None = None 10 | 11 | 12 | @serde 13 | class Foo: 14 | a: defaultdict[str, list[int]] 15 | b: defaultdict[str, int] 16 | c: defaultdict[str, Bar] 17 | 18 | 19 | def main() -> None: 20 | a: defaultdict[str, list[int]] = defaultdict(list) 21 | a["a"].append(1) 22 | b: defaultdict[str, int] = defaultdict(int) 23 | b["b"] 24 | c: defaultdict[str, Bar] = defaultdict(Bar) 25 | c["c"].v = 10 26 | 27 | f = Foo(a, b, c) 28 | print(f"Into Json: {to_json(f)}") 29 | 30 | s = '{"a": {"a": [1, 2]}, "b": {"b": 10}, "c": {"c": {}}}' 31 | print(f"From Json: {from_json(Foo, s)}") 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /examples/deny_unknown_fields.py: -------------------------------------------------------------------------------- 1 | from serde import serde, SerdeError 2 | from serde.json import from_json 3 | 4 | 5 | @serde(deny_unknown_fields=True) 6 | class Foo: 7 | a: int 8 | b: str 9 | 10 | 11 | def main() -> None: 12 | try: 13 | s = '{"a": 10, "b": "foo", "c": 100.0, "d": true}' 14 | print(f"From Json: {from_json(Foo, s)}") 15 | except SerdeError: 16 | pass 17 | 18 | 19 | if __name__ == "__main__": 20 | main() 21 | -------------------------------------------------------------------------------- /examples/enum34.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | import imported 4 | 5 | from serde import serde 6 | from serde.json import from_json, to_json 7 | 8 | 9 | class Nested(enum.Enum): 10 | S = "foo" 11 | 12 | 13 | class E(enum.Enum): 14 | S = "foo" 15 | F = 10.0 16 | B = True 17 | N = Nested.S 18 | 19 | 20 | class IE(enum.IntEnum): 21 | V0 = enum.auto() 22 | V1 = enum.auto() 23 | V2 = enum.auto() 24 | 25 | 26 | @serde 27 | class Foo: 28 | v0: IE 29 | v1: IE = IE.V1 # Default enum value. 30 | v2: E = E.S 31 | v3: imported.ImportedEnum = imported.ImportedEnum.V3 # Use enum imported from other module. 32 | v4: E = E.N # Use nested enum. 33 | 34 | 35 | def main() -> None: 36 | f = Foo(IE.V0) 37 | s = to_json(f) 38 | print(s) 39 | 40 | f = from_json(Foo, s) 41 | print(f) 42 | s = to_json(f) 43 | 44 | s = to_json(Foo(IE(3))) 45 | print(s) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /examples/env.py: -------------------------------------------------------------------------------- 1 | from envclasses import envclass, load_env 2 | from pathlib import Path 3 | 4 | from serde import deserialize 5 | from serde.yaml import from_yaml 6 | 7 | basedir = Path(__file__).parent 8 | 9 | 10 | @deserialize 11 | @envclass 12 | class App: 13 | addr: str 14 | port: int 15 | secret: str 16 | workers: int 17 | 18 | 19 | def main() -> None: 20 | with open(basedir / "app.yml") as f: 21 | yml = f.read() 22 | cfg = from_yaml(App, yml) 23 | print(cfg) 24 | 25 | load_env(cfg, prefix="APP") 26 | print(cfg) 27 | 28 | 29 | if __name__ == "__main__": 30 | main() 31 | -------------------------------------------------------------------------------- /examples/field_order.py: -------------------------------------------------------------------------------- 1 | from serde import serde, field 2 | 3 | 4 | @serde 5 | class Foo: 6 | id: int = field(rename="ID") # Field with attributes can be defined before other fields 7 | # thanks to dataclass_transform field_specifiers 8 | # https://peps.python.org/pep-0681/#the-dataclass-transform-decorator 9 | # NOTE: you still get error with mypy as of mypy v1.8.0 10 | comments: str 11 | 12 | 13 | if __name__ == "__main__": 14 | pass 15 | -------------------------------------------------------------------------------- /examples/flatten.py: -------------------------------------------------------------------------------- 1 | from serde import field, serde 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde 6 | class Bar: 7 | c: float 8 | d: bool 9 | 10 | 11 | @serde 12 | class Foo: 13 | a: int 14 | b: str 15 | bar: Bar = field(flatten=True) 16 | 17 | 18 | def main() -> None: 19 | f = Foo(a=10, b="foo", bar=Bar(c=100.0, d=True)) 20 | print(f"Into Json: {to_json(f)}") 21 | 22 | s = '{"a": 10, "b": "foo", "c": 100.0, "d": true}' 23 | print(f"From Json: {from_json(Foo, s)}") 24 | 25 | 26 | if __name__ == "__main__": 27 | main() 28 | -------------------------------------------------------------------------------- /examples/forward_reference.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from serde import serde 5 | from serde.json import from_json, to_json 6 | 7 | 8 | @dataclass 9 | class Foo: 10 | i: int 11 | s: str 12 | bar: Bar 13 | 14 | 15 | @serde 16 | class Bar: 17 | f: float 18 | b: bool 19 | 20 | 21 | # Evaluate pyserde decorator after `Bar` is defined. 22 | serde(Foo) 23 | 24 | 25 | def main() -> None: 26 | f = Foo(i=10, s="foo", bar=Bar(f=100.0, b=True)) 27 | print(f"Into Json: {to_json(f)}") 28 | 29 | s = '{"i": 10, "s": "foo", "bar": {"f": 100.0, "b": true}}' 30 | print(f"From Json: {from_json(Foo, s)}") 31 | 32 | 33 | if __name__ == "__main__": 34 | main() 35 | -------------------------------------------------------------------------------- /examples/frozen_set.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde 6 | class Foo: 7 | i: frozenset[int] 8 | 9 | 10 | def main() -> None: 11 | f = Foo(i=frozenset({1, 2})) 12 | print(f"Into Json: {to_json(f)}") 13 | 14 | s = '{"i": [1, 2]}' 15 | print(f"From Json: {from_json(Foo, s)}") 16 | 17 | 18 | if __name__ == "__main__": 19 | main() 20 | -------------------------------------------------------------------------------- /examples/generics.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar 2 | 3 | from serde import from_dict, serde, to_dict 4 | 5 | T = TypeVar("T") 6 | 7 | 8 | @serde 9 | class Bar: 10 | n: int 11 | 12 | 13 | @serde 14 | class Foo(Generic[T]): 15 | inner: T 16 | 17 | 18 | @serde 19 | class Baz(Generic[T]): 20 | foo: Foo[T] 21 | 22 | 23 | def main() -> None: 24 | # Use dataclass as type parameter 25 | foobar = Foo[Bar](Bar(10)) 26 | d = to_dict(foobar) 27 | print(from_dict(Foo[Bar], d)) 28 | 29 | # Use primitive as type parameter 30 | fooint = Foo[int](10) 31 | d = to_dict(fooint) 32 | print(from_dict(Foo[int], d)) 33 | 34 | # No type parameter. Any is deduced. 35 | foobar = Foo(Bar(10)) 36 | d = to_dict(foobar) 37 | print(from_dict(Foo, d)) 38 | 39 | # Nested 40 | bazint = Baz[int](Foo[int](10)) 41 | d = to_dict(bazint) 42 | print(from_dict(Baz[int], d)) 43 | 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /examples/generics_nested.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar 2 | 3 | from serde import from_dict, serde, to_dict 4 | 5 | 6 | @serde 7 | class EventData: 8 | pass 9 | 10 | 11 | @serde 12 | class A(EventData): 13 | a: str 14 | 15 | 16 | # Additional subclasses of EventData exist 17 | 18 | Data = TypeVar("Data", bound=EventData) 19 | 20 | 21 | @serde 22 | class Payload(Generic[Data]): 23 | id: int 24 | data: Data 25 | 26 | 27 | @serde 28 | class Event(Generic[Data]): 29 | name: str 30 | payload: Payload[Data] 31 | 32 | 33 | def main() -> None: 34 | event_a = Event("a", Payload(1, A("a_str"))) 35 | 36 | a_dict = to_dict(event_a) 37 | new_event_a = from_dict(Event[A], a_dict) 38 | 39 | print(event_a) 40 | print(new_event_a) 41 | 42 | # This has a bug, see https://github.com/yukinarit/pyserde/issues/464 43 | # payload = Payload(1, A("a_str")) 44 | # payload_dict = to_dict(payload) 45 | # new_payload = from_dict(Payload[A], payload_dict) 46 | # print(payload) 47 | # print(new_payload) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /examples/generics_pep695.py: -------------------------------------------------------------------------------- 1 | from serde import from_dict, serde, to_dict 2 | 3 | 4 | @serde 5 | class Bar: 6 | n: int 7 | 8 | 9 | @serde 10 | class Foo[T]: 11 | inner: T 12 | 13 | 14 | @serde 15 | class Baz[T]: 16 | foo: Foo[T] 17 | 18 | 19 | def main() -> None: 20 | # Use dataclass as type parameter 21 | foobar = Foo[Bar](Bar(10)) 22 | d = to_dict(foobar) 23 | print(from_dict(Foo[Bar], d)) 24 | 25 | # Use primitive as type parameter 26 | fooint = Foo[int](10) 27 | d = to_dict(fooint) 28 | print(from_dict(Foo[int], d)) 29 | 30 | # No type parameter. Any is deduced. 31 | foobar = Foo(Bar(10)) 32 | d = to_dict(foobar) 33 | print(from_dict(Foo, d)) 34 | 35 | # Nested 36 | bazint = Baz[int](Foo[int](10)) 37 | d = to_dict(bazint) 38 | print(from_dict(Baz[int], d)) 39 | 40 | 41 | if __name__ == "__main__": 42 | main() 43 | -------------------------------------------------------------------------------- /examples/global_custom_class_serializer.py: -------------------------------------------------------------------------------- 1 | from plum import dispatch 2 | from datetime import datetime 3 | from serde import serde, add_serializer, add_deserializer 4 | from serde.json import from_json, to_json 5 | from typing import Type, Any 6 | 7 | 8 | class MySerializer: 9 | @dispatch 10 | def serialize(self, value: datetime) -> str: 11 | return value.strftime("%d/%m/%y") 12 | 13 | 14 | class MyDeserializer: 15 | @dispatch 16 | def deserialize(self, cls: Type[datetime], value: Any) -> datetime: 17 | return datetime.strptime(value, "%d/%m/%y") 18 | 19 | 20 | class MySerializer2: 21 | @dispatch 22 | def serialize(self, value: int) -> str: 23 | return str(value) 24 | 25 | 26 | class MyDeserializer2: 27 | @dispatch 28 | def deserialize(self, cls: Type[int], value: Any) -> int: 29 | return int(value) 30 | 31 | 32 | class MySerializer3: 33 | @dispatch 34 | def serialize(self, value: float) -> str: 35 | return str(value) 36 | 37 | 38 | class MyDeserializer3: 39 | @dispatch 40 | def deserialize(self, cls: Type[float], value: Any) -> float: 41 | return float(value) 42 | 43 | 44 | add_serializer(MySerializer()) 45 | add_serializer(MySerializer2()) 46 | add_deserializer(MyDeserializer()) 47 | add_deserializer(MyDeserializer2()) 48 | 49 | 50 | @serde(class_serializer=MySerializer3(), class_deserializer=MyDeserializer3()) 51 | class Foo: 52 | a: datetime 53 | b: int 54 | c: float 55 | 56 | 57 | def main() -> None: 58 | dt = datetime(2021, 1, 1, 0, 0, 0) 59 | f = Foo(dt, 10, 100.0) 60 | print(f"Into Json: {to_json(f)}") 61 | 62 | s = '{"a": "01/01/21", "b": "10", "c": "100.0"}' 63 | print(f"From Json: {from_json(Foo, s)}") 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /examples/imported.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class ImportedEnum(enum.IntEnum): 5 | V0 = enum.auto() 6 | V1 = enum.auto() 7 | V2 = 10 8 | V3 = 100 9 | -------------------------------------------------------------------------------- /examples/init_var.py: -------------------------------------------------------------------------------- 1 | from dataclasses import InitVar 2 | 3 | from serde import serde, field 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Foo: 9 | a: int 10 | b: int | None = field(default=None, init=False) 11 | c: InitVar[int | None] = 1000 12 | 13 | def __post_init__(self, c: int | None) -> None: 14 | self.b = self.a * 10 15 | 16 | 17 | def main() -> None: 18 | f = Foo(10) 19 | print(f"Into Json: {to_json(f)}") 20 | 21 | s = '{"a": 10}' 22 | print(f"From Json: {from_json(Foo, s)}") 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /examples/jsonfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | jsonfile.py 3 | 4 | Deserialize JSON into an object. 5 | 6 | Usage: 7 | $ poetry install 8 | $ poetry run python jsonfile.py 9 | """ 10 | 11 | from serde import serde 12 | from serde.json import from_json 13 | 14 | 15 | @serde 16 | class Slide: 17 | title: str 18 | type: str 19 | items: list[str] | None 20 | 21 | 22 | @serde 23 | class Slideshow: 24 | author: str 25 | date: str 26 | slides: list[Slide] 27 | title: str 28 | 29 | 30 | @serde 31 | class Data: 32 | slideshow: Slideshow 33 | 34 | 35 | def main() -> None: 36 | text = r"""{ 37 | "slideshow": { 38 | "author": "Yours Truly", 39 | "date": "date of publication", 40 | "slides": [ 41 | { 42 | "title": "Wake up to WonderWidgets!", 43 | "type": "all" 44 | }, 45 | { 46 | "items": [ 47 | "Why WonderWidgets are great", 48 | "Who buys WonderWidgets" 49 | ], 50 | "title": "Overview", 51 | "type": "all" 52 | } 53 | ], 54 | "title": "Sample Slide Show" 55 | } 56 | } 57 | """ 58 | data = from_json(Data, text) 59 | print(data) 60 | 61 | 62 | if __name__ == "__main__": 63 | main() 64 | -------------------------------------------------------------------------------- /examples/kw_only.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | @dataclass(kw_only=True) 9 | class Foo: 10 | i: int 11 | s: str 12 | f: float 13 | b: bool 14 | 15 | 16 | def main() -> None: 17 | f = Foo(i=10, s="foo", f=100.0, b=True) 18 | print(f"Into Json: {to_json(f)}") 19 | 20 | s = '{"i": 10, "s": "foo", "f": 100.0, "b": true}' 21 | print(f"From Json: {from_json(Foo, s)}") 22 | 23 | 24 | if __name__ == "__main__": 25 | main() 26 | -------------------------------------------------------------------------------- /examples/lazy_type_evaluation.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Foo: 9 | i: int 10 | s: str 11 | f: float 12 | b: bool 13 | 14 | def foo(self, cls: type[Foo]) -> None: # You can use "Foo" type before it's defined. 15 | print("foo") 16 | 17 | 18 | def main() -> None: 19 | f = Foo(i=10, s="foo", f=100.0, b=True) 20 | print(f"Into Json: {to_json(f)}") 21 | 22 | s = '{"i": 10, "s": "foo", "f": 100.0, "b": true}' 23 | print(f"From Json: {from_json(Foo, s)}") 24 | 25 | f.foo(Foo) 26 | 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /examples/literal.py: -------------------------------------------------------------------------------- 1 | from serde.compat import SerdeError 2 | from typing import Literal 3 | 4 | from serde import serde 5 | from serde.json import from_json, to_json 6 | 7 | 8 | @serde 9 | class Foo: 10 | i: Literal[1, 2] 11 | s: Literal["a", "b"] 12 | 13 | 14 | def main() -> None: 15 | f = Foo(1, "a") 16 | print(f"Into Json: {to_json(f)}") 17 | 18 | s = '{"i": 2, "s": "b"}' 19 | print(f"From Json: {from_json(Foo, s)}") 20 | 21 | t = '{"i": 3, "s": "a"}' 22 | try: 23 | from_json(Foo, t) 24 | except SerdeError as err: 25 | print(f"Cannot parse '{t}' as Foo: {err}") 26 | 27 | u = '{"i": 2, "s": "A"}' 28 | try: 29 | from_json(Foo, u) 30 | except SerdeError as err: 31 | print(f"Cannot parse '{u}' as Foo: {err}") 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /examples/msg_pack.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.msgpack import from_msgpack, to_msgpack 3 | 4 | 5 | @serde 6 | class Foo: 7 | i: int 8 | s: str 9 | f: float 10 | b: bool 11 | 12 | 13 | def main() -> None: 14 | f = Foo(i=10, s="foo", f=100.0, b=True) 15 | p = to_msgpack(f) 16 | print("Into MsgPack:", p) 17 | print(f"From MsgPack: {from_msgpack(Foo, p)}") 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /examples/nested.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde 6 | class Bar: 7 | b: int 8 | 9 | 10 | @serde 11 | class Foo: 12 | a: Bar 13 | 14 | 15 | def main() -> None: 16 | f = Foo(Bar(10)) 17 | print(f"Into Json: {to_json(f)}") 18 | 19 | s = '{"a": {"b": 10}}' 20 | print(f"From Json: {from_json(Foo, s)}") 21 | 22 | 23 | if __name__ == "__main__": 24 | main() 25 | -------------------------------------------------------------------------------- /examples/newtype.py: -------------------------------------------------------------------------------- 1 | from typing import NewType 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | UserId = NewType("UserId", int) 7 | 8 | 9 | @serde 10 | class Foo: 11 | id: UserId 12 | 13 | 14 | def main() -> None: 15 | f = Foo(id=UserId(10)) 16 | print(f"Into Json: {to_json(f)}") 17 | 18 | s = '{"id": 10}' 19 | print(f"From Json: {from_json(Foo, s)}") 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /examples/pep681.py: -------------------------------------------------------------------------------- 1 | # mypy: ignore-errors 2 | from serde import serde 3 | from serde.json import from_json, to_json 4 | 5 | 6 | # Thanks to PEP681 @dataclass_transform, @dataclass decorator is no longer mandatory 7 | # if you use PEP681 supported type check such as pyright. If you are a mypy user, 8 | # you still need @dataclass decorator. 9 | @serde 10 | class Foo: 11 | i: int 12 | s: str 13 | f: float 14 | b: bool 15 | 16 | 17 | def main() -> None: 18 | f = Foo(10, "foo", 100.0, True) 19 | print(f"Into Json: {to_json(f)}") 20 | 21 | s = '{"i": 10, "s": "foo", "f": 100.0, "b": true}' 22 | print(f"From Json: {from_json(Foo, s)}") 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /examples/plain_dataclass.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from serde.json import from_json, to_json 4 | 5 | 6 | # Works without @serde since v0.10.0 7 | @dataclass 8 | class Foo: 9 | i: int 10 | s: str 11 | f: float 12 | b: bool 13 | 14 | 15 | def main() -> None: 16 | f = Foo(i=10, s="foo", f=100.0, b=True) 17 | print(f"Into Json: {to_json(f)}") 18 | 19 | s = '{"i": 10, "s": "foo", "f": 100.0, "b": true}' 20 | print(f"From Json: {from_json(Foo, s)}") 21 | 22 | 23 | if __name__ == "__main__": 24 | main() 25 | -------------------------------------------------------------------------------- /examples/plain_dataclass_class_attribute.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | # Suppose this is a dataclass defined in an external library 8 | # To (de)serialize this dataclass with a class attribute, 9 | # create a new class `Wrapper` to extend `External` then add the 10 | # class attribute to `Wrapper`. 11 | @dataclass 12 | class External: 13 | some_value: int 14 | 15 | 16 | @serde(rename_all="kebabcase") 17 | @dataclass 18 | class Wrapper(External): 19 | pass 20 | 21 | 22 | def main() -> None: 23 | f = Wrapper(some_value=10) 24 | print(f"Into Json: {to_json(f)}") 25 | 26 | s = '{"some-value": 10}' 27 | print(f"From Json: {from_json(Wrapper, s)}") 28 | 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /examples/primitive_subclass.py: -------------------------------------------------------------------------------- 1 | from serde.json import from_json, to_json 2 | from serde import field, serde 3 | 4 | 5 | class Id(str): 6 | def __str__(self) -> str: 7 | return "ID " + self 8 | 9 | 10 | @serde 11 | class Foo: 12 | a: Id = field(default_factory=Id) 13 | b: dict[Id, float] = field(default_factory=dict) 14 | c: list[Id] = field(default_factory=list) 15 | 16 | 17 | def main() -> None: 18 | f = Foo(Id("a"), {Id("b"): 1.0}, [Id("c")]) 19 | print(f) 20 | print(type(f.a)) 21 | print(type(list(f.b.keys())[0])) 22 | print(type(f.c[0])) 23 | 24 | d = to_json(f) 25 | print(d) 26 | 27 | ff = from_json(Foo, d) 28 | print(ff) 29 | print(type(ff.a)) 30 | print(type(list(ff.b.keys())[0])) 31 | print(type(ff.c[0])) 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /examples/python_pickle.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.pickle import from_pickle, to_pickle 3 | 4 | 5 | @serde 6 | class Foo: 7 | i: int 8 | s: str 9 | f: float 10 | b: bool 11 | 12 | 13 | def main() -> None: 14 | f = Foo(i=10, s="foo", f=100.0, b=True) 15 | bin = to_pickle(f) 16 | print("Into Pickle: ", bin) 17 | print(f"From Pickle: {from_pickle(Foo, bin)}") 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /examples/recursive.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from dataclasses import dataclass 3 | 4 | from serde import from_dict, serde, to_dict 5 | 6 | 7 | @dataclass 8 | class Recur: 9 | f: Recur | None 10 | 11 | 12 | serde(Recur) 13 | 14 | 15 | def main() -> None: 16 | f = Recur(Recur(Recur(None))) 17 | print(to_dict(f)) 18 | print(from_dict(Recur, {"f": {"f": {"f": None}}})) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /examples/recursive_list.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from dataclasses import dataclass 3 | 4 | from serde import serde 5 | from serde.json import from_json, to_json 6 | 7 | 8 | @dataclass 9 | class Node: 10 | name: str 11 | children: list[Node] 12 | 13 | 14 | serde(Node) 15 | 16 | 17 | def main() -> None: 18 | n = Node("a", [Node("b", [Node("c", [])])]) 19 | s = to_json(n) 20 | print(f"Into Json: {s}") 21 | print(f"From Json: {from_json(Node, s)}") 22 | 23 | 24 | if __name__ == "__main__": 25 | main() 26 | -------------------------------------------------------------------------------- /examples/recursive_union.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from serde import serde, to_dict, InternalTagging, from_dict 3 | from dataclasses import dataclass 4 | 5 | 6 | @serde(tagging=InternalTagging("type")) 7 | @dataclass 8 | class Leaf: 9 | value: int 10 | 11 | 12 | @dataclass 13 | class Node: 14 | name: str 15 | children: list[Leaf | Node] 16 | 17 | 18 | serde(Node, tagging=InternalTagging("type")) 19 | 20 | 21 | def main() -> None: 22 | node1 = Node("node1", [Leaf(10)]) 23 | node2 = Node("node2", [node1]) 24 | d = to_dict(node2) 25 | print(f"Into dict: {d}") 26 | node = from_dict(Node, d) 27 | print(f"From dict: {node}") 28 | 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /examples/rename.py: -------------------------------------------------------------------------------- 1 | """ 2 | rename.py 3 | 4 | 5 | Usage: 6 | $ poetry install 7 | $ poetry run python rename.py 8 | """ 9 | 10 | from serde import field, serde 11 | from serde.json import to_json 12 | 13 | 14 | @serde 15 | class Foo: 16 | # Use 'class_name' because 'class' is a keyword. 17 | class_name: str = field(rename="class") 18 | 19 | 20 | def main() -> None: 21 | print(to_json(Foo(class_name="Foo"))) 22 | 23 | 24 | if __name__ == "__main__": 25 | main() 26 | -------------------------------------------------------------------------------- /examples/rename_all.py: -------------------------------------------------------------------------------- 1 | """ 2 | rename_all.py 3 | 4 | 5 | Usage: 6 | $ poetry install 7 | $ poetry run python rename_all.py 8 | """ 9 | 10 | from serde import serde 11 | from serde.json import from_json, to_json 12 | 13 | 14 | @serde(rename_all="pascalcase") 15 | class Foo: 16 | name: str 17 | no: int | None = None 18 | 19 | 20 | def main() -> None: 21 | f = Foo("Pikachu") 22 | print(f"Into Json: {to_json(f)}") 23 | 24 | s = '{"Name": "Pikachu", "No": 25}' 25 | print(f"From Json: {from_json(Foo, s)}") 26 | 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /examples/runner.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import typing 3 | 4 | 5 | if sys.version_info[:3] < (3, 12, 0): 6 | print("examples require at least Python 3.12") 7 | sys.exit(1) 8 | 9 | 10 | def run_all() -> None: 11 | import alias 12 | import any 13 | import class_var 14 | import primitive_subclass 15 | import collection 16 | import custom_class_serializer 17 | import custom_legacy_class_serializer 18 | import custom_field_serializer 19 | import default 20 | import default_dict 21 | import env 22 | import flatten 23 | import forward_reference 24 | import frozen_set 25 | import generics 26 | import generics_pep695 27 | import generics_nested 28 | import nested 29 | import init_var 30 | import jsonfile 31 | import lazy_type_evaluation 32 | import literal 33 | import msg_pack 34 | import newtype 35 | import pep681 36 | import plain_dataclass 37 | import plain_dataclass_class_attribute 38 | import deny_unknown_fields 39 | import python_pickle 40 | import recursive 41 | import recursive_list 42 | import recursive_union 43 | import rename 44 | import rename_all 45 | import simple 46 | import skip 47 | import tomlfile 48 | import type_check_coerce 49 | import type_check_disabled 50 | import type_datetime 51 | import type_decimal 52 | import type_ipaddress 53 | import type_numpy 54 | import type_pathlib 55 | import type_uuid 56 | import type_alias_pep695 57 | import union 58 | import union_tagging 59 | import union_directly 60 | import user_exception 61 | import variable_length_tuple 62 | import yamlfile 63 | import enum34 64 | import kw_only 65 | 66 | run(any) 67 | run(simple) 68 | run(enum34) 69 | run(frozen_set) 70 | run(newtype) 71 | run(collection) 72 | run(default) 73 | run(default_dict) 74 | run(env) 75 | run(flatten) 76 | run(jsonfile) 77 | run(rename) 78 | run(rename_all) 79 | run(skip) 80 | run(tomlfile) 81 | run(yamlfile) 82 | run(union) 83 | run(custom_class_serializer) 84 | run(custom_legacy_class_serializer) 85 | run(custom_field_serializer) 86 | run(forward_reference) 87 | run(type_decimal) 88 | run(type_datetime) 89 | run(union_tagging) 90 | run(union_directly) 91 | run(generics) 92 | run(type_alias_pep695) 93 | run(generics_pep695) 94 | run(generics_nested) 95 | run(nested) 96 | run(lazy_type_evaluation) 97 | run(literal) 98 | run(type_check_coerce) 99 | run(type_check_disabled) 100 | run(user_exception) 101 | run(pep681) 102 | run(variable_length_tuple) 103 | run(init_var) 104 | run(python_pickle) 105 | run(class_var) 106 | run(alias) 107 | run(recursive) 108 | run(recursive_list) 109 | run(recursive_union) 110 | run(class_var) 111 | run(plain_dataclass) 112 | run(plain_dataclass_class_attribute) 113 | run(deny_unknown_fields) 114 | run(msg_pack) 115 | run(primitive_subclass) 116 | run(kw_only) 117 | run(type_pathlib) 118 | run(type_ipaddress) 119 | run(type_uuid) 120 | run(type_numpy) 121 | 122 | try: 123 | import type_sqlalchemy 124 | 125 | run(type_sqlalchemy) 126 | except ImportError: 127 | pass 128 | 129 | 130 | def run(module: typing.Any) -> None: 131 | print("-----------------") 132 | print(f"running {module.__name__}") 133 | module.main() 134 | 135 | 136 | if __name__ == "__main__": 137 | try: 138 | run_all() 139 | print("-----------------") 140 | print("all examples completed successfully!") 141 | except Exception: 142 | sys.exit(1) 143 | -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde 6 | class Foo: 7 | i: int 8 | s: str 9 | f: float 10 | b: bool 11 | 12 | 13 | def main() -> None: 14 | f = Foo(i=10, s="foo", f=100.0, b=True) 15 | print(f"Into Json: {to_json(f)}") 16 | 17 | s = '{"i": 10, "s": "foo", "f": 100.0, "b": true}' 18 | print(f"From Json: {from_json(Foo, s)}") 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /examples/skip.py: -------------------------------------------------------------------------------- 1 | """ 2 | skip.py 3 | 4 | Example usage of skip and skip_if attributes. 5 | 6 | Usage: 7 | $ poetry install 8 | $ poetry run python skip.py 9 | """ 10 | 11 | from serde import field, serde 12 | from serde.json import to_json 13 | 14 | 15 | @serde 16 | class Resource: 17 | name: str 18 | hash: str 19 | metadata: dict[str, str] = field(default_factory=dict, skip=True) 20 | 21 | 22 | @serde 23 | class World: 24 | player: str 25 | enemies: list[str] = field(default_factory=list, skip_if_false=True) 26 | buddy: str = field(default="", skip_if=lambda v: v == "Pikachu") 27 | town: str = field(default="Masara Town", skip_if_default=True) 28 | 29 | 30 | def main() -> None: 31 | resources = [ 32 | Resource("Stack Overflow", "b6469c3f31653d281bbbfa6f94d60fea130abe38"), 33 | Resource( 34 | "GitHub", 35 | "5cb7a0c47e53854cd00e1a968de5abce1c124601", 36 | metadata={"headquarters": "San Francisco"}, 37 | ), 38 | ] 39 | print(to_json(resources)) 40 | 41 | # "buddy" and "town" field will be omitted 42 | world = World("satoshi", ["Rattata", "Pidgey"], "Pikachu") 43 | print(to_json(world)) 44 | 45 | # "enemies" field will be omitted 46 | world = World("green", [], "Charmander", "Black City") 47 | print(to_json(world)) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /examples/swagger.yml: -------------------------------------------------------------------------------- 1 | swagger: 2 2 | info: 3 | title: Echo API 4 | description: Simple Rest Echo 5 | version: "1.0.0" 6 | host: "localhost:8002" 7 | schemes: 8 | - http 9 | basePath: /v1 10 | produces: 11 | - application/json 12 | paths: 13 | /echo: 14 | get: 15 | description: "Returns the 'message' to the caller" 16 | operationId: "echo" 17 | parameters: 18 | - name: headerParam 19 | in: header 20 | type: string 21 | required: false 22 | - name: message 23 | # in: query 24 | type: string 25 | required: true 26 | responses: 27 | 200: 28 | description: "Success" 29 | default: 30 | description: "Error" 31 | definitions: 32 | EchoResponse: 33 | required: 34 | - message 35 | properties: 36 | message: 37 | type: string 38 | Error: 39 | properties: 40 | code: 41 | type: integer 42 | format: int32 43 | message: 44 | type: string 45 | fields: 46 | type: string 47 | -------------------------------------------------------------------------------- /examples/tomlfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | tomlfile.py 3 | 4 | Read Pipenv Pipfile by pyserde. 5 | 6 | Usage: 7 | $ poetry install 8 | $ poetry run python tomlfile.py 9 | """ 10 | 11 | from pathlib import Path 12 | 13 | from serde import Untagged, serde 14 | from serde.toml import from_toml 15 | 16 | basedir = Path(__file__).parent 17 | 18 | 19 | @serde 20 | class Source: 21 | url: str 22 | verify_ssl: bool 23 | name: str 24 | 25 | 26 | @serde 27 | class Requires: 28 | python_version: str 29 | 30 | 31 | @serde 32 | class Package: 33 | path: str | None = None 34 | version: str | None = None 35 | editable: bool | None = False 36 | 37 | 38 | @serde(tagging=Untagged) 39 | class Pipfile: 40 | source: list[Source] 41 | requires: Requires | None 42 | packages: dict[str, str | Package] 43 | 44 | 45 | def main() -> None: 46 | with open(basedir / "Pipfile") as f: 47 | toml = f.read() 48 | pip = from_toml(Pipfile, toml) 49 | print(pip) 50 | 51 | 52 | if __name__ == "__main__": 53 | main() 54 | -------------------------------------------------------------------------------- /examples/type_alias_pep695.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde 6 | class Bar: 7 | a: int 8 | 9 | 10 | @serde 11 | class Baz: 12 | b: int 13 | 14 | 15 | # BarBaz = Bar | Baz 16 | type BarBaz = Bar | Baz 17 | 18 | 19 | @serde 20 | class Foo: 21 | barbaz: BarBaz 22 | 23 | 24 | def main() -> None: 25 | f = Foo(Baz(10)) 26 | s = to_json(f) 27 | print(f"Into Json: {s}") 28 | ff = from_json(Foo, s) 29 | print(f"From Json: {ff}") 30 | 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /examples/type_check_coerce.py: -------------------------------------------------------------------------------- 1 | from serde import coerce, serde 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde(type_check=coerce) 6 | class Bar: 7 | e: int 8 | 9 | 10 | @serde(type_check=coerce) 11 | class Foo: 12 | a: int 13 | b: list[int] 14 | c: list[dict[str, int]] 15 | d: Bar | None = None 16 | 17 | 18 | def main() -> None: 19 | f = Foo(a="1", b=[True], c=[{10: 1.0}]) # type: ignore 20 | print(f"Into Json: {to_json(f)}") 21 | 22 | s = '{"a": "1", "b": [false], "c": [{"10": 1.0}], "d": {"e": "100"}}' 23 | print(f"From Json: {from_json(Foo, s)}") 24 | 25 | 26 | if __name__ == "__main__": 27 | main() 28 | -------------------------------------------------------------------------------- /examples/type_check_disabled.py: -------------------------------------------------------------------------------- 1 | from serde import disabled, serde 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde(type_check=disabled) 6 | class Foo: 7 | a: int 8 | b: list[int] 9 | c: list[dict[str, int]] 10 | 11 | 12 | def main() -> None: 13 | # Foo is instantiated with wrong types but serde won't complain 14 | f = Foo(a=1.0, b=[1.0], c=[{"k": 1.0}]) # type: ignore 15 | print(f"Into Json: {to_json(f)}") 16 | 17 | # Also, JSON contains wrong types of values but serde won't complain 18 | s = '{"a": 1, "b": [1], "c": [{"k": 1.0}]}' 19 | print(f"From Json: {from_json(Foo, s)}") 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /examples/type_datetime.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Foo: 9 | d: datetime.date 10 | t: datetime.time 11 | dt: datetime.datetime 12 | 13 | 14 | def main() -> None: 15 | dt = datetime.datetime(2021, 1, 1, 0, 0, 0) 16 | 17 | foo = Foo(dt.date(), dt.time(), dt) 18 | print(f"Into Json: {to_json(foo)}") 19 | 20 | s = '{"d": "2021-01-01", "t": "00:00:00", "dt": "2021-01-01T00:00:00"}' 21 | print(f"From Json: {from_json(Foo, s)}") 22 | 23 | 24 | if __name__ == "__main__": 25 | main() 26 | -------------------------------------------------------------------------------- /examples/type_decimal.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Foo: 9 | v: Decimal 10 | 11 | 12 | def main() -> None: 13 | foo = Foo(Decimal(0.1)) 14 | print(f"Into Json: {to_json(foo)}") 15 | 16 | s = '{"v": "0.1000000000000000055511151231257827021181583404541015625"}' 17 | print(f"From Json: {from_json(Foo, s)}") 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /examples/type_ipaddress.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Foo: 9 | v4: ipaddress.IPv4Address 10 | v6: ipaddress.IPv6Address 11 | v4n: ipaddress.IPv4Network 12 | v6n: ipaddress.IPv6Network 13 | 14 | 15 | def main() -> None: 16 | foo = Foo( 17 | v4=ipaddress.IPv4Address("192.168.0.1"), 18 | v6=ipaddress.IPv6Address("2001:db8::"), 19 | v4n=ipaddress.IPv4Network("192.168.0.0/28"), 20 | v6n=ipaddress.IPv6Network("2001:db8::/32"), 21 | ) 22 | print(f"Into Json: {to_json(foo)}") 23 | 24 | s = '{"v4": "192.168.0.1", "v6": "2001:db8::", "v4n": "192.168.0.0/28", "v6n": "2001:db8::/32"}' 25 | print(f"From Json: {from_json(Foo, s)}") 26 | 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /examples/type_numpy.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Foo: 9 | bo: numpy.bool_ 10 | by: numpy.byte 11 | in_: numpy.int_ 12 | inc: numpy.intc 13 | ui: numpy.uint 14 | fl: numpy.float64 15 | st: numpy.str_ 16 | nd: numpy.typing.NDArray[numpy.int_] 17 | ha: numpy.half 18 | da: numpy.datetime64 19 | 20 | 21 | def main() -> None: 22 | foo = Foo( 23 | bo=numpy.bool_(True), 24 | by=numpy.byte(38), 25 | in_=numpy.int_(42), 26 | inc=numpy.intc(42), 27 | ui=numpy.uint(42), 28 | fl=numpy.float64(3.14), 29 | st=numpy.str_("numpy str"), 30 | nd=numpy.array([1, 2, 3]), 31 | ha=numpy.half(3.14), 32 | da=numpy.datetime64("2020-01-01T03:30:00.162"), 33 | ) 34 | print(f"Into Json: {to_json(foo)}") 35 | 36 | s = """{ 37 | "bo": true, 38 | "by": 38, 39 | "in_": 42, 40 | "inc": 42, 41 | "ui": 42, 42 | "fl": 3.14, 43 | "st": "numpy str", 44 | "nd": [1, 2, 3], 45 | "ha": 3.14, 46 | "da": "2020-01-01T03:30:00.162" 47 | }""" 48 | print(f"From Json: {from_json(Foo, s)}") 49 | 50 | 51 | if __name__ == "__main__": 52 | main() 53 | -------------------------------------------------------------------------------- /examples/type_numpy_jaxtyping.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from jaxtyping import ( 3 | Float, 4 | Float16, 5 | Float32, 6 | Float64, 7 | Inexact, 8 | Int, 9 | Int8, 10 | Int16, 11 | Int32, 12 | Int64, 13 | Integer, 14 | UInt, 15 | UInt8, 16 | UInt16, 17 | UInt32, 18 | UInt64, 19 | ) 20 | from serde import serde 21 | from serde.json import from_json, to_json 22 | 23 | 24 | @serde 25 | class Foo: 26 | float_: Float[numpy.ndarray, "3 3"] 27 | float16: Float16[numpy.ndarray, "3 3"] 28 | float32: Float32[numpy.ndarray, "3 3"] 29 | float64: Float64[numpy.ndarray, "3 3"] 30 | inexact: Inexact[numpy.ndarray, "3 3"] 31 | int_: Int[numpy.ndarray, "3 3"] 32 | int8: Int8[numpy.ndarray, "3 3"] 33 | int16: Int16[numpy.ndarray, "3 3"] 34 | int32: Int32[numpy.ndarray, "3 3"] 35 | int64: Int64[numpy.ndarray, "3 3"] 36 | integer: Integer[numpy.ndarray, "3 3"] 37 | uint: UInt[numpy.ndarray, "3 3"] 38 | uint8: UInt8[numpy.ndarray, "3 3"] 39 | uint16: UInt16[numpy.ndarray, "3 3"] 40 | uint32: UInt32[numpy.ndarray, "3 3"] 41 | uint64: UInt64[numpy.ndarray, "3 3"] 42 | 43 | 44 | def main() -> None: 45 | foo = Foo( 46 | float_=numpy.zeros((3, 3), dtype=float), 47 | float16=numpy.zeros((3, 3), dtype=numpy.float16), 48 | float32=numpy.zeros((3, 3), dtype=numpy.float32), 49 | float64=numpy.zeros((3, 3), dtype=numpy.float64), 50 | inexact=numpy.zeros((3, 3), dtype=numpy.inexact), 51 | int_=numpy.zeros((3, 3), dtype=int), 52 | int8=numpy.zeros((3, 3), dtype=numpy.int8), 53 | int16=numpy.zeros((3, 3), dtype=numpy.int16), 54 | int32=numpy.zeros((3, 3), dtype=numpy.int32), 55 | int64=numpy.zeros((3, 3), dtype=numpy.int64), 56 | integer=numpy.zeros((3, 3), dtype=numpy.integer), 57 | uint=numpy.zeros((3, 3), dtype=numpy.uint), 58 | uint8=numpy.zeros((3, 3), dtype=numpy.uint8), 59 | uint16=numpy.zeros((3, 3), dtype=numpy.uint16), 60 | uint32=numpy.zeros((3, 3), dtype=numpy.uint32), 61 | uint64=numpy.zeros((3, 3), dtype=numpy.uint64), 62 | ) 63 | 64 | print(f"Into Json: {to_json(foo)}") 65 | 66 | s = """ 67 | { 68 | "float_": [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], 69 | "float16": [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], 70 | "float32": [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], 71 | "float64": [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], 72 | "inexact": [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], 73 | "int_": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 74 | "int8": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 75 | "int16": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 76 | "int32": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 77 | "int64": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 78 | "integer": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 79 | "uint": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 80 | "uint8": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 81 | "uint16": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 82 | "uint32": [[0, 0, 0], [0, 0, 0], [0, 0, 0]], 83 | "uint64": [[0, 0, 0], [0, 0, 0], [0, 0, 0]] 84 | } 85 | """ 86 | print(f"From Json: {from_json(Foo, s)}") 87 | 88 | 89 | if __name__ == "__main__": 90 | main() 91 | -------------------------------------------------------------------------------- /examples/type_pathlib.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Foo: 9 | p: pathlib.Path 10 | 11 | 12 | def main() -> None: 13 | foo = Foo(pathlib.Path("foo/bar/baz.txt")) 14 | print(f"Into Json: {to_json(foo)}") 15 | 16 | s = '{"p": "foo/bar/baz.txt"}' 17 | print(f"From Json: {from_json(Foo, s)}") 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /examples/type_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from sqlalchemy import Text, JSON, ForeignKey 4 | from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass, Mapped, mapped_column, relationship 5 | 6 | from serde import serde 7 | from serde.json import from_json, to_json 8 | 9 | 10 | class Base(MappedAsDataclass, DeclarativeBase): # type: ignore[misc] 11 | pass 12 | 13 | 14 | @serde 15 | class Project(Base): 16 | __tablename__ = "projects" 17 | id: Mapped[int] = mapped_column(primary_key=True) 18 | name: Mapped[str] = mapped_column(Text, nullable=False) 19 | owner_id: Mapped[int] = mapped_column(ForeignKey("users.id")) 20 | 21 | 22 | @serde 23 | class User(Base): 24 | __tablename__ = "users" 25 | 26 | id: Mapped[int] = mapped_column(primary_key=True) 27 | name: Mapped[str] = mapped_column(Text, nullable=False) 28 | fullname: Mapped[str] = mapped_column(Text, nullable=False) 29 | nickname: Mapped[Optional[str]] = mapped_column(Text) 30 | attributes: Mapped[Optional[dict[str, str]]] = mapped_column(JSON) 31 | projects: Mapped[list[Project]] = relationship(backref="owner") 32 | 33 | 34 | def main() -> None: 35 | user = User( 36 | id=1, 37 | name="john", 38 | fullname="John Doe", 39 | nickname=None, 40 | attributes={"color": "green"}, 41 | projects=[Project(id=1, name="Dummy", owner_id=1)], 42 | ) 43 | print(f"Into Json: {to_json(user)}") 44 | 45 | s = """{ 46 | "id": 1, 47 | "name": "john", 48 | "fullname": "John Doe", 49 | "nickname": null, 50 | "attributes": { 51 | "color": "green" 52 | }, 53 | "projects": [ 54 | { 55 | "id": 1, 56 | "name": "Dummy", 57 | "owner_id": 1 58 | } 59 | ] 60 | }""" 61 | print(f"From Json: {from_json(User, s)}") 62 | 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /examples/type_uuid.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from serde import serde 4 | from serde.json import from_json, to_json 5 | 6 | 7 | @serde 8 | class Foo: 9 | v1: uuid.UUID 10 | v3: uuid.UUID 11 | v4: uuid.UUID 12 | v5: uuid.UUID 13 | 14 | 15 | def main() -> None: 16 | foo = Foo( 17 | v1=uuid.uuid1(), 18 | v3=uuid.uuid3(uuid.NAMESPACE_DNS, "pyserde.org"), 19 | v4=uuid.uuid4(), 20 | v5=uuid.uuid5(uuid.NAMESPACE_DNS, "pyserde.org"), 21 | ) 22 | print(f"Into Json: {to_json(foo)}") 23 | 24 | s = """{ 25 | "v1": "a8098c1a-f86e-11da-bd1a-00112444be1e", 26 | "v3": "6fa459ea-ee8a-3ca4-894e-db77e160355e", 27 | "v4": "916b3609-0015-41be-be4c-53b74cab0d03", 28 | "v5": "886313e1-3b8a-5372-9b90-0c9aee199e5d" 29 | }""" 30 | print(f"From Json: {from_json(Foo, s)}") 31 | 32 | 33 | if __name__ == "__main__": 34 | main() 35 | -------------------------------------------------------------------------------- /examples/union.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde 6 | class Bar: 7 | v: int 8 | 9 | 10 | @serde 11 | class Baz: 12 | v: float 13 | 14 | 15 | @serde 16 | class Foo: 17 | a: int | str 18 | b: dict[str, int] | list[int] 19 | c: Bar | Baz 20 | 21 | 22 | def main() -> None: 23 | f = Foo(10, [1, 2, 3], Bar(10)) 24 | print(f"Into Json: {to_json(f)}") 25 | 26 | s = '{"a": 10, "b": [1, 2, 3], "c": {"Bar": {"v": 10}}}' 27 | print(f"From Json: {from_json(Foo, s)}") 28 | 29 | f = Foo("foo", {"bar": 1, "baz": 2}, Baz(100.0)) 30 | print(f"Into Json: {to_json(f)}") 31 | 32 | s = '{"a": "foo", "b": {"bar": 1, "baz": 2}, "c": {"Baz": {"v": 100.0}}}' 33 | print(f"From Json: {from_json(Foo, s)}") 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /examples/union_directly.py: -------------------------------------------------------------------------------- 1 | from serde import serde, Untagged, AdjacentTagging, InternalTagging 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde 6 | class Bar: 7 | b: int 8 | 9 | 10 | @serde 11 | class Baz: 12 | b: int 13 | 14 | 15 | def main() -> None: 16 | baz = Baz(10) 17 | 18 | print("# external tagging (default)") 19 | a = to_json(baz, cls=Bar | Baz) 20 | print("IntoJSON", a) 21 | print("FromJSON", from_json(Bar | Baz, a)) 22 | 23 | print("# internal tagging") 24 | a = to_json(baz, cls=InternalTagging("type", Bar | Baz)) 25 | print("IntoJSON", a) 26 | print("FromJSON", from_json(InternalTagging("type", Bar | Baz), a)) 27 | 28 | print("# adjacent tagging") 29 | a = to_json(baz, cls=AdjacentTagging("type", "content", Bar | Baz)) 30 | print("IntoJSON", a) 31 | print("FromJSON", from_json(AdjacentTagging("type", "content", Bar | Baz), a)) 32 | 33 | print("# untagged") 34 | a = to_json(baz, cls=Untagged(Bar | Baz)) 35 | print("IntoJSON", a) 36 | print("FromJSON", from_json(Untagged(Bar | Baz), a)) 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /examples/union_tagging.py: -------------------------------------------------------------------------------- 1 | from serde import AdjacentTagging, InternalTagging, Untagged, serde 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde 6 | class Bar: 7 | b: int 8 | 9 | 10 | @serde 11 | class Baz: 12 | b: int 13 | 14 | 15 | def external_tagging() -> None: 16 | @serde 17 | class Foo: 18 | a: Bar | Baz 19 | 20 | f = Foo(Baz(10)) 21 | print(f"Into Json: {to_json(f)}") 22 | 23 | s = '{"a": {"Baz": {"b": 10}}}' 24 | print(f"From Json: {from_json(Foo, s)}") 25 | 26 | 27 | def internal_tagging() -> None: 28 | @serde(tagging=InternalTagging("type")) 29 | class Foo: 30 | a: Bar | Baz 31 | 32 | f = Foo(Baz(10)) 33 | print(f"Into Json: {to_json(f)}") 34 | 35 | s = '{"a": {"type": "Baz", "b": 10}}' 36 | print(f"From Json: {from_json(Foo, s)}") 37 | 38 | 39 | def adjacent_tagging() -> None: 40 | @serde(tagging=AdjacentTagging(tag="type", content="content")) 41 | class Foo: 42 | a: Bar | Baz 43 | 44 | f = Foo(Baz(10)) 45 | print(f"Into Json: {to_json(f)}") 46 | 47 | s = '{"a": {"type": "Baz", "content": {"b": 10}}}' 48 | print(f"From Json: {from_json(Foo, s)}") 49 | 50 | 51 | def untagged() -> None: 52 | @serde(tagging=Untagged) 53 | class Foo: 54 | a: Bar | Baz 55 | 56 | f = Foo(Baz(10)) 57 | print(f"Into Json: {to_json(f)}") 58 | 59 | # Untagged cannot correctly deserialize unions with the same fields. 60 | s = '{"a": {"b": 10}}' 61 | print(f"From Json: {from_json(Foo, s)}") 62 | 63 | 64 | def main() -> None: 65 | print("# external tagging (default)") 66 | external_tagging() 67 | print("# internal tagging") 68 | internal_tagging() 69 | print("# adjacent tagging") 70 | adjacent_tagging() 71 | print("# untagged") 72 | untagged() 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /examples/user_exception.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.json import from_json 3 | 4 | 5 | class MyException(Exception): 6 | pass 7 | 8 | 9 | @serde 10 | class Foo: 11 | v: int 12 | 13 | def __post_init__(self) -> None: 14 | if self.v == 10: 15 | raise MyException("Invalid value") 16 | 17 | 18 | def main() -> None: 19 | try: 20 | s = '{"v": 10}' 21 | print(f"From Json: {from_json(Foo, s)}") 22 | 23 | except MyException as e: 24 | # Any exception from __post_init__ won't be wrapped by SerdeError. 25 | print(f"Got user exception: {e}") 26 | 27 | 28 | if __name__ == "__main__": 29 | main() 30 | -------------------------------------------------------------------------------- /examples/variable_length_tuple.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.json import from_json, to_json 3 | 4 | 5 | @serde 6 | class Foo: 7 | v: tuple[int, ...] 8 | 9 | 10 | def main() -> None: 11 | f = Foo(v=(1, 2, 3)) 12 | print(f"Into Json: {to_json(f)}") 13 | 14 | s = '{"v": [1, 2, 3]}' 15 | print(f"From Json: {from_json(Foo, s)}") 16 | 17 | 18 | if __name__ == "__main__": 19 | main() 20 | -------------------------------------------------------------------------------- /examples/yamlfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | yamlfile.py 3 | 4 | Read swagger echo example yaml. 5 | 6 | Usage: 7 | $ poetry install 8 | $ poetry run python yamlfile.py 9 | """ 10 | 11 | import pathlib 12 | 13 | from serde import Untagged, serde 14 | from serde.yaml import from_yaml 15 | 16 | basedir = pathlib.Path(__file__).parent 17 | 18 | 19 | @serde(rename_all="camelcase") 20 | class Info: 21 | title: str 22 | description: str 23 | version: str 24 | 25 | 26 | @serde(rename_all="camelcase") 27 | class Parameter: 28 | name: str 29 | # not yet supported. 30 | # infield: str = field(rename='in') 31 | type: str 32 | required: bool 33 | 34 | 35 | @serde(rename_all="camelcase") 36 | class Response: 37 | description: str 38 | 39 | 40 | @serde(rename_all="camelcase", tagging=Untagged) 41 | class Path: 42 | description: str 43 | operation_id: str 44 | parameters: list[str | Parameter] 45 | responses: dict[str | int, Response] 46 | 47 | 48 | @serde(rename_all="camelcase") 49 | class Prop: 50 | type: str 51 | format: str | None 52 | 53 | 54 | @serde(rename_all="camelcase") 55 | class Definition: 56 | required: list[str] | None 57 | properties: dict[str, Prop] 58 | 59 | 60 | @serde(rename_all="camelcase") 61 | class Swagger: 62 | swagger: int 63 | info: Info 64 | host: str 65 | schemes: list[str] 66 | base_path = str 67 | produces: list[str] 68 | paths: dict[str, dict[str, Path]] 69 | definitions: dict[str, Definition] 70 | 71 | 72 | def main() -> None: 73 | with open(basedir / "swagger.yml") as f: 74 | yaml = f.read() 75 | swagger = from_yaml(Swagger, yaml) 76 | print(swagger) 77 | 78 | 79 | if __name__ == "__main__": 80 | main() 81 | -------------------------------------------------------------------------------- /profile_codegen.py: -------------------------------------------------------------------------------- 1 | from serde import serde, serialize, deserialize 2 | from serde.json import from_json, to_json 3 | from dataclasses import dataclass 4 | from beartype import beartype 5 | import cProfile 6 | import pstats 7 | 8 | 9 | @beartype 10 | @dataclass 11 | class FewFields: 12 | a: int 13 | b: float 14 | c: str 15 | d: bool 16 | 17 | 18 | @beartype 19 | @dataclass 20 | class ManyFields: 21 | a1: int 22 | a2: int 23 | a3: int 24 | a4: int 25 | a5: int 26 | a6: int 27 | a7: int 28 | a8: int 29 | a9: int 30 | a10: int 31 | a11: int 32 | a12: int 33 | a13: int 34 | a14: int 35 | a15: int 36 | a16: int 37 | a17: int 38 | a18: int 39 | a19: int 40 | a20: int 41 | b1: int 42 | b2: int 43 | b3: int 44 | b4: int 45 | b5: int 46 | b6: int 47 | b7: int 48 | b8: int 49 | b9: int 50 | b10: int 51 | b11: int 52 | b12: int 53 | b13: int 54 | b14: int 55 | b15: int 56 | b16: int 57 | b17: int 58 | b18: int 59 | b19: int 60 | b20: int 61 | c1: int 62 | c2: int 63 | c3: int 64 | c4: int 65 | c5: int 66 | c6: int 67 | c7: int 68 | c8: int 69 | c9: int 70 | c10: int 71 | c11: int 72 | c12: int 73 | c13: int 74 | c14: int 75 | c15: int 76 | c16: int 77 | c17: int 78 | c18: int 79 | c19: int 80 | c20: int 81 | d1: int 82 | d2: int 83 | d3: int 84 | d4: int 85 | d5: int 86 | d6: int 87 | d7: int 88 | d8: int 89 | d9: int 90 | d10: int 91 | d11: int 92 | d12: int 93 | d13: int 94 | d14: int 95 | d15: int 96 | d16: int 97 | d17: int 98 | d18: int 99 | d19: int 100 | d20: int 101 | 102 | 103 | def profile_few_fields() -> None: 104 | for n in range(100): 105 | serde(FewFields) 106 | 107 | 108 | def profile_many_fields() -> None: 109 | for n in range(100): 110 | serde(ManyFields) 111 | 112 | 113 | cProfile.run("profile_few_fields()", filename="profile_results.prof") 114 | stats = pstats.Stats("profile_results.prof") 115 | stats.sort_stats("tottime").print_stats(20) 116 | 117 | cProfile.run("profile_many_fields()", filename="profile_results.prof") 118 | stats = pstats.Stats("profile_results.prof") 119 | stats.sort_stats("tottime").print_stats(20) 120 | -------------------------------------------------------------------------------- /serde/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. include:: ../README.md 3 | 4 | ## Modules 5 | 6 | The following modules provide the core functionalities of `pyserde`. 7 | * `serde.se`: All about serialization. 8 | * `serde.de`: All about deserialization. 9 | * `serde.core`: Core module used by `serde.se` and `serde.de` modules. 10 | * `serde.compat`: Compatibility layer which handles mostly differences of `typing` module between 11 | python versions. 12 | 13 | The following modules provide pyserde's (de)serialize APIs. 14 | * `serde.json`: Serialize and Deserialize in JSON. 15 | * `serde.msgpack`: Serialize and Deserialize in MsgPack. 16 | * `serde.yaml`: Serialize and Deserialize in YAML. 17 | * `serde.toml`: Serialize and Deserialize in TOML. 18 | * `serde.pickle`: Serialize and Deserialize in Pickle. 19 | 20 | Other modules 21 | * `serde.inspect`: Prints generated code by pyserde. 22 | """ 23 | 24 | from dataclasses import dataclass 25 | from collections.abc import Callable 26 | from typing import Optional, overload, Any, Type 27 | 28 | from typing_extensions import dataclass_transform 29 | 30 | from .compat import SerdeError, SerdeSkip, T 31 | from .core import ( 32 | ClassSerializer, 33 | ClassDeserializer, 34 | AdjacentTagging, 35 | coerce, 36 | DefaultTagging, 37 | ExternalTagging, 38 | InternalTagging, 39 | disabled, 40 | strict, 41 | Tagging, 42 | TypeCheck, 43 | Untagged, 44 | field, 45 | init, 46 | logger, 47 | should_impl_dataclass, 48 | add_serializer, 49 | add_deserializer, 50 | ) 51 | from .de import ( 52 | DeserializeFunc, 53 | default_deserializer, 54 | deserialize, 55 | from_dict, 56 | from_tuple, 57 | is_deserializable, 58 | ) 59 | from .se import ( 60 | SerializeFunc, 61 | asdict, 62 | astuple, 63 | default_serializer, 64 | is_serializable, 65 | serialize, 66 | to_dict, 67 | to_tuple, 68 | ) 69 | 70 | __all__ = [ 71 | "serde", 72 | "serialize", 73 | "deserialize", 74 | "is_serializable", 75 | "is_deserializable", 76 | "to_dict", 77 | "from_dict", 78 | "to_tuple", 79 | "from_tuple", 80 | "SerdeError", 81 | "SerdeSkip", 82 | "AdjacentTagging", 83 | "ExternalTagging", 84 | "InternalTagging", 85 | "Untagged", 86 | "disabled", 87 | "strict", 88 | "coerce", 89 | "field", 90 | "default_deserializer", 91 | "asdict", 92 | "astuple", 93 | "default_serializer", 94 | "compat", 95 | "core", 96 | "de", 97 | "inspect", 98 | "json", 99 | "msgpack", 100 | "numpy", 101 | "se", 102 | "toml", 103 | "pickle", 104 | "yaml", 105 | "init", 106 | "logger", 107 | "ClassSerializer", 108 | "ClassDeserializer", 109 | "add_serializer", 110 | "add_deserializer", 111 | ] 112 | 113 | 114 | @overload 115 | def serde( 116 | _cls: Type[T], 117 | rename_all: Optional[str] = None, 118 | reuse_instances_default: bool = True, 119 | convert_sets_default: bool = False, 120 | serializer: Optional[SerializeFunc] = None, 121 | deserializer: Optional[DeserializeFunc] = None, 122 | tagging: Tagging = DefaultTagging, 123 | type_check: TypeCheck = strict, 124 | serialize_class_var: bool = False, 125 | class_serializer: Optional[ClassSerializer] = None, 126 | class_deserializer: Optional[ClassDeserializer] = None, 127 | deny_unknown_fields: bool = False, 128 | ) -> Type[T]: ... 129 | 130 | 131 | @overload 132 | def serde( 133 | _cls: Any = None, 134 | rename_all: Optional[str] = None, 135 | reuse_instances_default: bool = True, 136 | convert_sets_default: bool = False, 137 | serializer: Optional[SerializeFunc] = None, 138 | deserializer: Optional[DeserializeFunc] = None, 139 | tagging: Tagging = DefaultTagging, 140 | type_check: TypeCheck = strict, 141 | serialize_class_var: bool = False, 142 | class_serializer: Optional[ClassSerializer] = None, 143 | class_deserializer: Optional[ClassDeserializer] = None, 144 | deny_unknown_fields: bool = False, 145 | ) -> Callable[[type[T]], type[T]]: ... 146 | 147 | 148 | @dataclass_transform(field_specifiers=(field,)) 149 | def serde( 150 | _cls: Any = None, 151 | rename_all: Optional[str] = None, 152 | reuse_instances_default: bool = True, 153 | convert_sets_default: bool = False, 154 | serializer: Optional[SerializeFunc] = None, 155 | deserializer: Optional[DeserializeFunc] = None, 156 | tagging: Tagging = DefaultTagging, 157 | type_check: TypeCheck = strict, 158 | serialize_class_var: bool = False, 159 | class_serializer: Optional[ClassSerializer] = None, 160 | class_deserializer: Optional[ClassDeserializer] = None, 161 | deny_unknown_fields: bool = False, 162 | ) -> Any: 163 | """ 164 | serde decorator. Keyword arguments are passed in `serialize` and `deserialize`. 165 | """ 166 | 167 | def wrap(cls: Any) -> Any: 168 | if should_impl_dataclass(cls): 169 | dataclass(cls) 170 | serialize( 171 | cls, 172 | rename_all=rename_all, 173 | reuse_instances_default=reuse_instances_default, 174 | convert_sets_default=convert_sets_default, 175 | serializer=serializer, 176 | deserializer=deserializer, 177 | tagging=tagging, 178 | type_check=type_check, 179 | serialize_class_var=serialize_class_var, 180 | class_serializer=class_serializer, 181 | ) 182 | deserialize( 183 | cls, 184 | rename_all=rename_all, 185 | reuse_instances_default=reuse_instances_default, 186 | convert_sets_default=convert_sets_default, 187 | serializer=serializer, 188 | deserializer=deserializer, 189 | tagging=tagging, 190 | type_check=type_check, 191 | serialize_class_var=serialize_class_var, 192 | class_deserializer=class_deserializer, 193 | deny_unknown_fields=deny_unknown_fields, 194 | ) 195 | return cls 196 | 197 | if _cls is None: 198 | return wrap 199 | 200 | return wrap(_cls) 201 | -------------------------------------------------------------------------------- /serde/inspect.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyserde inspection tool. 3 | 4 | #### Usage 5 | 6 | ``` 7 | $ python -m serde.inspect 8 | 9 | PATH Python script path. 10 | NAME Pyserde class name. 11 | ``` 12 | 13 | """ 14 | 15 | import argparse 16 | import importlib 17 | import logging 18 | import os 19 | import sys 20 | 21 | from typing import Any 22 | 23 | from .core import SERDE_SCOPE, init, logger 24 | 25 | init(True) 26 | 27 | 28 | def inspect(cls: type[Any]) -> None: 29 | """ 30 | Inspect a pyserde class. 31 | """ 32 | scope = getattr(cls, SERDE_SCOPE, {}) 33 | print(scope) 34 | 35 | 36 | def main(arg: Any) -> None: 37 | """ 38 | Main entrypoint of `serde.inspect`. 39 | """ 40 | if arg.verbose: 41 | logging.basicConfig(level=logging.DEBUG) 42 | 43 | try: 44 | import black 45 | 46 | assert black 47 | except ImportError: 48 | logger.warning( 49 | ( 50 | 'Tips: Installing "black" makes the output prettier! Try this command:\n' 51 | "pip install back" 52 | ) 53 | ) 54 | 55 | dir = os.path.dirname(arg.path) 56 | mod = os.path.basename(arg.path)[:-3] 57 | print(f"Loading {mod}.{arg.name} from {dir}/{mod}.py") 58 | sys.path.append(dir) 59 | pkg = importlib.import_module(mod) 60 | cls = getattr(pkg, arg.name) 61 | inspect(cls) 62 | 63 | 64 | parser = argparse.ArgumentParser(description="pyserde-inspect") 65 | parser.add_argument("path", type=str, help="Python script path.") 66 | parser.add_argument("name", type=str, help="Pyserde class name.") 67 | parser.add_argument("-v", dest="verbose", action="store_true", help="Enable debug logging.") 68 | 69 | 70 | if __name__ == "__main__": 71 | args = parser.parse_args() 72 | main(args) 73 | -------------------------------------------------------------------------------- /serde/json.py: -------------------------------------------------------------------------------- 1 | """ 2 | Serialize and Deserialize in JSON format. 3 | """ 4 | 5 | from typing import Any, AnyStr, overload, Optional, Union 6 | 7 | from .compat import T 8 | from .de import Deserializer, from_dict 9 | from .se import Serializer, to_dict 10 | from .numpy import encode_numpy 11 | 12 | try: # pragma: no cover 13 | import orjson 14 | 15 | def json_dumps(obj: Any, **opts: Any) -> str: 16 | if "option" not in opts: 17 | opts["option"] = orjson.OPT_SERIALIZE_NUMPY 18 | return orjson.dumps(obj, **opts).decode() # type: ignore 19 | 20 | def json_loads(s: Union[str, bytes], **opts: Any) -> Any: 21 | return orjson.loads(s, **opts) 22 | 23 | except ImportError: 24 | import json 25 | 26 | def json_dumps(obj: Any, **opts: Any) -> str: 27 | if "default" not in opts: 28 | opts["default"] = encode_numpy 29 | # compact output 30 | ensure_ascii = opts.pop("ensure_ascii", False) 31 | separators = opts.pop("separators", (",", ":")) 32 | return json.dumps(obj, ensure_ascii=ensure_ascii, separators=separators, **opts) 33 | 34 | def json_loads(s: Union[str, bytes], **opts: Any) -> Any: 35 | return json.loads(s, **opts) 36 | 37 | 38 | __all__ = ["from_json", "to_json"] 39 | 40 | 41 | class JsonSerializer(Serializer[str]): 42 | @classmethod 43 | def serialize(cls, obj: Any, **opts: Any) -> str: 44 | return json_dumps(obj, **opts) 45 | 46 | 47 | class JsonDeserializer(Deserializer[AnyStr]): 48 | @classmethod 49 | def deserialize(cls, data: AnyStr, **opts: Any) -> Any: 50 | return json_loads(data, **opts) 51 | 52 | 53 | def to_json( 54 | obj: Any, 55 | cls: Optional[Any] = None, 56 | se: type[Serializer[str]] = JsonSerializer, 57 | reuse_instances: bool = False, 58 | convert_sets: bool = True, 59 | skip_none: bool = False, 60 | **opts: Any, 61 | ) -> str: 62 | """ 63 | Serialize the object into JSON str. [orjson](https://github.com/ijl/orjson) 64 | will be used if installed. 65 | 66 | You can pass any serializable `obj`. If you supply other keyword arguments, 67 | they will be passed in `dumps` function. 68 | By default, numpy objects are serialized, this behaviour can be customized with the `option` 69 | argument with [orjson](https://github.com/ijl/orjson#numpy), or the `default` argument with 70 | Python standard json library. 71 | 72 | * `skip_none`: When set to True, any field in the class with a None value is excluded from the 73 | serialized output. Defaults to False. 74 | 75 | If you want to use another json package, you can subclass `JsonSerializer` and implement 76 | your own logic. 77 | """ 78 | return se.serialize( 79 | to_dict( 80 | obj, 81 | c=cls, 82 | reuse_instances=reuse_instances, 83 | convert_sets=convert_sets, 84 | skip_none=skip_none, 85 | ), 86 | **opts, 87 | ) 88 | 89 | 90 | @overload 91 | def from_json( 92 | c: type[T], s: AnyStr, de: type[Deserializer[AnyStr]] = JsonDeserializer, **opts: Any 93 | ) -> T: ... 94 | 95 | 96 | # For Union, Optional etc. 97 | @overload 98 | def from_json( 99 | c: Any, s: AnyStr, de: type[Deserializer[AnyStr]] = JsonDeserializer, **opts: Any 100 | ) -> Any: ... 101 | 102 | 103 | def from_json( 104 | c: Any, s: AnyStr, de: type[Deserializer[AnyStr]] = JsonDeserializer, **opts: Any 105 | ) -> Any: 106 | """ 107 | Deserialize from JSON into the object. [orjson](https://github.com/ijl/orjson) will be used 108 | if installed. 109 | 110 | `c` is a class object and `s` is JSON bytes or str. If you supply other keyword arguments, 111 | they will be passed in `loads` function. 112 | 113 | If you want to use another json package, you can subclass `JsonDeserializer` and implement 114 | your own logic. 115 | """ 116 | return from_dict(c, de.deserialize(s, **opts), reuse_instances=False) 117 | -------------------------------------------------------------------------------- /serde/msgpack.py: -------------------------------------------------------------------------------- 1 | """ 2 | Serialize and Deserialize in MsgPack format. This module depends on 3 | [msgpack](https://pypi.org/project/msgpack/) package. 4 | """ 5 | 6 | from typing import Any, Optional, overload 7 | 8 | import msgpack 9 | 10 | from .compat import T 11 | from .compat import SerdeError 12 | from .de import Deserializer, from_dict, from_tuple 13 | from .numpy import encode_numpy 14 | from .se import Serializer, to_dict, to_tuple 15 | 16 | __all__ = ["from_msgpack", "to_msgpack"] 17 | 18 | 19 | class MsgPackSerializer(Serializer[bytes]): 20 | @classmethod 21 | def serialize( 22 | cls, obj: Any, use_bin_type: bool = True, ext_type_code: Optional[int] = None, **opts: Any 23 | ) -> bytes: 24 | if "default" not in opts: 25 | opts["default"] = encode_numpy 26 | if ext_type_code is not None: 27 | obj_bytes = msgpack.packb(obj, use_bin_type=use_bin_type, **opts) 28 | obj_or_ext = msgpack.ExtType(ext_type_code, obj_bytes) 29 | else: 30 | obj_or_ext = obj 31 | return msgpack.packb(obj_or_ext, use_bin_type=use_bin_type, **opts) 32 | 33 | 34 | class MsgPackDeserializer(Deserializer[bytes]): 35 | @classmethod 36 | def deserialize( 37 | cls, data: bytes, raw: bool = False, use_list: bool = False, **opts: Any 38 | ) -> Any: 39 | return msgpack.unpackb(data, raw=raw, use_list=use_list, **opts) 40 | 41 | 42 | def to_msgpack( 43 | obj: Any, 44 | cls: Optional[Any] = None, 45 | se: type[Serializer[bytes]] = MsgPackSerializer, 46 | named: bool = True, 47 | ext_dict: Optional[dict[type[Any], int]] = None, 48 | reuse_instances: bool = False, 49 | convert_sets: bool = True, 50 | **opts: Any, 51 | ) -> bytes: 52 | """ 53 | Serialize the object into MsgPack. 54 | 55 | You can pass any serializable `obj`. If `ext_dict` option is specified, `obj` is encoded 56 | as a `msgpack.ExtType` If you supply other keyword arguments, they will be passed in 57 | `msgpack.packb` function. 58 | 59 | * `named`: If `named` is True, field names are preserved, namely the object is encoded as `dict` 60 | then serialized into MsgPack. If `named` is False, the object is encoded as `tuple` then 61 | serialized into MsgPack. `named=False` will produces compact binary. 62 | 63 | * `skip_none`: When set to True, any field in the class with a None value is excluded from the 64 | serialized output. Defaults to False. 65 | 66 | If you want to use the other msgpack package, you can subclass `MsgPackSerializer` and 67 | implement your own logic. 68 | """ 69 | ext_type_code = None 70 | if ext_dict is not None: 71 | obj_type = type(obj) 72 | ext_type_code = ext_dict.get(obj_type) 73 | if ext_type_code is None: 74 | raise SerdeError(f"Could not find type code for {obj_type.__name__} in ext_dict") 75 | 76 | kwargs: Any = {"c": cls, "reuse_instances": reuse_instances, "convert_sets": convert_sets} 77 | dict_or_tuple = to_dict(obj, **kwargs) if named else to_tuple(obj, **kwargs) 78 | return se.serialize( 79 | dict_or_tuple, 80 | ext_type_code=ext_type_code, 81 | **opts, 82 | ) 83 | 84 | 85 | @overload 86 | def from_msgpack( 87 | c: type[T], 88 | s: bytes, 89 | de: type[Deserializer[bytes]] = MsgPackDeserializer, 90 | named: bool = True, 91 | ext_dict: Optional[dict[int, type[Any]]] = None, 92 | **opts: Any, 93 | ) -> T: ... 94 | 95 | 96 | @overload 97 | def from_msgpack( 98 | c: Any, 99 | s: bytes, 100 | de: type[Deserializer[bytes]] = MsgPackDeserializer, 101 | named: bool = True, 102 | ext_dict: Optional[dict[int, type[Any]]] = None, 103 | **opts: Any, 104 | ) -> Any: ... 105 | 106 | 107 | def from_msgpack( 108 | c: Any, 109 | s: bytes, 110 | de: type[Deserializer[bytes]] = MsgPackDeserializer, 111 | named: bool = True, 112 | ext_dict: Optional[dict[int, type[Any]]] = None, 113 | skip_none: bool = False, 114 | **opts: Any, 115 | ) -> Any: 116 | """ 117 | Deserialize from MsgPack into the object. 118 | 119 | `c` is a class object and `s` is MsgPack binary. If `ext_dict` option is specified, 120 | `c` is ignored and type is inferred from `msgpack.ExtType` If you supply other keyword 121 | arguments, they will be passed in `msgpack.unpackb` function. 122 | 123 | If you want to use the other msgpack package, you can subclass `MsgPackDeserializer` 124 | and implement your own logic. 125 | """ 126 | if ext_dict is not None: 127 | ext = de.deserialize(s, **opts) 128 | ext_type = ext_dict.get(ext.code) 129 | if ext_type is None: 130 | raise SerdeError(f"Could not find type for code {ext.code} in ext_dict") 131 | return from_msgpack(ext_type, ext.data, de, named, **opts) 132 | else: 133 | from_func = from_dict if named else from_tuple 134 | return from_func(c, de.deserialize(s, **opts), reuse_instances=False) 135 | -------------------------------------------------------------------------------- /serde/numpy.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import Any, Optional 3 | 4 | from serde.compat import get_args, get_origin 5 | 6 | 7 | def fullname(klass): 8 | module = klass.__module__ 9 | if module == "builtins": 10 | return klass.__qualname__ # avoid outputs like 'builtins.str' 11 | return module + "." + klass.__qualname__ 12 | 13 | 14 | def is_numpy_type(typ) -> bool: 15 | return is_bare_numpy_array(typ) or is_numpy_scalar(typ) or is_numpy_array(typ) 16 | 17 | 18 | def is_numpy_available() -> bool: 19 | return encode_numpy is not None 20 | 21 | 22 | try: 23 | import numpy as np 24 | import numpy.typing as npt 25 | 26 | encode_numpy: Optional[Callable[[Any], Any]] 27 | 28 | def encode_numpy(obj: Any): 29 | if isinstance(obj, np.ndarray): 30 | return obj.tolist() 31 | if isinstance(obj, np.datetime64): 32 | return obj.item().isoformat() 33 | if isinstance(obj, np.generic): 34 | return obj.item() 35 | raise TypeError(f"Object of type {fullname(type(obj))} is not serializable") 36 | 37 | def is_bare_numpy_array(typ) -> bool: 38 | """ 39 | Test if the type is `np.ndarray` or `npt.NDArray` without type args. 40 | 41 | >>> import numpy as np 42 | >>> import numpy.typing as npt 43 | >>> is_bare_numpy_array(npt.NDArray[np.int64]) 44 | False 45 | >>> is_bare_numpy_array(npt.NDArray) 46 | True 47 | >>> is_bare_numpy_array(np.ndarray) 48 | True 49 | """ 50 | return typ in (np.ndarray, npt.NDArray) 51 | 52 | def is_numpy_scalar(typ) -> bool: 53 | try: 54 | return issubclass(typ, np.generic) 55 | except TypeError: 56 | return False 57 | 58 | def is_numpy_datetime(typ) -> bool: 59 | try: 60 | return issubclass(typ, np.datetime64) 61 | except TypeError: 62 | return False 63 | 64 | def serialize_numpy_scalar(arg) -> str: 65 | return f"{arg.varname}.item()" 66 | 67 | def deserialize_numpy_scalar(arg: Any) -> str: 68 | return f"{fullname(arg.type)}({arg.data})" 69 | 70 | def is_numpy_array(typ) -> bool: 71 | origin = get_origin(typ) 72 | if origin is not None: 73 | typ = origin 74 | return typ is np.ndarray 75 | 76 | def is_numpy_jaxtyping(typ) -> bool: 77 | try: 78 | origin = get_origin(typ) 79 | if origin is not None: 80 | typ = origin 81 | return typ is not np.ndarray and issubclass(typ, np.ndarray) 82 | except TypeError: 83 | return False 84 | 85 | def serialize_numpy_array(arg) -> str: 86 | return f"{arg.varname}.tolist()" 87 | 88 | def serialize_numpy_datetime(arg) -> str: 89 | return f"{arg.varname}.item().isoformat()" 90 | 91 | def deserialize_numpy_array(arg) -> str: 92 | if is_bare_numpy_array(arg.type): 93 | return f"numpy.array({arg.data})" 94 | 95 | dtype = fullname(arg[1][0].type) 96 | return f"numpy.array({arg.data}, dtype={dtype})" 97 | 98 | def deserialize_numpy_jaxtyping_array(arg) -> str: 99 | dtype = f"numpy.{arg.type.dtypes[-1]}" 100 | return f"numpy.array({arg.data}, dtype={dtype})" 101 | 102 | def deserialize_numpy_array_direct(typ: Any, arg: Any) -> Any: 103 | if is_bare_numpy_array(typ): 104 | return np.array(arg) 105 | 106 | dtype = get_args(get_args(typ)[1])[0] 107 | return np.array(arg, dtype=dtype) 108 | 109 | except ImportError: 110 | encode_numpy = None 111 | 112 | def is_numpy_scalar(typ) -> bool: 113 | return False 114 | 115 | def is_numpy_datetime(typ) -> bool: 116 | return False 117 | 118 | def serialize_numpy_scalar(arg) -> str: 119 | return "" 120 | 121 | def deserialize_numpy_scalar(arg): 122 | return "" 123 | 124 | def is_numpy_array(typ) -> bool: 125 | return False 126 | 127 | def is_numpy_jaxtyping(typ) -> bool: 128 | return False 129 | 130 | def serialize_numpy_array(arg) -> str: 131 | return "" 132 | 133 | def serialize_numpy_datetime(arg) -> str: 134 | return "" 135 | 136 | def deserialize_numpy_array(arg) -> str: 137 | return "" 138 | 139 | def deserialize_numpy_jaxtyping_array(arg) -> str: 140 | return "" 141 | 142 | def deserialize_numpy_array_direct(typ: Any, arg: Any) -> Any: 143 | return arg 144 | -------------------------------------------------------------------------------- /serde/pickle.py: -------------------------------------------------------------------------------- 1 | """ 2 | Serialize and Deserialize in Pickle format. 3 | """ 4 | 5 | import pickle 6 | from typing import overload, Any, Optional 7 | 8 | from .compat import T 9 | from .de import Deserializer, from_dict 10 | from .se import Serializer, to_dict 11 | 12 | __all__ = ["from_pickle", "to_pickle"] 13 | 14 | 15 | class PickleSerializer(Serializer[bytes]): 16 | @classmethod 17 | def serialize(cls, obj: Any, **opts: Any) -> bytes: 18 | return pickle.dumps(obj, **opts) 19 | 20 | 21 | class PickleDeserializer(Deserializer[bytes]): 22 | @classmethod 23 | def deserialize(cls, data: bytes, **opts: Any) -> Any: 24 | return pickle.loads(data, **opts) 25 | 26 | 27 | def to_pickle( 28 | obj: Any, 29 | cls: Optional[Any] = None, 30 | se: type[Serializer[bytes]] = PickleSerializer, 31 | reuse_instances: bool = False, 32 | convert_sets: bool = True, 33 | skip_none: bool = False, 34 | **opts: Any, 35 | ) -> bytes: 36 | return se.serialize( 37 | to_dict(obj, c=cls, reuse_instances=reuse_instances, convert_sets=convert_sets), **opts 38 | ) 39 | 40 | 41 | @overload 42 | def from_pickle( 43 | c: type[T], data: bytes, de: type[Deserializer[bytes]] = PickleDeserializer, **opts: Any 44 | ) -> T: ... 45 | 46 | 47 | @overload 48 | def from_pickle( 49 | c: Any, data: bytes, de: type[Deserializer[bytes]] = PickleDeserializer, **opts: Any 50 | ) -> Any: ... 51 | 52 | 53 | # For Union, Optional etc. 54 | def from_pickle( 55 | c: Any, data: bytes, de: type[Deserializer[bytes]] = PickleDeserializer, **opts: Any 56 | ) -> Any: 57 | return from_dict(c, de.deserialize(data, **opts), reuse_instances=False) 58 | -------------------------------------------------------------------------------- /serde/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/serde/py.typed -------------------------------------------------------------------------------- /serde/sqlalchemy.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | try: 4 | from sqlalchemy.inspection import inspect 5 | from sqlalchemy.exc import NoInspectionAvailable 6 | 7 | def is_sqlalchemy_inspectable(subject: Any) -> bool: 8 | try: 9 | inspect(subject) 10 | return True 11 | except NoInspectionAvailable: 12 | return False 13 | 14 | except ImportError: 15 | 16 | def is_sqlalchemy_inspectable(subject: Any) -> bool: 17 | return False 18 | -------------------------------------------------------------------------------- /serde/toml.py: -------------------------------------------------------------------------------- 1 | """ 2 | Serialize and Deserialize in TOML format. This module depends on 3 | [tomli](https://github.com/hukkin/tomli) (for python<=3.10) and 4 | [tomli-w](https://github.com/hukkin/tomli-w) packages. 5 | """ 6 | 7 | import sys 8 | from typing import overload, Optional, Any 9 | 10 | import tomli_w 11 | 12 | from .compat import T 13 | from .de import Deserializer, from_dict 14 | from .se import Serializer, to_dict 15 | 16 | __all__ = ["from_toml", "to_toml"] 17 | 18 | 19 | if sys.version_info[:2] >= (3, 11): 20 | import tomllib 21 | else: 22 | import tomli as tomllib 23 | 24 | 25 | class TomlSerializer(Serializer[str]): 26 | @classmethod 27 | def serialize(cls, obj: Any, **opts: Any) -> str: 28 | return tomli_w.dumps(obj, **opts) 29 | 30 | 31 | class TomlDeserializer(Deserializer[str]): 32 | @classmethod 33 | def deserialize(cls, data: str, **opts: Any) -> Any: 34 | return tomllib.loads(data, **opts) 35 | 36 | 37 | def to_toml( 38 | obj: Any, 39 | cls: Optional[Any] = None, 40 | se: type[Serializer[str]] = TomlSerializer, 41 | reuse_instances: bool = False, 42 | convert_sets: bool = True, 43 | skip_none: bool = True, 44 | **opts: Any, 45 | ) -> str: 46 | """ 47 | Serialize the object into TOML. 48 | 49 | You can pass any serializable `obj`. If you supply keyword arguments other than `se`, 50 | they will be passed in `toml_w.dumps` function. 51 | 52 | * `skip_none`: When set to True, any field in the class with a None value is excluded from the 53 | serialized output. Defaults to True. 54 | 55 | If you want to use the other toml package, you can subclass `TomlSerializer` and implement 56 | your own logic. 57 | """ 58 | return se.serialize( 59 | to_dict( 60 | obj, 61 | c=cls, 62 | reuse_instances=reuse_instances, 63 | convert_sets=convert_sets, 64 | skip_none=skip_none, 65 | ), 66 | **opts, 67 | ) 68 | 69 | 70 | @overload 71 | def from_toml( 72 | c: type[T], s: str, de: type[Deserializer[str]] = TomlDeserializer, **opts: Any 73 | ) -> T: ... 74 | 75 | 76 | # For Union, Optional etc. 77 | @overload 78 | def from_toml( 79 | c: Any, s: str, de: type[Deserializer[str]] = TomlDeserializer, **opts: Any 80 | ) -> Any: ... 81 | 82 | 83 | def from_toml(c: Any, s: str, de: type[Deserializer[str]] = TomlDeserializer, **opts: Any) -> Any: 84 | """ 85 | Deserialize from TOML into the object. 86 | 87 | `c` is a class object and `s` is TOML string. If you supply keyword arguments other than `de`, 88 | they will be passed in `toml.loads` function. 89 | 90 | If you want to use the other toml package, you can subclass `TomlDeserializer` and implement 91 | your own logic. 92 | """ 93 | return from_dict(c, de.deserialize(s, **opts), reuse_instances=False) 94 | -------------------------------------------------------------------------------- /serde/yaml.py: -------------------------------------------------------------------------------- 1 | """ 2 | Serialize and Deserialize in YAML format. This module depends on 3 | [pyyaml](https://pypi.org/project/PyYAML/) package. 4 | """ 5 | 6 | from typing import overload, Optional, Any 7 | 8 | import yaml 9 | 10 | from .compat import T 11 | from .de import Deserializer, from_dict 12 | from .se import Serializer, to_dict 13 | 14 | __all__ = ["from_yaml", "to_yaml"] 15 | 16 | 17 | class YamlSerializer(Serializer[str]): 18 | @classmethod 19 | def serialize(cls, obj: Any, **opts: Any) -> str: 20 | return yaml.safe_dump(obj, **opts) # type: ignore 21 | 22 | 23 | class YamlDeserializer(Deserializer[str]): 24 | @classmethod 25 | def deserialize(cls, data: str, **opts: Any) -> Any: 26 | return yaml.safe_load(data, **opts) 27 | 28 | 29 | def to_yaml( 30 | obj: Any, 31 | cls: Optional[Any] = None, 32 | se: type[Serializer[str]] = YamlSerializer, 33 | reuse_instances: bool = False, 34 | convert_sets: bool = True, 35 | skip_none: bool = False, 36 | **opts: Any, 37 | ) -> str: 38 | """ 39 | Serialize the object into YAML. 40 | 41 | You can pass any serializable `obj`. If you supply keyword arguments other than `se`, 42 | they will be passed in `yaml.safe_dump` function. 43 | 44 | * `skip_none`: When set to True, any field in the class with a None value is excluded from the 45 | serialized output. Defaults to False. 46 | 47 | If you want to use the other yaml package, you can subclass `YamlSerializer` and implement 48 | your own logic. 49 | """ 50 | return se.serialize( 51 | to_dict( 52 | obj, 53 | c=cls, 54 | reuse_instances=reuse_instances, 55 | convert_sets=convert_sets, 56 | skip_none=skip_none, 57 | ), 58 | **opts, 59 | ) 60 | 61 | 62 | @overload 63 | def from_yaml( 64 | c: type[T], s: str, de: type[Deserializer[str]] = YamlDeserializer, **opts: Any 65 | ) -> T: ... 66 | 67 | 68 | # For Union, Optional etc. 69 | @overload 70 | def from_yaml( 71 | c: Any, s: str, de: type[Deserializer[str]] = YamlDeserializer, **opts: Any 72 | ) -> Any: ... 73 | 74 | 75 | def from_yaml(c: Any, s: str, de: type[Deserializer[str]] = YamlDeserializer, **opts: Any) -> Any: 76 | """ 77 | `c` is a class object and `s` is YAML string. If you supply keyword arguments other than `de`, 78 | they will be passed in `yaml.safe_load` function. 79 | 80 | If you want to use the other yaml package, you can subclass `YamlDeserializer` and implement 81 | your own logic. 82 | """ 83 | return from_dict(c, de.deserialize(s, **opts), reuse_instances=False) 84 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | [tool:pytest] 4 | addopts = -v 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yukinarit/pyserde/22d0b40d5ecbfbc048bb94978ac9c528a713af38/tests/__init__.py -------------------------------------------------------------------------------- /tests/data.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from dataclasses import dataclass 3 | import enum 4 | from typing import Optional, Any 5 | 6 | from serde import field, serde, disabled 7 | 8 | from . import imported 9 | 10 | 11 | @serde 12 | class Inner: 13 | i: int 14 | 15 | 16 | @serde 17 | @dataclass(unsafe_hash=True) 18 | class Int: 19 | """ 20 | Integer. 21 | """ 22 | 23 | i: int 24 | 25 | @staticmethod 26 | def uncheck_new(value: Any) -> Int: 27 | """ 28 | Bypass runtime type checker by mutating inner value. 29 | """ 30 | obj = Int(0) 31 | obj.i = value 32 | return obj 33 | 34 | 35 | @serde(type_check=disabled) 36 | @dataclass(unsafe_hash=True) 37 | class UncheckedInt: 38 | """ 39 | Integer. 40 | """ 41 | 42 | i: int 43 | 44 | 45 | @serde 46 | @dataclass(unsafe_hash=True) 47 | class Str: 48 | """ 49 | String. 50 | """ 51 | 52 | s: str 53 | 54 | 55 | @serde 56 | @dataclass(unsafe_hash=True) 57 | class Float: 58 | """ 59 | Float. 60 | """ 61 | 62 | f: float 63 | 64 | 65 | @serde 66 | @dataclass(unsafe_hash=True) 67 | class Bool: 68 | """ 69 | Boolean. 70 | """ 71 | 72 | b: bool 73 | 74 | 75 | @serde 76 | @dataclass(unsafe_hash=True) 77 | class Pri: 78 | """ 79 | Primitives. 80 | """ 81 | 82 | i: int 83 | s: str 84 | f: float 85 | b: bool 86 | 87 | 88 | @serde 89 | class PriOpt: 90 | """ 91 | Optional Primitives. 92 | """ 93 | 94 | i: Optional[int] 95 | s: Optional[str] 96 | f: Optional[float] 97 | b: Optional[bool] 98 | 99 | 100 | @serde 101 | class PriList: 102 | """ 103 | List containing primitives. 104 | """ 105 | 106 | i: list[int] 107 | s: list[str] 108 | f: list[float] 109 | b: list[bool] 110 | 111 | 112 | @serde 113 | class PriDict: 114 | """ 115 | Dict containing primitives. 116 | """ 117 | 118 | i: dict[str, int] 119 | s: dict[str, str] 120 | f: dict[str, float] 121 | b: dict[str, bool] 122 | 123 | 124 | @serde 125 | class PriTuple: 126 | """ 127 | Tuple containing primitives. 128 | """ 129 | 130 | i: tuple[int, int, int] 131 | s: tuple[str, str, str, str] 132 | f: tuple[float, float, float, float, float] 133 | b: tuple[bool, bool, bool, bool, bool, bool] 134 | 135 | 136 | @dataclass(unsafe_hash=True) 137 | class NestedInt: 138 | """ 139 | Nested integer. 140 | """ 141 | 142 | i: Int 143 | 144 | 145 | @serde 146 | @dataclass(unsafe_hash=True) 147 | class NestedPri: 148 | """ 149 | Nested primitives. 150 | """ 151 | 152 | i: Int 153 | s: Str 154 | f: Float 155 | b: Bool 156 | 157 | 158 | @serde 159 | class NestedPriOpt: 160 | """ 161 | Optional Primitives. 162 | """ 163 | 164 | i: Optional[Int] 165 | s: Optional[Str] 166 | f: Optional[Float] 167 | b: Optional[Bool] 168 | 169 | 170 | @serde 171 | class NestedPriList: 172 | """ 173 | List containing nested primitives. 174 | """ 175 | 176 | i: list[Int] 177 | s: list[Str] 178 | f: list[Float] 179 | b: list[Bool] 180 | 181 | 182 | @serde 183 | class NestedPriDict: 184 | """ 185 | Dict containing nested primitives. 186 | """ 187 | 188 | i: dict[Str, Int] 189 | s: dict[Str, Str] 190 | f: dict[Str, Float] 191 | b: dict[Str, Bool] 192 | 193 | 194 | @serde 195 | class NestedPriTuple: 196 | """ 197 | Tuple containing nested primitives. 198 | """ 199 | 200 | i: tuple[Int, Int, Int] 201 | s: tuple[Str, Str, Str, Str] 202 | f: tuple[Float, Float, Float, Float, Float] 203 | b: tuple[Bool, Bool, Bool, Bool, Bool, Bool] 204 | 205 | 206 | @serde 207 | @dataclass(unsafe_hash=True) 208 | class PriDefault: 209 | """ 210 | Primitives. 211 | """ 212 | 213 | i: int = 10 214 | s: str = "foo" 215 | f: float = 100.0 216 | b: bool = True 217 | 218 | 219 | @serde 220 | class OptDefault: 221 | """ 222 | Optionals. 223 | """ 224 | 225 | n: Optional[int] = None 226 | i: Optional[int] = 10 227 | 228 | 229 | class E(enum.Enum): 230 | S = "foo" 231 | F = 10.0 232 | B = True 233 | 234 | 235 | class IE(enum.IntEnum): 236 | V0 = enum.auto() 237 | V1 = enum.auto() 238 | V2 = 10 239 | V3 = 100 240 | 241 | 242 | @serde 243 | class EnumInClass: 244 | """ 245 | Class having enum fields. 246 | """ 247 | 248 | e: IE = IE.V2 249 | o: Optional[E] = E.S 250 | i: imported.IE = imported.IE.V1 251 | 252 | 253 | @dataclass(unsafe_hash=True) 254 | class Recur: 255 | a: Optional[Recur] 256 | b: Optional[list[Recur]] 257 | c: Optional[dict[str, Recur]] 258 | 259 | 260 | @dataclass(unsafe_hash=True) 261 | class RecurContainer: 262 | a: list[RecurContainer] 263 | b: dict[str, RecurContainer] 264 | 265 | 266 | serde(Recur) 267 | serde(RecurContainer) 268 | 269 | 270 | ListPri = list[Pri] 271 | 272 | DictPri = dict[str, Pri] 273 | 274 | INT = Int(10) 275 | 276 | STR = Str("foo") 277 | 278 | FLOAT = Float(100.0) 279 | 280 | BOOL = Bool(True) 281 | 282 | PRI = Pri(10, "foo", 100.0, True) 283 | 284 | PRI_TUPLE = (10, "foo", 100.0, True) 285 | 286 | PRILIST = ([10], ["foo"], [100.0], [True]) 287 | 288 | NESTED_PRILIST = ([INT], [STR], [FLOAT], [BOOL]) 289 | 290 | NESTED_PRILIST_TUPLE = ([(10,)], [("foo",)], [(100.0,)], [(True,)]) 291 | 292 | 293 | @serde 294 | class Init: 295 | a: int 296 | b: int = field(init=False) 297 | 298 | def __post_init__(self) -> None: 299 | self.b = self.a * 10 300 | 301 | 302 | class StrSubclass(str): 303 | pass 304 | 305 | 306 | @serde 307 | class PrimitiveSubclass: 308 | v: StrSubclass 309 | -------------------------------------------------------------------------------- /tests/imported.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class IE(enum.IntEnum): 5 | V0 = enum.auto() 6 | V1 = enum.auto() 7 | V2 = 10 8 | -------------------------------------------------------------------------------- /tests/test_beartype.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from beartype import beartype 3 | from beartype.roar import BeartypeCallHintParamViolation 4 | from dataclasses import dataclass 5 | from serde import serde, SerdeError 6 | 7 | 8 | def test_raises_serde_error_instead_of_beartype_error() -> None: 9 | @serde 10 | class Foo: 11 | v: int 12 | 13 | with pytest.raises(SerdeError): 14 | Foo("foo") # type: ignore 15 | 16 | 17 | def test_raises_beartype_error_if_beartype_decorated() -> None: 18 | # If a class already has beartype, pyserde cannot set custom validation error. 19 | @serde 20 | @beartype 21 | @dataclass 22 | class Foo: 23 | v: int 24 | 25 | with pytest.raises(BeartypeCallHintParamViolation): 26 | Foo("foo") # type: ignore 27 | -------------------------------------------------------------------------------- /tests/test_code_completion.py: -------------------------------------------------------------------------------- 1 | import jedi 2 | 3 | 4 | def test_jedi() -> None: 5 | source = """ 6 | from serde import serde 7 | 8 | @serde 9 | class Foo: 10 | a: int 11 | b: float 12 | c: str 13 | baz: bool 14 | 15 | foo = Foo(10, 100.0, "foo", True) 16 | """ 17 | source_completion = source + "\n" + "foo." 18 | jedi_script = jedi.Script(source_completion, path="foo.py") 19 | completions = jedi_script.complete(9, len("foo.")) 20 | completions = [comp.name for comp in completions] 21 | assert "a" in completions 22 | assert "b" in completions 23 | assert "c" in completions 24 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, is_dataclass 2 | 3 | from serde.core import should_impl_dataclass 4 | 5 | 6 | def test_should_impl_dataclass(): 7 | @dataclass 8 | class Base: 9 | a: int 10 | 11 | class Derived(Base): 12 | b: int 13 | 14 | is_dataclass(Base) 15 | is_dataclass(Derived) 16 | assert not should_impl_dataclass(Base) 17 | assert should_impl_dataclass(Derived) 18 | 19 | @dataclass 20 | class Base: 21 | pass 22 | 23 | class Derived(Base): 24 | pass 25 | 26 | is_dataclass(Base) 27 | is_dataclass(Derived) 28 | assert not should_impl_dataclass(Base) 29 | assert not should_impl_dataclass(Derived) 30 | 31 | @dataclass 32 | class Base: 33 | pass 34 | 35 | class Derived(Base): 36 | b: int 37 | 38 | is_dataclass(Base) 39 | is_dataclass(Derived) 40 | assert not should_impl_dataclass(Base) 41 | assert should_impl_dataclass(Derived) 42 | 43 | class Base: 44 | a: int 45 | 46 | @dataclass 47 | class Derived(Base): 48 | b: int 49 | 50 | is_dataclass(Base) 51 | is_dataclass(Derived) 52 | assert should_impl_dataclass(Base) 53 | assert not should_impl_dataclass(Derived) 54 | 55 | class Base: 56 | a: int 57 | 58 | class Derived(Base): 59 | b: int 60 | 61 | is_dataclass(Base) 62 | is_dataclass(Derived) 63 | assert should_impl_dataclass(Base) 64 | assert should_impl_dataclass(Derived) 65 | 66 | @dataclass 67 | class Base1: 68 | a: int 69 | 70 | @dataclass 71 | class Base2: 72 | a: int 73 | 74 | class Derived(Base1, Base2): 75 | c: int 76 | 77 | is_dataclass(Base) 78 | is_dataclass(Derived) 79 | assert not should_impl_dataclass(Base1) 80 | assert not should_impl_dataclass(Base2) 81 | assert should_impl_dataclass(Derived) 82 | -------------------------------------------------------------------------------- /tests/test_flatten.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for flatten attribute. 3 | """ 4 | 5 | from typing import Any, Optional 6 | 7 | import pytest 8 | 9 | from serde import field, serde, SerdeError 10 | from serde.json import from_json, to_json 11 | 12 | from .common import all_formats 13 | 14 | 15 | def test_flatten_simple() -> None: 16 | @serde 17 | class Bar: 18 | c: float 19 | d: bool 20 | 21 | @serde 22 | class Foo: 23 | a: int 24 | b: str 25 | bar: Bar = field(flatten=True) 26 | 27 | f = Foo(a=10, b="foo", bar=Bar(c=100.0, d=True)) 28 | s = '{"a":10,"b":"foo","c":100.0,"d":true}' 29 | assert to_json(f) == s 30 | assert from_json(Foo, s) == f 31 | 32 | 33 | @pytest.mark.parametrize("se,de", all_formats) 34 | def test_flatten(se: Any, de: Any) -> None: 35 | @serde 36 | class Baz: 37 | e: list[int] 38 | f: dict[str, str] 39 | 40 | @serde 41 | class Bar: 42 | c: float 43 | d: bool 44 | baz: Baz = field(flatten=True) 45 | 46 | @serde 47 | class Foo: 48 | a: int 49 | b: str 50 | bar: Bar = field(flatten=True) 51 | 52 | f = Foo(a=10, b="foo", bar=Bar(c=100.0, d=True, baz=Baz([1, 2], {"a": "10"}))) 53 | assert de(Foo, se(f)) == f 54 | 55 | 56 | @pytest.mark.parametrize("se,de", all_formats) 57 | def test_flatten_optional(se: Any, de: Any) -> None: 58 | @serde 59 | class Bar: 60 | c: float 61 | d: bool 62 | 63 | @serde 64 | class Foo: 65 | a: int 66 | b: str 67 | bar: Optional[Bar] = field(flatten=True) 68 | 69 | f = Foo(a=10, b="foo", bar=Bar(c=100.0, d=True)) 70 | assert de(Foo, se(f)) == f 71 | 72 | 73 | @pytest.mark.parametrize("se,de", all_formats) 74 | def test_flatten_not_supported(se: Any, de: Any) -> None: 75 | @serde 76 | class Bar: 77 | pass 78 | 79 | with pytest.raises(SerdeError): 80 | 81 | @serde 82 | class Foo: 83 | bar: list[Bar] = field(flatten=True) 84 | 85 | 86 | def test_flatten_default() -> None: 87 | @serde 88 | class Bar: 89 | c: float = field(default=0.0) 90 | d: bool = field(default=False) 91 | 92 | @serde 93 | class Foo: 94 | a: int 95 | b: str = field(default="foo") 96 | bar: Bar = field(flatten=True, default_factory=Bar) 97 | 98 | f = Foo(a=10, b="b", bar=Bar(c=100.0, d=True)) 99 | assert from_json(Foo, to_json(f)) == f 100 | 101 | assert from_json(Foo, '{"a": 20}') == Foo(20, "foo", Bar()) 102 | 103 | 104 | def test_flatten_default_alias() -> None: 105 | @serde 106 | class Bar: 107 | a: float = field(default=0.0, alias=["aa"]) # type: ignore 108 | b: bool = field(default=False, alias=["bb"]) # type: ignore 109 | 110 | @serde 111 | class Foo: 112 | bar: Bar = field(flatten=True, default_factory=Bar) 113 | 114 | f = Foo(bar=Bar(100.0, True)) 115 | assert from_json(Foo, to_json(f)) == f 116 | 117 | assert from_json(Foo, '{"aa": 20.0, "bb": false}') == Foo(Bar(20.0, False)) 118 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.json import to_json, from_json 3 | from typing import Optional 4 | 5 | 6 | def test_json_basics() -> None: 7 | @serde 8 | class Foo: 9 | v: Optional[int] 10 | 11 | f = Foo(10) 12 | assert '{"v":10}' == to_json(f) 13 | assert f == from_json(Foo, '{"v":10}') 14 | 15 | @serde 16 | class Bar: 17 | v: set[int] 18 | 19 | b = Bar({1, 2, 3}) 20 | assert '{"v":[1,2,3]}' == to_json(b) 21 | assert b == from_json(Bar, '{"v":[1,2,3]}') 22 | 23 | 24 | def test_skip_none() -> None: 25 | @serde 26 | class Foo: 27 | a: int 28 | b: Optional[int] 29 | 30 | f = Foo(10, 100) 31 | assert to_json(f, skip_none=True) == '{"a":10,"b":100}' 32 | 33 | f = Foo(10, None) 34 | assert to_json(f, skip_none=True) == '{"a":10}' 35 | -------------------------------------------------------------------------------- /tests/test_kwonly.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from dataclasses import dataclass, field 3 | from typing import Optional 4 | 5 | import pytest 6 | 7 | from serde import deserialize, from_dict 8 | 9 | 10 | @pytest.mark.skipif( 11 | sys.version_info < (3, 10), reason="dataclasses `kw_only` requires python3.10 or higher" 12 | ) 13 | def test_simple() -> None: 14 | @deserialize 15 | @dataclass(kw_only=True) 16 | class Hello: 17 | a: str 18 | 19 | assert Hello(a="ok") == from_dict(Hello, {"a": "ok"}) 20 | 21 | 22 | @pytest.mark.skipif( 23 | sys.version_info < (3, 10), reason="dataclasses `kw_only` requires python3.10 or higher" 24 | ) 25 | def test_inheritance() -> None: 26 | @dataclass(kw_only=True) 27 | class Friend: 28 | name: str = "MyFriend" 29 | 30 | @dataclass(kw_only=True) 31 | class Parent: 32 | child_val: Optional[str] 33 | 34 | @dataclass(kw_only=True) 35 | class Child(Parent): 36 | value: int = 42 37 | friend: Friend = field(default_factory=Friend) 38 | 39 | # check with defaults 40 | assert Child(child_val="test") == from_dict(Child, {"child_val": "test"}) 41 | 42 | # check without defaults 43 | assert Child(child_val="test", value=34, friend=Friend(name="my_friend")) == from_dict( 44 | Child, {"child_val": "test", "value": 34, "friend": {"name": "my_friend"}} 45 | ) 46 | -------------------------------------------------------------------------------- /tests/test_lazy_type_evaluation.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations # this is the line this test file is all about 2 | 3 | import dataclasses 4 | from enum import Enum 5 | 6 | import pytest 7 | 8 | from serde import SerdeError, deserialize, from_dict, serde, serialize, to_dict 9 | from serde.compat import dataclass_fields 10 | 11 | 12 | class Status(Enum): 13 | OK = "ok" 14 | ERR = "err" 15 | 16 | 17 | @serde 18 | class A: 19 | a: int 20 | b: Status 21 | c: list[str] 22 | 23 | 24 | @serde 25 | class B: 26 | a: A 27 | b: tuple[str, A] 28 | c: Status 29 | 30 | 31 | # only works with global classes 32 | def test_serde_with_lazy_type_annotations() -> None: 33 | a = A(1, Status.ERR, ["foo"]) 34 | a_dict = {"a": 1, "b": "err", "c": ["foo"]} 35 | 36 | assert a == from_dict(A, a_dict) 37 | assert a_dict == to_dict(a) 38 | 39 | b = B(a, ("foo", a), Status.OK) 40 | b_dict = {"a": a_dict, "b": ("foo", a_dict), "c": "ok"} 41 | 42 | assert b == from_dict(B, b_dict) 43 | assert b_dict == to_dict(b) 44 | 45 | 46 | # test_forward_reference_works currently only works with global visible classes 47 | @dataclasses.dataclass 48 | class ForwardReferenceFoo: 49 | # this is not a string forward reference because we use PEP 563 (see 1st line of this file) 50 | bar: ForwardReferenceBar 51 | 52 | 53 | @serde 54 | class ForwardReferenceBar: 55 | i: int 56 | 57 | 58 | # assert type is str 59 | typ = dataclasses.fields(ForwardReferenceFoo)[0].type 60 | assert isinstance(typ, str) and "ForwardReferenceBar" == typ 61 | 62 | # setup pyserde for Foo after Bar becomes visible to global scope 63 | deserialize(ForwardReferenceFoo) 64 | serialize(ForwardReferenceFoo) 65 | 66 | # now the type really is of type Bar 67 | assert ForwardReferenceBar == dataclasses.fields(ForwardReferenceFoo)[0].type 68 | assert ForwardReferenceBar == next(dataclass_fields(ForwardReferenceFoo)).type 69 | 70 | 71 | # verify usage works 72 | def test_forward_reference_works() -> None: 73 | h = ForwardReferenceFoo(bar=ForwardReferenceBar(i=10)) 74 | h_dict = {"bar": {"i": 10}} 75 | 76 | assert to_dict(h) == h_dict 77 | assert from_dict(ForwardReferenceFoo, h_dict) == h 78 | 79 | 80 | # trying to use forward reference normally will throw 81 | def test_unresolved_forward_reference_throws() -> None: 82 | with pytest.raises(SerdeError) as e: 83 | 84 | @serde 85 | class UnresolvedForwardFoo: 86 | bar: UnresolvedForwardBar 87 | 88 | @serde 89 | class UnresolvedForwardBar: 90 | i: int 91 | 92 | assert "Failed to resolve type hints for UnresolvedForwardFoo" in str(e) 93 | 94 | 95 | # trying to use string forward reference will throw 96 | def test_string_forward_reference_throws() -> None: 97 | with pytest.raises(SerdeError): 98 | 99 | @serde 100 | class UnresolvedStringForwardFoo: 101 | # string forward references are not compatible with PEP 563 and will throw 102 | bar: "UnresolvedStringForwardBar" 103 | 104 | @serde 105 | class UnresolvedStringForwardBar: 106 | i: int 107 | -------------------------------------------------------------------------------- /tests/test_legacy_custom.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for custom serializer/deserializer. 3 | """ 4 | 5 | from datetime import datetime 6 | from typing import Optional, Union 7 | 8 | import pytest 9 | 10 | from serde import ( 11 | SerdeSkip, 12 | default_deserializer, 13 | default_serializer, 14 | field, 15 | from_tuple, 16 | serde, 17 | to_tuple, 18 | SerdeError, 19 | ) 20 | from serde.json import from_json, to_json 21 | 22 | 23 | def test_legacy_custom_class_serializer(): 24 | def serializer(cls, o): 25 | if cls is datetime: 26 | return o.strftime("%d/%m/%y") 27 | else: 28 | raise SerdeSkip() 29 | 30 | def deserializer(cls, o): 31 | if cls is datetime: 32 | return datetime.strptime(o, "%d/%m/%y") 33 | else: 34 | raise SerdeSkip() 35 | 36 | @serde(serializer=serializer, deserializer=deserializer) 37 | class Foo: 38 | a: int 39 | b: datetime 40 | c: datetime 41 | d: Optional[str] = None 42 | e: Union[str, int] = 10 43 | f: list[int] = field(default_factory=list) 44 | g: set[int] = field(default_factory=set) 45 | 46 | dt = datetime(2021, 1, 1, 0, 0, 0) 47 | f = Foo(10, dt, dt, f=[1, 2, 3], g={4, 5, 6}) 48 | 49 | assert ( 50 | to_json(f) 51 | == '{"a":10,"b":"01/01/21","c":"01/01/21","d":null,"e":10,"f":[1,2,3],"g":[4,5,6]}' 52 | ) 53 | assert f == from_json(Foo, to_json(f)) 54 | 55 | assert to_tuple(f) == (10, "01/01/21", "01/01/21", None, 10, [1, 2, 3], {4, 5, 6}) 56 | assert f == from_tuple(Foo, to_tuple(f)) 57 | 58 | def fallback(_, __): 59 | raise SerdeSkip() 60 | 61 | @serde(serializer=fallback, deserializer=fallback) 62 | class Foo: 63 | a: Optional[str] 64 | b: str 65 | 66 | f = Foo("foo", "bar") 67 | assert to_json(f) == '{"a":"foo","b":"bar"}' 68 | assert f == from_json(Foo, '{"a":"foo","b":"bar"}') 69 | assert Foo(None, "bar") == from_json(Foo, '{"b":"bar"}') 70 | with pytest.raises(SerdeError): 71 | assert Foo(None, "bar") == from_json(Foo, "{}") 72 | with pytest.raises(SerdeError): 73 | assert Foo("foo", "bar") == from_json(Foo, '{"a": "foo"}') 74 | 75 | 76 | def test_field_serialize_override_legacy_class_serializer(): 77 | def serializer(cls, o): 78 | if cls is datetime: 79 | return o.strftime("%d/%m/%y") 80 | else: 81 | raise SerdeSkip() 82 | 83 | def deserializer(cls, o): 84 | if cls is datetime: 85 | return datetime.strptime(o, "%d/%m/%y") 86 | else: 87 | raise SerdeSkip() 88 | 89 | @serde(serializer=serializer, deserializer=deserializer) 90 | class Foo: 91 | a: int 92 | b: datetime 93 | c: datetime = field( 94 | serializer=lambda x: x.strftime("%y.%m.%d"), 95 | deserializer=lambda x: datetime.strptime(x, "%y.%m.%d"), 96 | ) 97 | 98 | dt = datetime(2021, 1, 1, 0, 0, 0) 99 | f = Foo(10, dt, dt) 100 | 101 | assert to_json(f) == '{"a":10,"b":"01/01/21","c":"21.01.01"}' 102 | assert f == from_json(Foo, to_json(f)) 103 | 104 | assert to_tuple(f) == (10, "01/01/21", "21.01.01") 105 | assert f == from_tuple(Foo, to_tuple(f)) 106 | 107 | 108 | def test_override_by_default_serializer(): 109 | def serializer(cls, o): 110 | if cls is datetime: 111 | return o.strftime("%d/%m/%y") 112 | else: 113 | raise SerdeSkip() 114 | 115 | def deserializer(cls, o): 116 | if cls is datetime: 117 | return datetime.strptime(o, "%d/%m/%y") 118 | else: 119 | raise SerdeSkip() 120 | 121 | @serde(serializer=serializer, deserializer=deserializer) 122 | class Foo: 123 | a: int 124 | b: datetime 125 | c: datetime = field(serializer=default_serializer, deserializer=default_deserializer) 126 | 127 | dt = datetime(2021, 1, 1, 0, 0, 0) 128 | f = Foo(10, dt, dt) 129 | 130 | assert to_json(f) == '{"a":10,"b":"01/01/21","c":"2021-01-01T00:00:00"}' 131 | assert f == from_json(Foo, to_json(f)) 132 | 133 | assert to_tuple(f) == (10, "01/01/21", datetime(2021, 1, 1, 0, 0)) 134 | assert f == from_tuple(Foo, to_tuple(f)) 135 | -------------------------------------------------------------------------------- /tests/test_literal.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from dataclasses import dataclass 3 | from typing import Literal 4 | 5 | import pytest 6 | 7 | import serde 8 | 9 | from .common import ( 10 | all_formats, 11 | format_dict, 12 | format_json, 13 | format_msgpack, 14 | format_tuple, 15 | format_yaml, 16 | opt_case, 17 | opt_case_ids, 18 | ) 19 | 20 | log = logging.getLogger("test") 21 | 22 | serde.init(True) 23 | 24 | 25 | @serde.serde 26 | @dataclass(unsafe_hash=True) 27 | class LitInt: 28 | """ 29 | Integer. 30 | """ 31 | 32 | i: Literal[1, 2] 33 | 34 | 35 | @serde.serde 36 | @dataclass(unsafe_hash=True) 37 | class LitStr: 38 | """ 39 | String. 40 | """ 41 | 42 | s: Literal["foo", "bar"] 43 | 44 | 45 | @serde.serde 46 | @dataclass(unsafe_hash=True) 47 | class LitBool: 48 | """ 49 | Boolean. 50 | """ 51 | 52 | b: Literal[True] 53 | 54 | 55 | @serde.serde 56 | @dataclass(unsafe_hash=True) 57 | class LitMixed: 58 | """ 59 | Mixed Literal 60 | """ 61 | 62 | m: Literal[1, 2, "foo", "bar", False, True] 63 | 64 | 65 | @serde.serde 66 | @dataclass(unsafe_hash=True) 67 | class LitDict: 68 | """ 69 | Dict containing primitives. 70 | """ 71 | 72 | i: dict[Literal[1, 2], Literal[1, 2]] 73 | s: dict[Literal["foo", "bar"], Literal["foo", "bar"]] 74 | b: dict[Literal[True], Literal[True]] 75 | m: dict[ 76 | Literal[1, 2, "foo", "bar", False, True], 77 | Literal[1, 2, "foo", "bar", False, True], 78 | ] 79 | 80 | 81 | @serde.serde 82 | @dataclass(unsafe_hash=True) 83 | class LitStrDict: 84 | """ 85 | Dict containing primitives. 86 | """ 87 | 88 | i: dict[Literal["1", "2"], Literal[1, 2]] 89 | s: dict[Literal["foo", "bar"], Literal["foo", "bar"]] 90 | b: dict[Literal["True"], Literal[True]] 91 | m: dict[ 92 | Literal["1", "2", "foo", "bar", "False", "True"], 93 | Literal[1, 2, "foo", "bar", False, True], 94 | ] 95 | 96 | 97 | @serde.serde 98 | @dataclass(unsafe_hash=True) 99 | class Literals: 100 | """ 101 | Primitives. 102 | """ 103 | 104 | i: Literal[1, 2] 105 | s: Literal["foo", "bar"] 106 | b: Literal[True] 107 | m: Literal[1, 2, "foo", "bar", False, True] 108 | 109 | 110 | @serde.serde 111 | class LitNestedPrituple: 112 | """ 113 | tuple containing nested primitives. 114 | """ 115 | 116 | i: tuple[LitInt, LitInt] 117 | s: tuple[LitStr, LitStr] 118 | b: tuple[LitBool, LitBool, LitBool] 119 | m: tuple[LitMixed, LitMixed] 120 | 121 | 122 | listLiterals = list[Literals] 123 | PRI = Literals(1, "foo", True, 2) 124 | DictLiterals = dict[str, Literals] 125 | 126 | 127 | """ 128 | Tests 129 | """ 130 | 131 | 132 | @pytest.mark.parametrize("opt", opt_case, ids=opt_case_ids()) 133 | @pytest.mark.parametrize("se,de", all_formats) 134 | def test_dict(se, de, opt): 135 | if se in (serde.json.to_json, serde.msgpack.to_msgpack, serde.toml.to_toml): 136 | # JSON, Msgpack, Toml don't allow non string key. 137 | p = LitStrDict({"1": 2}, {"foo": "bar"}, {"True": True}, {"foo": True}) 138 | assert p == de(LitStrDict, se(p)) 139 | else: 140 | p = LitDict({1: 2}, {"foo": "bar"}, {True: True}, {2: False}) 141 | assert p == de(LitDict, se(p)) 142 | 143 | 144 | @pytest.mark.parametrize("opt", opt_case, ids=opt_case_ids()) 145 | @pytest.mark.parametrize("se,de", all_formats) 146 | def test_tuple(se, de, opt): 147 | if se is not serde.toml.to_toml: 148 | p = LitNestedPrituple( 149 | (LitInt(1), LitInt(2)), 150 | (LitStr("foo"), LitStr("bar")), 151 | (LitBool(True), LitBool(True), LitBool(True)), 152 | (LitMixed(False), LitMixed("foo")), 153 | ) 154 | assert p == de(LitNestedPrituple, se(p)) 155 | 156 | 157 | @pytest.mark.parametrize( 158 | "se,de", (format_dict + format_tuple + format_json + format_msgpack + format_yaml) 159 | ) 160 | def test_list_literals(se, de): 161 | p = [PRI, PRI] 162 | assert p == de(listLiterals, se(p)) 163 | 164 | p = [] 165 | assert p == de(listLiterals, se(p)) 166 | 167 | 168 | @pytest.mark.parametrize( 169 | "se,de", (format_dict + format_tuple + format_json + format_msgpack + format_yaml) 170 | ) 171 | def test_dict_literals(se, de): 172 | p = {"1": PRI, "2": PRI} 173 | assert p == de(DictLiterals, se(p)) 174 | 175 | p = {} 176 | assert p == de(DictLiterals, se(p)) 177 | 178 | 179 | def test_json(): 180 | p = Literals(1, "foo", True, "bar") 181 | s = '{"i":1,"s":"foo","b":true,"m":"bar"}' 182 | assert s == serde.json.to_json(p) 183 | 184 | 185 | def test_msgpack(): 186 | p = Literals(1, "foo", True, "bar") 187 | d = b"\x84\xa1i\x01\xa1s\xa3foo\xa1b\xc3\xa1m\xa3bar" 188 | assert d == serde.msgpack.to_msgpack(p) 189 | assert p == serde.msgpack.from_msgpack(Literals, d) 190 | 191 | 192 | def test_msgpack_unnamed(): 193 | p = Literals(1, "foo", True, "bar") 194 | d = b"\x94\x01\xa3foo\xc3\xa3bar" 195 | assert d == serde.msgpack.to_msgpack(p, named=False) 196 | assert p == serde.msgpack.from_msgpack(Literals, d, named=False) 197 | -------------------------------------------------------------------------------- /tests/test_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | import pytest 5 | 6 | import serde 7 | from .common import ( 8 | opt_case, 9 | opt_case_ids, 10 | FormatFuncs, 11 | format_dict, 12 | format_tuple, 13 | format_json, 14 | format_msgpack, 15 | format_yaml, 16 | format_pickle, 17 | format_toml, 18 | all_formats, 19 | ) 20 | 21 | sa = pytest.importorskip("sqlalchemy", "2.0.0") 22 | sa_orm = pytest.importorskip("sqlalchemy.orm") 23 | 24 | log = logging.getLogger("test") 25 | 26 | serde.init(True) 27 | 28 | all_except_toml: FormatFuncs = ( 29 | format_dict + format_tuple + format_json + format_msgpack + format_yaml + format_pickle 30 | ) 31 | 32 | 33 | @pytest.mark.parametrize("opt", opt_case, ids=opt_case_ids()) 34 | @pytest.mark.parametrize("se,de", all_except_toml) 35 | def test_sqlalchemy_simple(se, de, opt): 36 | log.info(f"Running test with se={se.__name__} de={de.__name__} opts={opt}") 37 | 38 | class Base(sa_orm.MappedAsDataclass, sa_orm.DeclarativeBase): 39 | pass 40 | 41 | @serde.serde(**opt) 42 | class User(Base): 43 | __tablename__ = "user" 44 | 45 | id: sa_orm.Mapped[int] = sa_orm.mapped_column(primary_key=True) 46 | name: sa_orm.Mapped[str] 47 | fullname: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String(30)) 48 | nickname: sa_orm.Mapped[Optional[str]] = sa_orm.mapped_column(init=False) 49 | 50 | user = User(1, "john", "John Doe") 51 | assert user == de(User, se(user)) 52 | 53 | 54 | @pytest.mark.parametrize("opt", opt_case, ids=opt_case_ids()) 55 | @pytest.mark.parametrize("se,de", format_toml) 56 | def test_sqlalchemy_simple_toml(se, de, opt): 57 | log.info(f"Running test with se={se.__name__} de={de.__name__} opts={opt}") 58 | 59 | class Base(sa_orm.MappedAsDataclass, sa_orm.DeclarativeBase): 60 | pass 61 | 62 | @serde.serde(**opt) 63 | class User(Base): 64 | __tablename__ = "user" 65 | 66 | id: sa_orm.Mapped[int] = sa_orm.mapped_column(primary_key=True) 67 | name: sa_orm.Mapped[str] 68 | fullname: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String(30)) 69 | 70 | user = User(1, "john", "John Doe") 71 | assert user == de(User, se(user)) 72 | 73 | 74 | @pytest.mark.parametrize("opt", opt_case, ids=opt_case_ids()) 75 | @pytest.mark.parametrize("se,de", all_formats) 76 | def test_sqlalchemy_nested(se, de, opt): 77 | log.info(f"Running test with se={se.__name__} de={de.__name__} opts={opt}") 78 | 79 | class Base(sa_orm.MappedAsDataclass, sa_orm.DeclarativeBase): 80 | pass 81 | 82 | @serde.serde(**opt) 83 | class Project(Base): 84 | __tablename__ = "projects" 85 | id: sa_orm.Mapped[int] = sa_orm.mapped_column(primary_key=True) 86 | name: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.Text, nullable=False) 87 | owner_id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.ForeignKey("users.id")) 88 | 89 | @serde.serde(**opt) 90 | class User(Base): 91 | __tablename__ = "users" 92 | 93 | id: sa_orm.Mapped[int] = sa_orm.mapped_column(primary_key=True) 94 | name: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.Text, nullable=False) 95 | fullname: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.Text, nullable=False) 96 | projects: sa_orm.Mapped[list[Project]] = sa_orm.relationship(backref="owner") 97 | 98 | user = User(1, "john", "John Doe", [Project(1, "Dummy", 1)]) 99 | assert user == de(User, se(user)) 100 | -------------------------------------------------------------------------------- /tests/test_toml.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.toml import to_toml, from_toml 3 | from typing import Optional 4 | import pytest 5 | 6 | 7 | def test_toml_basics() -> None: 8 | @serde 9 | class Foo: 10 | v: Optional[int] 11 | 12 | f = Foo(10) 13 | assert "v = 10\n" == to_toml(f) 14 | assert f == from_toml(Foo, "v = 10\n") 15 | 16 | @serde 17 | class Bar: 18 | v: set[int] 19 | 20 | toml_literal = """\ 21 | v = [ 22 | 1, 23 | 2, 24 | 3, 25 | ] 26 | """ 27 | b = Bar({1, 2, 3}) 28 | assert toml_literal == to_toml(b) 29 | assert b == from_toml(Bar, toml_literal) 30 | 31 | 32 | def test_skip_none() -> None: 33 | @serde 34 | class Foo: 35 | a: int 36 | b: Optional[int] 37 | 38 | f = Foo(10, 100) 39 | assert ( 40 | to_toml(f) 41 | == """\ 42 | a = 10 43 | b = 100 44 | """ 45 | ) 46 | 47 | f = Foo(10, None) 48 | assert ( 49 | to_toml(f) 50 | == """\ 51 | a = 10 52 | """ 53 | ) 54 | 55 | 56 | def test_skip_none_container_not_supported_yet() -> None: 57 | @serde 58 | class Foo: 59 | a: int 60 | b: list[Optional[int]] 61 | 62 | f = Foo(10, [100, None]) 63 | with pytest.raises(TypeError): 64 | to_toml(f) 65 | -------------------------------------------------------------------------------- /tests/test_type_alias.py: -------------------------------------------------------------------------------- 1 | from serde import serde, from_dict, to_dict 2 | 3 | 4 | type S = str 5 | 6 | 7 | def test_pep695_type_alias() -> None: 8 | 9 | @serde 10 | class Foo: 11 | s: S 12 | 13 | f = Foo("foo") 14 | assert f == from_dict(Foo, to_dict(f)) 15 | 16 | 17 | @serde 18 | class Bar: 19 | a: int 20 | 21 | 22 | @serde 23 | class Baz: 24 | b: int 25 | 26 | 27 | type BarBaz = Bar | Baz 28 | 29 | 30 | def test_pep695_type_alias_union() -> None: 31 | @serde 32 | class Foo: 33 | barbaz: BarBaz 34 | 35 | f = Foo(Baz(10)) 36 | assert f == from_dict(Foo, to_dict(f)) 37 | -------------------------------------------------------------------------------- /tests/test_yaml.py: -------------------------------------------------------------------------------- 1 | from serde import serde 2 | from serde.yaml import to_yaml, from_yaml 3 | from typing import Optional 4 | 5 | 6 | def test_yaml_basics() -> None: 7 | @serde 8 | class Foo: 9 | v: Optional[int] 10 | 11 | f = Foo(10) 12 | assert "v: 10\n" == to_yaml(f) 13 | assert f == from_yaml(Foo, "v: 10\n") 14 | 15 | @serde 16 | class Bar: 17 | v: set[int] 18 | 19 | b = Bar({1, 2, 3}) 20 | assert "v:\n- 1\n- 2\n- 3\n" == to_yaml(b) 21 | assert b == from_yaml(Bar, "v:\n- 1\n- 2\n- 3\n") 22 | 23 | 24 | def test_skip_none() -> None: 25 | @serde 26 | class Foo: 27 | a: int 28 | b: Optional[int] 29 | 30 | f = Foo(10, 100) 31 | assert ( 32 | to_yaml(f, skip_none=True) 33 | == """\ 34 | a: 10 35 | b: 100 36 | """ 37 | ) 38 | 39 | f = Foo(10, None) 40 | assert ( 41 | to_yaml(f, skip_none=True) 42 | == """\ 43 | a: 10 44 | """ 45 | ) 46 | --------------------------------------------------------------------------------