├── .github └── workflows │ ├── CD.yml │ └── CI.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bench ├── __init__.py ├── requirements.txt └── run.py ├── examples └── django_example │ ├── Dockerfile │ ├── db.sqlite3 │ ├── django_example │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ ├── docker-compose.yml │ ├── manage.py │ ├── products │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── schema.py │ ├── tests.py │ ├── urls.py │ └── views.py │ └── requirements.txt ├── pyproject.toml ├── schemars ├── __init__.py └── _schemars.pyi ├── src ├── errors.rs ├── fields │ ├── any.rs │ ├── base.rs │ ├── bool.rs │ ├── bytes.rs │ ├── date.rs │ ├── datetime.rs │ ├── decimal.rs │ ├── dict.rs │ ├── float.rs │ ├── int.rs │ ├── list.rs │ ├── method.rs │ ├── mod.rs │ ├── str.rs │ ├── union.rs │ └── uuid.rs ├── lib.rs ├── macros.rs └── schema.rs └── tests ├── test_any_field.py ├── test_bool_field.py ├── test_bytes_field.py ├── test_date_field.py ├── test_datetime_field.py ├── test_decimal_field.py ├── test_dict_field.py ├── test_float_field.py ├── test_int_field.py ├── test_list_field.py ├── test_method_field.py ├── test_schema.py ├── test_str_field.py ├── test_union_field.py └── test_uuid_field.py /.github/workflows/CD.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | linux: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: ["3.7", "3.8", "3.9", "3.10"] 17 | target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Build wheels 24 | uses: PyO3/maturin-action@v1 25 | with: 26 | target: ${{ matrix.target }} 27 | args: --release --out dist --find-interpreter --no-default-features 28 | sccache: "true" 29 | manylinux: auto 30 | - name: Upload wheels 31 | uses: actions/upload-artifact@v3 32 | with: 33 | name: dist 34 | path: dist 35 | 36 | windows: 37 | runs-on: windows-latest 38 | strategy: 39 | matrix: 40 | python-version: ["3.7", "3.8", "3.9", "3.10"] 41 | target: [x64, x86] 42 | steps: 43 | - uses: actions/checkout@v3 44 | - uses: actions/setup-python@v4 45 | with: 46 | python-version: ${{ matrix.python-version }} 47 | architecture: ${{ matrix.target }} 48 | - name: Build wheels 49 | uses: PyO3/maturin-action@v1 50 | with: 51 | target: ${{ matrix.target }} 52 | args: --release --out dist --find-interpreter 53 | sccache: "true" 54 | - name: Upload wheels 55 | uses: actions/upload-artifact@v3 56 | with: 57 | name: dist 58 | path: dist 59 | 60 | macos: 61 | runs-on: macos-latest 62 | strategy: 63 | matrix: 64 | python-version: ["3.7", "3.8", "3.9", "3.10"] 65 | target: [x86_64, aarch64] 66 | steps: 67 | - uses: actions/checkout@v3 68 | - uses: actions/setup-python@v4 69 | with: 70 | python-version: "3.10" 71 | - name: Build wheels 72 | uses: PyO3/maturin-action@v1 73 | with: 74 | target: ${{ matrix.target }} 75 | args: --release --out dist --find-interpreter 76 | sccache: "true" 77 | - name: Upload wheels 78 | uses: actions/upload-artifact@v3 79 | with: 80 | name: dist 81 | path: dist 82 | 83 | sdist: 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@v3 87 | - name: Build sdist 88 | uses: PyO3/maturin-action@v1 89 | with: 90 | command: sdist 91 | args: --out dist 92 | - name: Upload sdist 93 | uses: actions/upload-artifact@v3 94 | with: 95 | name: dist 96 | path: dist 97 | 98 | release: 99 | name: Release 100 | runs-on: ubuntu-latest 101 | needs: [linux, windows, macos, sdist] 102 | steps: 103 | - uses: actions/download-artifact@v3 104 | with: 105 | name: dist 106 | path: dist 107 | - name: Publish to PyPI 108 | uses: PyO3/maturin-action@v1 109 | env: 110 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 111 | with: 112 | command: upload 113 | args: --skip-existing dist/* 114 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | # Cargo check. 11 | check: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout sources 15 | uses: actions/checkout@v2 16 | 17 | - name: Install stable toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | 24 | - name: Run cargo check 25 | uses: actions-rs/cargo@v1 26 | with: 27 | command: check 28 | args: --all 29 | 30 | # Cargo fmt and clippy 31 | lints: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout sources 35 | uses: actions/checkout@v2 36 | 37 | - name: Install stable toolchain 38 | uses: actions-rs/toolchain@v1 39 | with: 40 | profile: minimal 41 | toolchain: stable 42 | override: true 43 | components: rustfmt, clippy 44 | 45 | - name: Run cargo fmt 46 | uses: actions-rs/cargo@v1 47 | with: 48 | command: fmt 49 | args: --all -- --check 50 | 51 | - name: Run cargo clippy 52 | uses: actions-rs/cargo@v1 53 | with: 54 | command: clippy 55 | args: --all -- -D warnings -A deprecated 56 | 57 | # Pytest 58 | test: 59 | runs-on: ubuntu-20.04 60 | strategy: 61 | matrix: 62 | python-version: [3.7, 3.8, 3.9, "3.10.12"] 63 | 64 | steps: 65 | - name: Checkout sources 66 | uses: actions/checkout@v2 67 | 68 | - name: Set up Python 69 | uses: actions/setup-python@v4 70 | with: 71 | python-version: ${{ matrix.python-version }} 72 | 73 | - name: Create and activate virtual environment 74 | run: | 75 | python -m venv .venv 76 | echo ".venv/bin" >> $GITHUB_PATH 77 | 78 | - name: Install Rust 79 | uses: actions-rs/toolchain@v1 80 | with: 81 | profile: minimal 82 | toolchain: stable 83 | override: true 84 | 85 | - name: Install maturin 86 | run: cargo install maturin 87 | 88 | - name: Build and Install Python Package 89 | run: | 90 | source .venv/bin/activate 91 | maturin develop --release 92 | 93 | - name: Install pytest 94 | run: | 95 | source .venv/bin/activate 96 | pip install pytest 97 | 98 | - name: Run pytest 99 | run: | 100 | source .venv/bin/activate 101 | pytest tests 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | .pytest_cache/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | .venv/ 14 | env/ 15 | bin/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | include/ 26 | man/ 27 | venv/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | pip-selfcheck.json 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | 60 | .DS_Store 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyCharm 66 | .idea/ 67 | 68 | # VSCode 69 | .vscode/ 70 | 71 | # Pyenv 72 | .python-version -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 0.1.0 (November 18, 2023) 2 | ------------------------------ 3 | 4 | Initial release! 5 | 6 | ## Version 0.2.0 (November 19, 2023) 7 | ------------------------------ 8 | 9 | * Update CD @Mng-dev-ai in [#4](https://github.com/Mng-dev-ai/schemars/pull/4) 10 | 11 | ## Version 0.3.0 (November 19, 2023) 12 | ------------------------------ 13 | 14 | * Add call to schema trait @Mng-dev-ai in [#7](https://github.com/Mng-dev-ai/schemars/pull/7) 15 | 16 | ## Version 0.3.1 (November 19, 2023) 17 | ------------------------------ 18 | 19 | * Enhance serialization to support general iterables @Mng-dev-ai in [#9](https://github.com/Mng-dev-ai/schemars/pull/9) 20 | 21 | ## Version 0.3.2 (November 20, 2023) 22 | ------------------------------ 23 | 24 | * Allow schema to store custom attributes @Mng-dev-ai in [#11](https://github.com/Mng-dev-ai/schemars/pull/11) 25 | * Add examples @Mng-dev-ai in [#12](https://github.com/Mng-dev-ai/schemars/pull/12) 26 | 27 | 28 | ## Version 0.4.0 (November 25, 2023) 29 | ------------------------------ 30 | 31 | * Add child to Dict field @Mng-dev-ai in [#16](https://github.com/Mng-dev-ai/schemars/pull/16) 32 | * improvements @Mng-dev-ai in [#17](https://github.com/Mng-dev-ai/schemars/pull/17) 33 | * Add UUID field @Mng-dev-ai in [#18](https://github.com/Mng-dev-ai/schemars/pull/18) 34 | * Add Any field @Mng-dev-ai in [#19](https://github.com/Mng-dev-ai/schemars/pull/19) 35 | * Add alias support for fields @Mng-dev-ai in [#20](https://github.com/Mng-dev-ai/schemars/pull/20) 36 | * Add mimalloc @Mng-dev-ai in [#21](https://github.com/Mng-dev-ai/schemars/pull/21) 37 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.7.7" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" 10 | dependencies = [ 11 | "getrandom", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "ahash" 18 | version = "0.8.6" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" 21 | dependencies = [ 22 | "cfg-if", 23 | "once_cell", 24 | "version_check", 25 | "zerocopy", 26 | ] 27 | 28 | [[package]] 29 | name = "arrayvec" 30 | version = "0.7.4" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 33 | 34 | [[package]] 35 | name = "autocfg" 36 | version = "1.1.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 39 | 40 | [[package]] 41 | name = "bitflags" 42 | version = "1.3.2" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 45 | 46 | [[package]] 47 | name = "bitvec" 48 | version = "1.0.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 51 | dependencies = [ 52 | "funty", 53 | "radium", 54 | "tap", 55 | "wyz", 56 | ] 57 | 58 | [[package]] 59 | name = "borsh" 60 | version = "0.10.3" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" 63 | dependencies = [ 64 | "borsh-derive", 65 | "hashbrown 0.13.2", 66 | ] 67 | 68 | [[package]] 69 | name = "borsh-derive" 70 | version = "0.10.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" 73 | dependencies = [ 74 | "borsh-derive-internal", 75 | "borsh-schema-derive-internal", 76 | "proc-macro-crate", 77 | "proc-macro2", 78 | "syn 1.0.109", 79 | ] 80 | 81 | [[package]] 82 | name = "borsh-derive-internal" 83 | version = "0.10.3" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" 86 | dependencies = [ 87 | "proc-macro2", 88 | "quote", 89 | "syn 1.0.109", 90 | ] 91 | 92 | [[package]] 93 | name = "borsh-schema-derive-internal" 94 | version = "0.10.3" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" 97 | dependencies = [ 98 | "proc-macro2", 99 | "quote", 100 | "syn 1.0.109", 101 | ] 102 | 103 | [[package]] 104 | name = "bytecheck" 105 | version = "0.6.11" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" 108 | dependencies = [ 109 | "bytecheck_derive", 110 | "ptr_meta", 111 | "simdutf8", 112 | ] 113 | 114 | [[package]] 115 | name = "bytecheck_derive" 116 | version = "0.6.11" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" 119 | dependencies = [ 120 | "proc-macro2", 121 | "quote", 122 | "syn 1.0.109", 123 | ] 124 | 125 | [[package]] 126 | name = "bytes" 127 | version = "1.5.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 130 | 131 | [[package]] 132 | name = "cc" 133 | version = "1.0.83" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 136 | dependencies = [ 137 | "libc", 138 | ] 139 | 140 | [[package]] 141 | name = "cfg-if" 142 | version = "1.0.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 145 | 146 | [[package]] 147 | name = "funty" 148 | version = "2.0.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 151 | 152 | [[package]] 153 | name = "getrandom" 154 | version = "0.2.11" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 157 | dependencies = [ 158 | "cfg-if", 159 | "libc", 160 | "wasi", 161 | ] 162 | 163 | [[package]] 164 | name = "hashbrown" 165 | version = "0.12.3" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 168 | dependencies = [ 169 | "ahash 0.7.7", 170 | ] 171 | 172 | [[package]] 173 | name = "hashbrown" 174 | version = "0.13.2" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 177 | dependencies = [ 178 | "ahash 0.8.6", 179 | ] 180 | 181 | [[package]] 182 | name = "heck" 183 | version = "0.4.1" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 186 | 187 | [[package]] 188 | name = "indoc" 189 | version = "2.0.4" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" 192 | 193 | [[package]] 194 | name = "itoa" 195 | version = "1.0.9" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 198 | 199 | [[package]] 200 | name = "libc" 201 | version = "0.2.149" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" 204 | 205 | [[package]] 206 | name = "libmimalloc-sys" 207 | version = "0.1.35" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "3979b5c37ece694f1f5e51e7ecc871fdb0f517ed04ee45f88d15d6d553cb9664" 210 | dependencies = [ 211 | "cc", 212 | "libc", 213 | ] 214 | 215 | [[package]] 216 | name = "lock_api" 217 | version = "0.4.11" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 220 | dependencies = [ 221 | "autocfg", 222 | "scopeguard", 223 | ] 224 | 225 | [[package]] 226 | name = "memoffset" 227 | version = "0.9.0" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" 230 | dependencies = [ 231 | "autocfg", 232 | ] 233 | 234 | [[package]] 235 | name = "mimalloc" 236 | version = "0.1.39" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "fa01922b5ea280a911e323e4d2fd24b7fe5cc4042e0d2cda3c40775cdc4bdc9c" 239 | dependencies = [ 240 | "libmimalloc-sys", 241 | ] 242 | 243 | [[package]] 244 | name = "num-traits" 245 | version = "0.2.17" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 248 | dependencies = [ 249 | "autocfg", 250 | ] 251 | 252 | [[package]] 253 | name = "once_cell" 254 | version = "1.18.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 257 | 258 | [[package]] 259 | name = "parking_lot" 260 | version = "0.12.1" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 263 | dependencies = [ 264 | "lock_api", 265 | "parking_lot_core", 266 | ] 267 | 268 | [[package]] 269 | name = "parking_lot_core" 270 | version = "0.9.9" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 273 | dependencies = [ 274 | "cfg-if", 275 | "libc", 276 | "redox_syscall", 277 | "smallvec", 278 | "windows-targets", 279 | ] 280 | 281 | [[package]] 282 | name = "ppv-lite86" 283 | version = "0.2.17" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 286 | 287 | [[package]] 288 | name = "proc-macro-crate" 289 | version = "0.1.5" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" 292 | dependencies = [ 293 | "toml", 294 | ] 295 | 296 | [[package]] 297 | name = "proc-macro2" 298 | version = "1.0.69" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 301 | dependencies = [ 302 | "unicode-ident", 303 | ] 304 | 305 | [[package]] 306 | name = "ptr_meta" 307 | version = "0.1.4" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" 310 | dependencies = [ 311 | "ptr_meta_derive", 312 | ] 313 | 314 | [[package]] 315 | name = "ptr_meta_derive" 316 | version = "0.1.4" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" 319 | dependencies = [ 320 | "proc-macro2", 321 | "quote", 322 | "syn 1.0.109", 323 | ] 324 | 325 | [[package]] 326 | name = "pyo3" 327 | version = "0.20.0" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "04e8453b658fe480c3e70c8ed4e3d3ec33eb74988bd186561b0cc66b85c3bc4b" 330 | dependencies = [ 331 | "cfg-if", 332 | "indoc", 333 | "libc", 334 | "memoffset", 335 | "parking_lot", 336 | "pyo3-build-config", 337 | "pyo3-ffi", 338 | "pyo3-macros", 339 | "unindent", 340 | ] 341 | 342 | [[package]] 343 | name = "pyo3-build-config" 344 | version = "0.20.0" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "a96fe70b176a89cff78f2fa7b3c930081e163d5379b4dcdf993e3ae29ca662e5" 347 | dependencies = [ 348 | "once_cell", 349 | "target-lexicon", 350 | ] 351 | 352 | [[package]] 353 | name = "pyo3-ffi" 354 | version = "0.20.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "214929900fd25e6604661ed9cf349727c8920d47deff196c4e28165a6ef2a96b" 357 | dependencies = [ 358 | "libc", 359 | "pyo3-build-config", 360 | ] 361 | 362 | [[package]] 363 | name = "pyo3-macros" 364 | version = "0.20.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b" 367 | dependencies = [ 368 | "proc-macro2", 369 | "pyo3-macros-backend", 370 | "quote", 371 | "syn 2.0.38", 372 | ] 373 | 374 | [[package]] 375 | name = "pyo3-macros-backend" 376 | version = "0.20.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424" 379 | dependencies = [ 380 | "heck", 381 | "proc-macro2", 382 | "quote", 383 | "syn 2.0.38", 384 | ] 385 | 386 | [[package]] 387 | name = "quote" 388 | version = "1.0.33" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 391 | dependencies = [ 392 | "proc-macro2", 393 | ] 394 | 395 | [[package]] 396 | name = "radium" 397 | version = "0.7.0" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 400 | 401 | [[package]] 402 | name = "rand" 403 | version = "0.8.5" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 406 | dependencies = [ 407 | "libc", 408 | "rand_chacha", 409 | "rand_core", 410 | ] 411 | 412 | [[package]] 413 | name = "rand_chacha" 414 | version = "0.3.1" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 417 | dependencies = [ 418 | "ppv-lite86", 419 | "rand_core", 420 | ] 421 | 422 | [[package]] 423 | name = "rand_core" 424 | version = "0.6.4" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 427 | dependencies = [ 428 | "getrandom", 429 | ] 430 | 431 | [[package]] 432 | name = "redox_syscall" 433 | version = "0.4.1" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 436 | dependencies = [ 437 | "bitflags", 438 | ] 439 | 440 | [[package]] 441 | name = "rend" 442 | version = "0.4.1" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd" 445 | dependencies = [ 446 | "bytecheck", 447 | ] 448 | 449 | [[package]] 450 | name = "rkyv" 451 | version = "0.7.42" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" 454 | dependencies = [ 455 | "bitvec", 456 | "bytecheck", 457 | "hashbrown 0.12.3", 458 | "ptr_meta", 459 | "rend", 460 | "rkyv_derive", 461 | "seahash", 462 | "tinyvec", 463 | "uuid", 464 | ] 465 | 466 | [[package]] 467 | name = "rkyv_derive" 468 | version = "0.7.42" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" 471 | dependencies = [ 472 | "proc-macro2", 473 | "quote", 474 | "syn 1.0.109", 475 | ] 476 | 477 | [[package]] 478 | name = "rust_decimal" 479 | version = "1.32.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" 482 | dependencies = [ 483 | "arrayvec", 484 | "borsh", 485 | "bytes", 486 | "num-traits", 487 | "rand", 488 | "rkyv", 489 | "serde", 490 | "serde_json", 491 | ] 492 | 493 | [[package]] 494 | name = "rustversion" 495 | version = "1.0.14" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" 498 | 499 | [[package]] 500 | name = "ryu" 501 | version = "1.0.15" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 504 | 505 | [[package]] 506 | name = "schemars" 507 | version = "0.4.0" 508 | dependencies = [ 509 | "mimalloc", 510 | "pyo3", 511 | "pyo3-build-config", 512 | "rust_decimal", 513 | "speedate", 514 | "uuid", 515 | "version_check", 516 | ] 517 | 518 | [[package]] 519 | name = "scopeguard" 520 | version = "1.2.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 523 | 524 | [[package]] 525 | name = "seahash" 526 | version = "4.1.0" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" 529 | 530 | [[package]] 531 | name = "serde" 532 | version = "1.0.192" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" 535 | dependencies = [ 536 | "serde_derive", 537 | ] 538 | 539 | [[package]] 540 | name = "serde_derive" 541 | version = "1.0.192" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" 544 | dependencies = [ 545 | "proc-macro2", 546 | "quote", 547 | "syn 2.0.38", 548 | ] 549 | 550 | [[package]] 551 | name = "serde_json" 552 | version = "1.0.108" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 555 | dependencies = [ 556 | "itoa", 557 | "ryu", 558 | "serde", 559 | ] 560 | 561 | [[package]] 562 | name = "simdutf8" 563 | version = "0.1.4" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" 566 | 567 | [[package]] 568 | name = "smallvec" 569 | version = "1.11.1" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" 572 | 573 | [[package]] 574 | name = "speedate" 575 | version = "0.13.0" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "242f76c50fd18cbf098607090ade73a08d39cfd84ea835f3796a2c855223b19b" 578 | dependencies = [ 579 | "strum", 580 | "strum_macros", 581 | ] 582 | 583 | [[package]] 584 | name = "strum" 585 | version = "0.25.0" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" 588 | dependencies = [ 589 | "strum_macros", 590 | ] 591 | 592 | [[package]] 593 | name = "strum_macros" 594 | version = "0.25.3" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" 597 | dependencies = [ 598 | "heck", 599 | "proc-macro2", 600 | "quote", 601 | "rustversion", 602 | "syn 2.0.38", 603 | ] 604 | 605 | [[package]] 606 | name = "syn" 607 | version = "1.0.109" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 610 | dependencies = [ 611 | "proc-macro2", 612 | "quote", 613 | "unicode-ident", 614 | ] 615 | 616 | [[package]] 617 | name = "syn" 618 | version = "2.0.38" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" 621 | dependencies = [ 622 | "proc-macro2", 623 | "quote", 624 | "unicode-ident", 625 | ] 626 | 627 | [[package]] 628 | name = "tap" 629 | version = "1.0.1" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 632 | 633 | [[package]] 634 | name = "target-lexicon" 635 | version = "0.12.12" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" 638 | 639 | [[package]] 640 | name = "tinyvec" 641 | version = "1.6.0" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 644 | dependencies = [ 645 | "tinyvec_macros", 646 | ] 647 | 648 | [[package]] 649 | name = "tinyvec_macros" 650 | version = "0.1.1" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 653 | 654 | [[package]] 655 | name = "toml" 656 | version = "0.5.11" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 659 | dependencies = [ 660 | "serde", 661 | ] 662 | 663 | [[package]] 664 | name = "unicode-ident" 665 | version = "1.0.12" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 668 | 669 | [[package]] 670 | name = "unindent" 671 | version = "0.2.3" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" 674 | 675 | [[package]] 676 | name = "uuid" 677 | version = "1.6.1" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" 680 | 681 | [[package]] 682 | name = "version_check" 683 | version = "0.9.4" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 686 | 687 | [[package]] 688 | name = "wasi" 689 | version = "0.11.0+wasi-snapshot-preview1" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 692 | 693 | [[package]] 694 | name = "windows-targets" 695 | version = "0.48.5" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 698 | dependencies = [ 699 | "windows_aarch64_gnullvm", 700 | "windows_aarch64_msvc", 701 | "windows_i686_gnu", 702 | "windows_i686_msvc", 703 | "windows_x86_64_gnu", 704 | "windows_x86_64_gnullvm", 705 | "windows_x86_64_msvc", 706 | ] 707 | 708 | [[package]] 709 | name = "windows_aarch64_gnullvm" 710 | version = "0.48.5" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 713 | 714 | [[package]] 715 | name = "windows_aarch64_msvc" 716 | version = "0.48.5" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 719 | 720 | [[package]] 721 | name = "windows_i686_gnu" 722 | version = "0.48.5" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 725 | 726 | [[package]] 727 | name = "windows_i686_msvc" 728 | version = "0.48.5" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 731 | 732 | [[package]] 733 | name = "windows_x86_64_gnu" 734 | version = "0.48.5" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 737 | 738 | [[package]] 739 | name = "windows_x86_64_gnullvm" 740 | version = "0.48.5" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 743 | 744 | [[package]] 745 | name = "windows_x86_64_msvc" 746 | version = "0.48.5" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 749 | 750 | [[package]] 751 | name = "wyz" 752 | version = "0.5.1" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 755 | dependencies = [ 756 | "tap", 757 | ] 758 | 759 | [[package]] 760 | name = "zerocopy" 761 | version = "0.7.25" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" 764 | dependencies = [ 765 | "zerocopy-derive", 766 | ] 767 | 768 | [[package]] 769 | name = "zerocopy-derive" 770 | version = "0.7.25" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" 773 | dependencies = [ 774 | "proc-macro2", 775 | "quote", 776 | "syn 2.0.38", 777 | ] 778 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "schemars" 3 | version = "0.4.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | name = "schemars" 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | pyo3 = "0.20.0" 12 | speedate = "0.13.0" 13 | rust_decimal = "1.32.0" 14 | uuid = "1.6.1" 15 | mimalloc = { version = "0.1.34", optional = true, default-features = false, features = ["local_dynamic_tls"] } 16 | 17 | [features] 18 | extension-module = ["pyo3/extension-module"] 19 | default = ["mimalloc"] 20 | 21 | [profile.release] 22 | codegen-units = 1 23 | debug = false 24 | incremental = false 25 | lto = true 26 | opt-level = 3 27 | panic = "abort" 28 | strip = true 29 | 30 | [dev-dependencies] 31 | pyo3 = { version = "0.20.0", features = ["auto-initialize"] } 32 | 33 | [build-dependencies] 34 | version_check = "0.9.4" 35 | pyo3-build-config = { version = "0.20.0" } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023, Michael Gendy 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Schemars 3 | [![PyPI version](https://badge.fury.io/py/schemars.svg)](https://badge.fury.io/py/schemars) 4 | [![Downloads](https://static.pepy.tech/personalized-badge/schemars?period=total&units=international_system&left_color=black&right_color=brightgreen&left_text=Downloads)](https://pepy.tech/project/schemars) 5 | 6 | ## Introduction 7 | Schemars is a Python package, written in Rust and leveraging PyO3, designed for efficient and flexible serialization of Python class instances. It provides a simple yet powerful schema-based approach to serialize complex Python objects. 8 | 9 | ## Installation 10 | To install Schemars, run the following command: 11 | ``` 12 | pip install schemars 13 | ``` 14 | 15 | ## Requirements 16 | - Python 3.x 17 | - Rust (optional for development) 18 | 19 | ## Usage 20 | To use Schemars, define your Python class and corresponding schema class, then serialize instances as shown below: 21 | 22 | ```python 23 | class Product: 24 | def __init__(self, name, price, created): 25 | self.name = name 26 | self.price = price 27 | self.created = created 28 | 29 | product = Product("test", 10, "1577836800") 30 | 31 | import schemars 32 | class ProductSchema(schemars.Schema): 33 | name = schemars.Str(strict=True) 34 | price = schemars.Decimal() 35 | created = schemars.Date(format='%Y/%m/%d') 36 | 37 | print(ProductSchema().serialize(product)) 38 | ``` 39 | 40 | ## Documentation Coming Soon! 41 | We are currently working on comprehensive documentation for Schemars, which will cover detailed usage, advanced features, and integration guides. Stay tuned for updates, and feel free to reach out to us with any specific questions or suggestions in the meantime. 42 | 43 | ## Upcoming in Version 1.0 44 | In the upcoming Version 1.0 of Schemars, we will be introducing additional functionalities including both validation and deserialization. This enhancement aims to provide a more comprehensive and robust experience in handling and processing Python class instances. 45 | 46 | ## Inspired by Marshmallow and Django REST Framework 47 | Schemars was developed in response to performance challenges encountered with existing serialization tools like Marshmallow and Django REST Framework. Our goal was to create a solution that not only addresses these performance issues but also remains user-friendly and familiar. 48 | 49 | ## Easy Migration 50 | If you're already using Marshmallow or Django REST Framework, you'll find Schemars's syntax and usage comfortably similar. This design choice is intentional to ensure that migration to Schemars is smooth and can be accomplished in just minutes. We have prioritized maintaining a familiar interface while significantly enhancing performance, so you can switch to Schemars with minimal adjustments to your existing codebase. 51 | 52 | # Benchmarking Schemars 53 | 54 | ## Quick Benchmark Setup 55 | To compare Schemars with Django REST Framework (DRF), Marshmallow and Pydantic, follow these steps: 56 | 57 | ### Installation 58 | Install benchmarking requirements: 59 | ```bash 60 | python3 -m pip install -r bench/requirements.txt 61 | ``` 62 | 63 | ### Running the Benchmark 64 | Execute the benchmark script: 65 | ```bash 66 | python3 bench/run.py 67 | ``` 68 | 69 | This will run serialization tasks using Schemars, Marshmallow, DRF and Pydantic, allowing you to directly compare their performance. 70 | ## License 71 | Schemars is released under the [MIT License] 72 | -------------------------------------------------------------------------------- /bench/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mng-dev-ai/schemars/44681c2220ef9b8f9aa5b3ef7c18c15a58178c7e/bench/__init__.py -------------------------------------------------------------------------------- /bench/requirements.txt: -------------------------------------------------------------------------------- 1 | schemars 2 | marshmallow 3 | djangorestframework 4 | pydantic -------------------------------------------------------------------------------- /bench/run.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from django.conf import settings 3 | from marshmallow import Schema, fields 4 | import schemars 5 | import random 6 | import time 7 | from datetime import date 8 | import string 9 | from pydantic import BaseModel 10 | from typing import List 11 | 12 | settings.configure() 13 | 14 | 15 | class InnerSerializer(serializers.Serializer): 16 | field1 = serializers.IntegerField() 17 | field2 = serializers.CharField() 18 | 19 | 20 | class MiddleSerializer(serializers.Serializer): 21 | inner = InnerSerializer(many=True) 22 | field3 = serializers.CharField() 23 | 24 | 25 | class OuterSerializer(serializers.Serializer): 26 | middle = MiddleSerializer(many=True) 27 | field4 = serializers.DateField() 28 | 29 | 30 | class InnerSchema(Schema): 31 | field1 = fields.Int() 32 | field2 = fields.Str() 33 | 34 | 35 | class MiddleSchema(Schema): 36 | inner = fields.Nested(InnerSchema, many=True) 37 | field3 = fields.Str() 38 | 39 | 40 | class OuterSchema(Schema): 41 | middle = fields.Nested(MiddleSchema, many=True) 42 | field4 = fields.Date() 43 | 44 | 45 | class SchemarsInnerSchema(schemars.Schema): 46 | field1 = schemars.Int() 47 | field2 = schemars.Str() 48 | 49 | 50 | class SchemarsMiddleSchema(schemars.Schema): 51 | inner = SchemarsInnerSchema(many=True) 52 | field3 = schemars.Str() 53 | 54 | 55 | class SchemarsOuterSchema(schemars.Schema): 56 | middle = SchemarsMiddleSchema(many=True) 57 | field4 = schemars.Date() 58 | 59 | 60 | def random_string(length=5): 61 | letters = string.ascii_lowercase 62 | return "".join(random.choice(letters) for _ in range(length)) 63 | 64 | 65 | class Inner: 66 | def __init__(self, field1, field2): 67 | self.field1 = field1 68 | self.field2 = field2 69 | 70 | 71 | class Middle: 72 | def __init__(self, inner, field3): 73 | self.inner = inner 74 | self.field3 = field3 75 | 76 | 77 | class Outer: 78 | def __init__(self, middle, field4): 79 | self.middle = middle 80 | self.field4 = field4 81 | 82 | 83 | instances = [ 84 | Outer( 85 | middle=[ 86 | Middle( 87 | inner=[ 88 | Inner(field1=random.randint(0, 100), field2=random_string()) 89 | for _ in range(10) 90 | ], 91 | field3=random_string(), 92 | ) 93 | for _ in range(10) 94 | ], 95 | field4=date.today(), 96 | ) 97 | for _ in range(1000) 98 | ] 99 | 100 | 101 | class PydanticInnerSchema(BaseModel): 102 | field1: int 103 | field2: str 104 | 105 | 106 | class PydanticMiddleSchema(BaseModel): 107 | inner: List[PydanticInnerSchema] 108 | field3: str 109 | 110 | 111 | class PydanticOuterSchema(BaseModel): 112 | middle: List[PydanticMiddleSchema] 113 | field4: date 114 | 115 | 116 | pydantic_instances = [ 117 | PydanticOuterSchema( 118 | middle=[ 119 | PydanticMiddleSchema( 120 | inner=[ 121 | PydanticInnerSchema( 122 | field1=random.randint(0, 100), field2=random_string() 123 | ) 124 | for _ in range(10) 125 | ], 126 | field3=random_string(), 127 | ) 128 | for _ in range(10) 129 | ], 130 | field4=date.today(), 131 | ) 132 | for _ in range(1000) 133 | ] 134 | 135 | marshmallow_schema = OuterSchema() 136 | start_time = time.time() 137 | marshmallow_serialized_data = marshmallow_schema.dump(instances, many=True) 138 | marshmallow_time = time.time() - start_time 139 | 140 | serializer = OuterSerializer(instances, many=True) 141 | start_time = time.time() 142 | drf_serialized_data = serializer.data 143 | drf_time = time.time() - start_time 144 | 145 | schemars_schema = SchemarsOuterSchema() 146 | start_time = time.time() 147 | schemars_serialized_data = [ 148 | schemars_schema.serialize(instance) for instance in instances 149 | ] 150 | schemars_time = time.time() - start_time 151 | 152 | pydantic_schema = PydanticOuterSchema 153 | start_time = time.time() 154 | pydantic_serialized_data = [ 155 | pydantic_schema.model_dump(instance) for instance in pydantic_instances 156 | ] 157 | pydantic_time = time.time() - start_time 158 | 159 | print(f"Marshmallow: {marshmallow_time}") 160 | print(f"DRF: {drf_time}") 161 | print(f"Schemars: {schemars_time}") 162 | print(f"Pydantic: {pydantic_time}") 163 | -------------------------------------------------------------------------------- /examples/django_example/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.10 3 | 4 | # Set environment variables 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # Set the working directory in the container 9 | WORKDIR /app 10 | 11 | # Install dependencies 12 | COPY requirements.txt /app/ 13 | RUN pip install --no-cache-dir -r requirements.txt 14 | 15 | # Copy the current directory contents into the container at /app 16 | COPY . /app/ 17 | -------------------------------------------------------------------------------- /examples/django_example/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mng-dev-ai/schemars/44681c2220ef9b8f9aa5b3ef7c18c15a58178c7e/examples/django_example/db.sqlite3 -------------------------------------------------------------------------------- /examples/django_example/django_example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mng-dev-ai/schemars/44681c2220ef9b8f9aa5b3ef7c18c15a58178c7e/examples/django_example/django_example/__init__.py -------------------------------------------------------------------------------- /examples/django_example/django_example/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for django_example project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_example.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/django_example/django_example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_example project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-14ev_2m6@ckw!35-aee&-e*dh%oew*@o3ux5lq8rz2)-_#nxqp' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'rest_framework', 41 | 'products', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'django_example.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'django_example.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': BASE_DIR / 'db.sqlite3', 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 107 | 108 | LANGUAGE_CODE = 'en-us' 109 | 110 | TIME_ZONE = 'UTC' 111 | 112 | USE_I18N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 119 | 120 | STATIC_URL = 'static/' 121 | 122 | # Default primary key field type 123 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 124 | 125 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 126 | -------------------------------------------------------------------------------- /examples/django_example/django_example/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for django_example project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path('products/', include('products.urls')), 23 | 24 | ] 25 | -------------------------------------------------------------------------------- /examples/django_example/django_example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_example.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/django_example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | web: 5 | build: . 6 | command: bash -c "python manage.py runserver 0.0.0.0:8000" 7 | volumes: 8 | - .:/app 9 | ports: 10 | - "8000:8000" 11 | -------------------------------------------------------------------------------- /examples/django_example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_example.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /examples/django_example/products/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mng-dev-ai/schemars/44681c2220ef9b8f9aa5b3ef7c18c15a58178c7e/examples/django_example/products/__init__.py -------------------------------------------------------------------------------- /examples/django_example/products/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Product, Tag 3 | 4 | admin.site.register(Product) 5 | admin.site.register(Tag) -------------------------------------------------------------------------------- /examples/django_example/products/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProductsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'products' 7 | -------------------------------------------------------------------------------- /examples/django_example/products/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.7 on 2023-11-20 00:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Tag', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(blank=True, max_length=255, null=True)), 19 | ], 20 | ), 21 | migrations.CreateModel( 22 | name='Product', 23 | fields=[ 24 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('name', models.CharField(blank=True, max_length=255, null=True)), 26 | ('description', models.TextField()), 27 | ('created_at', models.DateTimeField(auto_now_add=True)), 28 | ('updated_at', models.DateTimeField(auto_now=True)), 29 | ('related_products', models.ManyToManyField(blank=True, to='products.product')), 30 | ('tags', models.ManyToManyField(blank=True, to='products.tag')), 31 | ], 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /examples/django_example/products/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mng-dev-ai/schemars/44681c2220ef9b8f9aa5b3ef7c18c15a58178c7e/examples/django_example/products/migrations/__init__.py -------------------------------------------------------------------------------- /examples/django_example/products/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Tag(models.Model): 4 | name = models.CharField(max_length=255, blank=True, null=True) 5 | 6 | def __str__(self): 7 | return self.name 8 | 9 | class Product(models.Model): 10 | name = models.CharField(max_length=255, blank=True, null=True) 11 | description = models.TextField() 12 | tags = models.ManyToManyField(Tag, blank=True) 13 | created_at = models.DateTimeField(auto_now_add=True) 14 | updated_at = models.DateTimeField(auto_now=True) 15 | related_products = models.ManyToManyField('self', blank=True) 16 | 17 | def __str__(self): 18 | return self.name 19 | -------------------------------------------------------------------------------- /examples/django_example/products/schema.py: -------------------------------------------------------------------------------- 1 | import schemars 2 | 3 | class TagSchema(schemars.Schema): 4 | name = schemars.Str() 5 | 6 | class BaseProductSchema(schemars.Schema): 7 | name = schemars.Str() 8 | description = schemars.Str() 9 | tags = TagSchema(many=True, source='tags.all', call=True) 10 | created_at = schemars.DateTime() 11 | updated_at = schemars.DateTime() 12 | 13 | 14 | class ProductSchema(BaseProductSchema): 15 | related_products = BaseProductSchema(many=True, source='related_products.all', call=True) 16 | ip = schemars.Method() 17 | 18 | def get_ip(self, obj): 19 | request = self.context.get('request') 20 | return request.META.get('REMOTE_ADDR') 21 | 22 | -------------------------------------------------------------------------------- /examples/django_example/products/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /examples/django_example/products/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ProductView 3 | 4 | app_name = 'products' 5 | 6 | urlpatterns = [ 7 | path('products/', ProductView.as_view(), name='product-list'), 8 | ] 9 | 10 | -------------------------------------------------------------------------------- /examples/django_example/products/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.views import APIView 2 | from rest_framework.response import Response 3 | import schemars 4 | 5 | from .models import Product 6 | from .schema import ProductSchema 7 | 8 | class ProductView(APIView): 9 | def get(self, request): 10 | products = Product.objects.all() 11 | try: 12 | data = ProductSchema(context={'request': request}).serialize(products, many=True) 13 | except schemars.ValidationError as e: 14 | return Response(e.errors, status=400) 15 | return Response(data) -------------------------------------------------------------------------------- /examples/django_example/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | djangorestframework 3 | schemars -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.1,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "schemars" 7 | requires-python = ">=3.7" 8 | authors = [ 9 | { name = "Michael Gendy", email = "nagymichel13@gmail.com" }, 10 | ] 11 | classifiers = [ 12 | "Programming Language :: Rust", 13 | "Programming Language :: Python :: Implementation :: CPython", 14 | "Programming Language :: Python :: Implementation :: PyPy", 15 | ] 16 | 17 | [tool.maturin] 18 | features = ["pyo3/extension-module"] 19 | module-name = "schemars._schemars" 20 | bindings = "pyo3" -------------------------------------------------------------------------------- /schemars/__init__.py: -------------------------------------------------------------------------------- 1 | from schemars._schemars import ( 2 | Schema as RustSchema, 3 | ValidationError, 4 | Str, 5 | Int, 6 | Bool, 7 | Float, 8 | Date, 9 | DateTime, 10 | Dict, 11 | List, 12 | Union, 13 | Method, 14 | Decimal, 15 | Bytes, 16 | Uuid, 17 | Any, 18 | ) 19 | 20 | class Schema(RustSchema): 21 | field_types = ( 22 | Str, 23 | Int, 24 | Bool, 25 | Float, 26 | Date, 27 | DateTime, 28 | Dict, 29 | List, 30 | Union, 31 | Method, 32 | Decimal, 33 | Bytes, 34 | Uuid, 35 | Any 36 | ) 37 | 38 | def __new__(cls, **kwargs): 39 | fields = cls._collect_fields(cls) 40 | return RustSchema.__new__( 41 | cls, 42 | fields=fields, 43 | write_only=kwargs.get("write_only", False), 44 | strict=kwargs.get("strict", False), 45 | default=kwargs.get("default", None), 46 | source=kwargs.get("source", None), 47 | call=kwargs.get("call", False), 48 | serialize_func=kwargs.get("serialize_func", None), 49 | context=kwargs.get("context", {}), 50 | alias=kwargs.get("alias", None), 51 | ) 52 | 53 | @classmethod 54 | def _collect_fields(cls, target_class): 55 | fields = {} 56 | for klass in reversed(target_class.mro()): 57 | for key, value in klass.__dict__.items(): 58 | if isinstance(value, cls.field_types): 59 | fields[key] = value 60 | elif isinstance(value, Schema): 61 | fields[key] = (value, getattr(value, "many", False)) 62 | return fields 63 | 64 | def __init__(self, **kwargs): 65 | self.many = kwargs.get("many", False) 66 | self.source = kwargs.get("source", None) 67 | self.write_only = kwargs.get("write_only", False) 68 | self.strict = kwargs.get("strict", False) 69 | self.call = kwargs.get("call", False) 70 | self.default = kwargs.get("default", None) 71 | self.serialize_func = kwargs.get("serialize_func", None) 72 | self.context = kwargs.get("context", {}) 73 | self.alias = kwargs.get("alias", None) 74 | 75 | def serialize(self, instance, many=None): 76 | many = self.many if many is None else many 77 | return super().serialize(instance, many, self.__class__) 78 | -------------------------------------------------------------------------------- /schemars/_schemars.pyi: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | class FieldBase: 4 | def __init__( 5 | self, 6 | write_only: bool = False, 7 | strict: bool = False, 8 | call: bool = False, 9 | default: typing.Optional[typing.Any] = None, 10 | source: typing.Optional[str] = None, 11 | serialize_func: typing.Optional[typing.Callable] = None, 12 | alias: typing.Optional[str] = None, 13 | **kwargs: Any 14 | ) -> None: ... 15 | 16 | class DateFieldBase(FieldBase): 17 | def __init__(self, format: typing.Optional[str] = None, **kwargs: Any) -> None: ... 18 | 19 | class Str(FieldBase): ... 20 | class Bytes(FieldBase): ... 21 | class Int(FieldBase): ... 22 | class Bool(FieldBase): ... 23 | class Float(FieldBase): ... 24 | class Decimal(FieldBase): ... 25 | class Date(DateFieldBase): ... 26 | class DateTime(DateFieldBase): ... 27 | 28 | class Dict(FieldBase): 29 | def __init__( 30 | self, child: typing.Optional[FieldBase] = None, **kwargs: Any 31 | ) -> None: ... 32 | 33 | class List(FieldBase): 34 | def __init__(self, child: typing.Optional[Any] = None, **kwargs: Any) -> None: ... 35 | 36 | class Uuid(FieldBase): ... 37 | class Any(FieldBase): ... 38 | 39 | class Union(FieldBase): 40 | def __init__(self, fields: Any, **kwargs: Any) -> None: ... 41 | 42 | class Method: 43 | def __init__(self, method_name: typing.Optional[str] = None) -> None: ... 44 | 45 | class Schema: 46 | def __new__(cls, **kwargs) -> None: ... 47 | def __init__(self, **kwargs) -> None: ... 48 | def serialize( 49 | self, instance: Any, many: bool = False, cls: typing.Optional[Any] = None 50 | ) -> Any: ... 51 | 52 | class ValidationError(BaseException): 53 | def __init__(self, errors: Any) -> None: ... 54 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{exceptions::PyBaseException, prelude::*}; 2 | 3 | #[pyclass(extends=PyBaseException)] 4 | pub struct ValidationError { 5 | errors: PyObject, 6 | } 7 | 8 | #[pymethods] 9 | impl ValidationError { 10 | #[new] 11 | pub fn new(errors: PyObject) -> Self { 12 | ValidationError { errors } 13 | } 14 | #[getter] 15 | pub fn errors(&self) -> PyObject { 16 | self.errors.clone() 17 | } 18 | } 19 | 20 | pub fn get_python_type(value: &PyAny) -> &'static str { 21 | match value { 22 | _ if value.is_instance_of::() => "str", 23 | _ if value.is_instance_of::() => "bytes", 24 | _ if value.is_instance_of::() => "int", 25 | _ if value.is_instance_of::() => "float", 26 | _ if value.is_instance_of::() => "bool", 27 | _ if value.is_instance_of::() => "date", 28 | _ if value.is_instance_of::() => "datetime", 29 | _ if value.is_instance_of::() => "dict", 30 | _ if value.is_instance_of::() => "list", 31 | _ => "unknown", 32 | } 33 | } 34 | 35 | pub fn generate_error_msg(field_type: &str, value: &PyAny) -> PyResult { 36 | let user_input_type = get_python_type(value); 37 | 38 | let user_input = value.to_string(); 39 | Ok(format!( 40 | "Received '{}' with value '{}' which is not a valid value for '{}'", 41 | user_input_type, user_input, field_type 42 | )) 43 | } 44 | -------------------------------------------------------------------------------- /src/fields/any.rs: -------------------------------------------------------------------------------- 1 | use crate::fields::base::BaseField; 2 | use pyo3::prelude::*; 3 | 4 | #[pyclass(subclass)] 5 | pub struct Any { 6 | pub base: BaseField, 7 | } 8 | 9 | impl_py_methods!(Any, none, { 10 | fn serialize(&self, _py: Python, value: &PyAny) -> PyResult { 11 | Ok(value.into()) 12 | } 13 | }); 14 | 15 | impl_field_trait!(Any); 16 | -------------------------------------------------------------------------------- /src/fields/base.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::ValidationError, fields::any::Any, fields::bool::Bool, fields::bytes::Bytes, 3 | fields::date::Date, fields::datetime::DateTime, fields::decimal::Decimal, fields::dict::Dict, 4 | fields::float::Float, fields::int::Int, fields::list::List, fields::method::Method, 5 | fields::str::Str, fields::union::Union, fields::uuid::Uuid, schema::Schema, 6 | }; 7 | use pyo3::prelude::*; 8 | 9 | pub trait FieldTrait { 10 | fn default(&self) -> Option { 11 | None 12 | } 13 | fn serialize( 14 | &self, 15 | py: Python, 16 | value: &PyAny, 17 | _parent: Option, 18 | ) -> PyResult { 19 | Ok(value.to_object(py)) 20 | } 21 | fn method_getter(&self, py: Python, _field_name: &str, _parent: &PyAny) -> PyResult { 22 | Ok(py.None()) 23 | } 24 | fn is_write_only(&self) -> bool { 25 | false 26 | } 27 | fn source(&self) -> Option { 28 | None 29 | } 30 | fn call(&self) -> bool { 31 | false 32 | } 33 | fn is_method_field(&self) -> bool { 34 | false 35 | } 36 | fn alias(&self) -> Option { 37 | None 38 | } 39 | } 40 | 41 | #[derive(Clone)] 42 | pub enum Field { 43 | Str(Py), 44 | Bytes(Py), 45 | Int(Py), 46 | Bool(Py), 47 | Float(Py), 48 | Decimal(Py), 49 | Date(Py), 50 | DateTime(Py), 51 | Dict(Py), 52 | List(Py), 53 | Uuid(Py), 54 | Union(Py), 55 | Any(Py), 56 | Method(Py), 57 | NestedSchema(Py, bool), 58 | } 59 | 60 | impl Field { 61 | fn with_field_ref PyResult>( 62 | &self, 63 | py: Python, 64 | f: F, 65 | ) -> PyResult { 66 | match self { 67 | Field::Str(field) => f(&*field.as_ref(py).borrow()), 68 | Field::Bytes(field) => f(&*field.as_ref(py).borrow()), 69 | Field::Int(field) => f(&*field.as_ref(py).borrow()), 70 | Field::Bool(field) => f(&*field.as_ref(py).borrow()), 71 | Field::Float(field) => f(&*field.as_ref(py).borrow()), 72 | Field::Decimal(field) => f(&*field.as_ref(py).borrow()), 73 | Field::Date(field) => f(&*field.as_ref(py).borrow()), 74 | Field::DateTime(field) => f(&*field.as_ref(py).borrow()), 75 | Field::Dict(field) => f(&*field.as_ref(py).borrow()), 76 | Field::List(field) => f(&*field.as_ref(py).borrow()), 77 | Field::Uuid(field) => f(&*field.as_ref(py).borrow()), 78 | Field::Union(field) => f(&*field.as_ref(py).borrow()), 79 | Field::Any(field) => f(&*field.as_ref(py).borrow()), 80 | Field::Method(field) => f(&*field.as_ref(py).borrow()), 81 | Field::NestedSchema(field, _) => f(&*field.as_ref(py).borrow()), 82 | } 83 | } 84 | pub fn default_value(&self, py: Python) -> PyResult> { 85 | self.with_field_ref(py, |field: &dyn FieldTrait| Ok(field.default())) 86 | } 87 | pub fn serialize( 88 | &self, 89 | py: Python, 90 | value: &PyAny, 91 | parent: Option, 92 | ) -> PyResult { 93 | match self { 94 | // If the field is schema, we will ignore the trait and use the schema's serialize method directly. 95 | // This is because we need to pass the many flag to the schema's serialize method. 96 | Field::NestedSchema(field, many) => { 97 | let field_ref = field.as_ref(py).borrow(); 98 | if *many { 99 | field_ref.serialize(py, value, Some(true), parent) 100 | } else { 101 | field_ref.serialize(py, value, None, parent) 102 | } 103 | } 104 | _ => self.with_field_ref(py, |field| field.serialize(py, value, parent)), 105 | } 106 | } 107 | pub fn method_getter( 108 | &self, 109 | py: Python, 110 | field_name: &str, 111 | parent: &PyAny, 112 | ) -> PyResult { 113 | self.with_field_ref(py, |field| field.method_getter(py, field_name, parent)) 114 | } 115 | pub fn is_write_only(&self, py: Python) -> PyResult { 116 | self.with_field_ref(py, |field| Ok(field.is_write_only())) 117 | } 118 | pub fn source(&self, py: Python) -> PyResult> { 119 | self.with_field_ref(py, |field| Ok(field.source())) 120 | } 121 | pub fn is_method_field(&self, py: Python) -> PyResult { 122 | self.with_field_ref(py, |field| Ok(field.is_method_field())) 123 | } 124 | pub fn call(&self, py: Python) -> PyResult { 125 | self.with_field_ref(py, |field| Ok(field.call())) 126 | } 127 | pub fn alias(&self, py: Python) -> PyResult> { 128 | self.with_field_ref(py, |field| Ok(field.alias())) 129 | } 130 | } 131 | 132 | impl<'source> FromPyObject<'source> for Field { 133 | #[inline] 134 | fn extract(obj: &'source PyAny) -> PyResult { 135 | if let Ok(field) = obj.extract::>() { 136 | Ok(Field::Str(field)) 137 | } else if let Ok(field) = obj.extract::>() { 138 | Ok(Field::Bytes(field)) 139 | } else if let Ok(field) = obj.extract::>() { 140 | Ok(Field::Int(field)) 141 | } else if let Ok(field) = obj.extract::>() { 142 | Ok(Field::Bool(field)) 143 | } else if let Ok(field) = obj.extract::>() { 144 | Ok(Field::Float(field)) 145 | } else if let Ok(field) = obj.extract::>() { 146 | Ok(Field::Decimal(field)) 147 | } else if let Ok(field) = obj.extract::>() { 148 | Ok(Field::Date(field)) 149 | } else if let Ok(field) = obj.extract::>() { 150 | Ok(Field::DateTime(field)) 151 | } else if let Ok(field) = obj.extract::>() { 152 | Ok(Field::Dict(field)) 153 | } else if let Ok(field) = obj.extract::>() { 154 | Ok(Field::List(field)) 155 | } else if let Ok(field) = obj.extract::>() { 156 | Ok(Field::Uuid(field)) 157 | } else if let Ok(field) = obj.extract::>() { 158 | Ok(Field::Union(field)) 159 | } else if let Ok(field) = obj.extract::>() { 160 | Ok(Field::Any(field)) 161 | } else if let Ok(field) = obj.extract::>() { 162 | Ok(Field::Method(field)) 163 | } else if let Ok((field, many)) = obj.extract::<(Py, bool)>() { 164 | Ok(Field::NestedSchema(field, many)) 165 | } else { 166 | Err(PyErr::new::(["Invalid field type."])) 167 | } 168 | } 169 | } 170 | 171 | #[pyclass(subclass)] 172 | #[derive(Clone)] 173 | pub struct BaseField { 174 | #[pyo3(get, set)] 175 | pub write_only: bool, 176 | #[pyo3(get, set)] 177 | pub strict: bool, 178 | #[pyo3(get, set)] 179 | pub call: bool, 180 | #[pyo3(get, set)] 181 | pub default: Option, 182 | #[pyo3(get, set)] 183 | pub source: Option, 184 | #[pyo3(get, set)] 185 | pub serialize_func: Option, 186 | #[pyo3(get, set)] 187 | pub alias: Option, 188 | pub is_method_field: bool, 189 | } 190 | 191 | impl BaseField { 192 | #[allow(clippy::too_many_arguments)] 193 | pub fn new( 194 | write_only: bool, 195 | strict: bool, 196 | call: bool, 197 | default: Option, 198 | source: Option, 199 | serialize_func: Option, 200 | alias: Option, 201 | is_method_field: bool, 202 | ) -> Self { 203 | BaseField { 204 | write_only, 205 | strict, 206 | call, 207 | default, 208 | source, 209 | serialize_func, 210 | alias, 211 | is_method_field, 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/fields/bool.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::generate_error_msg; 2 | use crate::errors::ValidationError; 3 | use crate::fields::base::BaseField; 4 | use pyo3::prelude::*; 5 | use pyo3::types::{PyBool, PyFloat, PyLong, PyString}; 6 | 7 | const TRUTHY: [&str; 6] = ["t", "true", "on", "y", "yes", "1"]; 8 | const FALSY: [&str; 6] = ["f", "false", "off", "n", "no", "0"]; 9 | 10 | #[pyclass(subclass)] 11 | pub struct Bool { 12 | pub base: BaseField, 13 | } 14 | 15 | impl_py_methods!(Bool, none, { 16 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 17 | if let Ok(py_bool) = value.downcast::() { 18 | return Ok(py_bool.into()); 19 | } 20 | if !self.is_strict() { 21 | if let Ok(py_str) = value.downcast::() { 22 | let s: &str = py_str.to_str()?; 23 | if TRUTHY.contains(&s) { 24 | return Ok(PyBool::new(py, true).into()); 25 | } 26 | if FALSY.contains(&s) { 27 | return Ok(PyBool::new(py, false).into()); 28 | } 29 | } 30 | if let Ok(py_long) = value.downcast::() { 31 | let i: isize = py_long.extract()?; 32 | if i == 1 { 33 | return Ok(PyBool::new(py, true).into()); 34 | } 35 | if i == 0 { 36 | return Ok(PyBool::new(py, false).into()); 37 | } 38 | } 39 | if let Ok(py_float) = value.downcast::() { 40 | let f: f64 = py_float.extract()?; 41 | if f == 1.0 { 42 | return Ok(PyBool::new(py, true).into()); 43 | } 44 | if f == 0.0 { 45 | return Ok(PyBool::new(py, false).into()); 46 | } 47 | } 48 | } 49 | Err(PyErr::new::(generate_error_msg( 50 | "Bool", value, 51 | )?)) 52 | } 53 | }); 54 | 55 | impl_field_trait!(Bool); 56 | -------------------------------------------------------------------------------- /src/fields/bytes.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::generate_error_msg; 2 | use crate::errors::ValidationError; 3 | use crate::fields::base::BaseField; 4 | use pyo3::prelude::*; 5 | use pyo3::types::{PyBytes, PyString}; 6 | 7 | #[pyclass(subclass)] 8 | pub struct Bytes { 9 | pub base: BaseField, 10 | } 11 | 12 | impl_py_methods!(Bytes, none, { 13 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 14 | if let Ok(py_bytes) = value.downcast::() { 15 | return Ok(py_bytes.into()); 16 | } 17 | if !self.is_strict() { 18 | if let Ok(py_str) = value.downcast::() { 19 | return Ok(PyBytes::new(py, py_str.to_string().as_bytes()).into()); 20 | } 21 | } 22 | Err(PyErr::new::(generate_error_msg( 23 | "Bytes", value, 24 | )?)) 25 | } 26 | }); 27 | 28 | impl_field_trait!(Bytes); 29 | -------------------------------------------------------------------------------- /src/fields/date.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::generate_error_msg; 2 | use crate::errors::ValidationError; 3 | use crate::fields::base::BaseField; 4 | use pyo3::types::{PyDate, PyDateTime, PyFloat, PyLong, PyString}; 5 | use pyo3::{intern, prelude::*}; 6 | use speedate::Date as SpeedDate; 7 | 8 | #[pyclass(subclass)] 9 | pub struct Date { 10 | pub base: BaseField, 11 | format: Option, 12 | } 13 | 14 | impl_py_methods!(Date, optional, { format: Option}, { 15 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 16 | if let Ok(py_datetime) = value.downcast::() { 17 | let py_date = py_datetime.call_method0(intern!(py, "date"))?; 18 | return self.format_and_return(py, py_date); 19 | } 20 | if let Ok(py_date) = value.downcast::() { 21 | return self.format_and_return(py, py_date); 22 | } 23 | if !self.is_strict() { 24 | if let Ok(py_str) = value.downcast::() { 25 | return self.parse_and_format_date(py, py_str); 26 | } 27 | if let Ok(py_long) = value.downcast::() { 28 | return self.parse_and_format_date(py, py_long); 29 | } 30 | if let Ok(py_float) = value.downcast::() { 31 | return self.parse_and_format_date(py, py_float); 32 | } 33 | } 34 | Err(PyErr::new::(generate_error_msg( 35 | "Date", 36 | value, 37 | )?)) 38 | } 39 | 40 | fn parse_and_format_date(&self, py: Python, date_str: &PyAny) -> PyResult { 41 | if let Ok(date) = SpeedDate::parse_str(&date_str.to_string()) { 42 | let py_date = PyDate::new(py, date.year as i32, date.month, date.day)?; 43 | return self.format_and_return(py, py_date); 44 | } 45 | Err(PyErr::new::(generate_error_msg( 46 | "Date", date_str, 47 | )?)) 48 | } 49 | 50 | fn format_and_return(&self, py: Python, py_date: &PyAny) -> PyResult { 51 | let date_str = if let Some(ref format) = self.format { 52 | py_date 53 | .call_method1(intern!(py, "strftime"), (format,))? 54 | .to_string() 55 | } else { 56 | py_date.call_method0(intern!(py, "isoformat"))?.to_string() 57 | }; 58 | Ok(date_str.to_object(py)) 59 | } 60 | }); 61 | 62 | impl_field_trait!(Date); 63 | -------------------------------------------------------------------------------- /src/fields/datetime.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::generate_error_msg; 2 | use crate::errors::ValidationError; 3 | use crate::fields::base::BaseField; 4 | use pyo3::types::PyDateAccess; 5 | use pyo3::types::{PyDate, PyDateTime, PyFloat, PyLong, PyString}; 6 | use pyo3::{intern, prelude::*}; 7 | use speedate::DateTime as SpeedDateTime; 8 | 9 | #[pyclass(subclass)] 10 | pub struct DateTime { 11 | pub base: BaseField, 12 | format: Option, 13 | } 14 | impl_py_methods!(DateTime, optional, { format: Option}, { 15 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 16 | if let Ok(py_datetime) = value.downcast::() { 17 | return self.format_and_return(py, py_datetime); 18 | } 19 | if !self.is_strict() { 20 | if let Ok(py_date) = value.downcast::() { 21 | let py_datetime = PyDateTime::new( 22 | py, 23 | py_date.get_year(), 24 | py_date.get_month(), 25 | py_date.get_day(), 26 | 0, 27 | 0, 28 | 0, 29 | 0, 30 | None, // Default time to midnight 31 | )?; 32 | return self.format_and_return(py, py_datetime); 33 | } 34 | if let Ok(py_str) = value.downcast::() { 35 | return self.parse_and_format_datetime(py, py_str); 36 | } 37 | if let Ok(py_long) = value.downcast::() { 38 | return self.parse_and_format_datetime(py, py_long); 39 | } 40 | if let Ok(py_float) = value.downcast::() { 41 | return self.parse_and_format_datetime(py, py_float); 42 | } 43 | } 44 | Err(PyErr::new::(generate_error_msg( 45 | "DateTime", 46 | value, 47 | )?)) 48 | } 49 | 50 | fn parse_and_format_datetime(&self, py: Python, datetime_str: &PyAny) -> PyResult { 51 | if let Ok(datetime) = SpeedDateTime::parse_str(&datetime_str.to_string()) { 52 | let py_datetime = PyDateTime::new( 53 | py, 54 | datetime.date.year as i32, 55 | datetime.date.month, 56 | datetime.date.day, 57 | datetime.time.hour, 58 | datetime.time.minute, 59 | datetime.time.second, 60 | datetime.time.microsecond, 61 | None, 62 | )?; 63 | return self.format_and_return(py, py_datetime); 64 | } 65 | Err(PyErr::new::(generate_error_msg( 66 | "DateTime", 67 | datetime_str, 68 | )?)) 69 | } 70 | 71 | fn format_and_return(&self, py: Python, py_datetime: &PyAny) -> PyResult { 72 | let datetime_str = if let Some(ref format) = self.format { 73 | py_datetime 74 | .call_method1(intern!(py, "strftime"), (format,))? 75 | .to_string() 76 | } else { 77 | py_datetime 78 | .call_method0(intern!(py, "isoformat"))? 79 | .to_string() 80 | }; 81 | Ok(datetime_str.to_object(py)) 82 | } 83 | }); 84 | 85 | impl_field_trait!(DateTime); 86 | -------------------------------------------------------------------------------- /src/fields/decimal.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::errors::generate_error_msg; 4 | use crate::errors::ValidationError; 5 | use crate::fields::base::BaseField; 6 | use pyo3::prelude::*; 7 | use pyo3::types::PyBool; 8 | use pyo3::types::{PyFloat, PyLong, PyString}; 9 | use rust_decimal::prelude::FromPrimitive; 10 | use rust_decimal::Decimal as RDecimal; 11 | 12 | #[pyclass(subclass)] 13 | pub struct Decimal { 14 | pub base: BaseField, 15 | } 16 | impl_py_methods!(Decimal, none, { 17 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 18 | if value.downcast::()?.getattr("quantize").is_ok() { 19 | return Ok(value.to_string().to_object(py)); 20 | } 21 | 22 | if !self.is_strict() { 23 | if let Ok(py_float) = value.downcast::() { 24 | let f: f64 = py_float.extract()?; 25 | if let Some(decimal_value) = RDecimal::from_f64(f) { 26 | return Ok(decimal_value.to_string().to_object(py)); 27 | } 28 | } 29 | 30 | if let Ok(py_str) = value.downcast::() { 31 | let s: &str = py_str.to_str()?; 32 | if let Ok(decimal_value) = RDecimal::from_str(s) { 33 | return Ok(decimal_value.to_string().to_object(py)); 34 | } 35 | } 36 | 37 | if let Ok(py_int) = value.downcast::() { 38 | if !value.is_instance_of::() { 39 | let i: i64 = py_int.extract()?; 40 | if let Some(decimal_value) = RDecimal::from_i64(i) { 41 | return Ok(decimal_value.to_string().to_object(py)); 42 | } 43 | } 44 | } 45 | } 46 | Err(PyErr::new::(generate_error_msg( 47 | "Decimal", value, 48 | )?)) 49 | } 50 | }); 51 | 52 | impl_field_trait!(Decimal); 53 | -------------------------------------------------------------------------------- /src/fields/dict.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{generate_error_msg, ValidationError}; 2 | use crate::fields::base::{BaseField, Field}; 3 | use pyo3::prelude::*; 4 | use pyo3::types::PyDict; 5 | 6 | #[pyclass(subclass)] 7 | pub struct Dict { 8 | pub base: BaseField, 9 | child: Option, 10 | } 11 | 12 | impl_py_methods!(Dict, optional, { child: Option}, { 13 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 14 | if let Ok(py_dict) = value.downcast::() { 15 | let dict = PyDict::new(py); 16 | for (key, val) in py_dict.iter() { 17 | let value = self.serialize_child(py, val)?; 18 | dict.set_item(key, value)?; 19 | } 20 | return Ok(dict.into()); 21 | } 22 | if !self.is_strict() && value.hasattr("items")? { 23 | let items = value.getattr("items")?.call0()?; 24 | let dict = PyDict::new(py); 25 | for item in items.iter()? { 26 | let (key, val): (PyObject, PyObject) = item?.extract()?; 27 | let value = self.serialize_child(py, val.as_ref(py))?; 28 | dict.set_item(key, value)?; 29 | } 30 | return Ok(dict.into()); 31 | } 32 | Err(PyErr::new::(generate_error_msg("Dict", value)?)) 33 | } 34 | fn serialize_child(&self, py: Python, child: &PyAny) -> PyResult { 35 | match &self.child { 36 | Some(child_type) => child_type.serialize(py, child, None), 37 | None => Ok(child.into()), 38 | } 39 | } 40 | 41 | }); 42 | 43 | impl_field_trait!(Dict); 44 | -------------------------------------------------------------------------------- /src/fields/float.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::generate_error_msg; 2 | use crate::errors::ValidationError; 3 | use crate::fields::base::BaseField; 4 | use pyo3::prelude::*; 5 | use pyo3::types::{PyBool, PyFloat, PyLong, PyString}; 6 | 7 | #[pyclass(subclass)] 8 | pub struct Float { 9 | pub base: BaseField, 10 | } 11 | 12 | impl_py_methods!(Float, none, { 13 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 14 | if let Ok(py_float) = value.downcast::() { 15 | return Ok(py_float.into()); 16 | } 17 | if !self.is_strict() { 18 | if let Ok(py_bool) = value.downcast::() { 19 | return Ok((py_bool.is_true() as i32).to_object(py)); 20 | } 21 | if let Ok(py_str) = value.downcast::() { 22 | let s: &str = py_str.to_str()?; 23 | if let Ok(f) = s.parse::() { 24 | return Ok(f.to_object(py)); 25 | } 26 | } 27 | if let Ok(py_int) = value.downcast::() { 28 | let i: i64 = py_int.extract()?; 29 | return Ok((i as f64).to_object(py)); 30 | } 31 | } 32 | Err(PyErr::new::(generate_error_msg( 33 | "Float", value, 34 | )?)) 35 | } 36 | }); 37 | 38 | impl_field_trait!(Float); 39 | -------------------------------------------------------------------------------- /src/fields/int.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::generate_error_msg; 2 | use crate::errors::ValidationError; 3 | use crate::fields::base::BaseField; 4 | use pyo3::prelude::*; 5 | use pyo3::types::{PyBool, PyFloat, PyLong, PyString}; 6 | 7 | #[pyclass(subclass)] 8 | pub struct Int { 9 | pub base: BaseField, 10 | } 11 | 12 | impl_py_methods!(Int, none, { 13 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 14 | if let Ok(py_int) = value.downcast::() { 15 | return Ok(py_int.into()); 16 | } 17 | if !self.is_strict() { 18 | if let Ok(py_bool) = value.downcast::() { 19 | return Ok((py_bool.is_true() as i32).to_object(py)); 20 | } 21 | if let Ok(py_str) = value.downcast::() { 22 | let s: &str = py_str.to_str()?; 23 | if let Ok(i) = s.parse::() { 24 | return Ok(i.to_object(py)); 25 | } 26 | } 27 | if let Ok(py_float) = value.downcast::() { 28 | let f: f64 = py_float.extract()?; 29 | if f.fract() == 0.0 && f >= i32::MIN as f64 && f <= i32::MAX as f64 { 30 | return Ok((f as i32).to_object(py)); 31 | } 32 | } 33 | } 34 | Err(PyErr::new::(generate_error_msg( 35 | "Int", value, 36 | )?)) 37 | } 38 | }); 39 | 40 | impl_field_trait!(Int); 41 | -------------------------------------------------------------------------------- /src/fields/list.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::generate_error_msg; 2 | use crate::errors::ValidationError; 3 | use crate::fields::base::{BaseField, Field}; 4 | use pyo3::prelude::*; 5 | use pyo3::types::PyList; 6 | 7 | #[pyclass(subclass)] 8 | pub struct List { 9 | pub base: BaseField, 10 | child: Option, 11 | } 12 | 13 | impl_py_methods!(List, optional, { child: Option }, { 14 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 15 | let as_list = |v: &PyAny| -> PyResult<_> { 16 | match v.downcast::() { 17 | Ok(py_list) => Ok(py_list.into()), 18 | _ if !self.is_strict() => { 19 | let temp_list = PyList::empty(py); 20 | for item in v.iter()? { 21 | temp_list.append(item?)?; 22 | } 23 | Ok(temp_list.to_object(py)) 24 | } 25 | _ => Err(PyErr::new::(generate_error_msg( 26 | "List", 27 | value, 28 | )?)), 29 | } 30 | }; 31 | match self.child { 32 | Some(ref child) => { 33 | let list = as_list(value)?; 34 | let downcasted_list = PyList::empty(py); 35 | for item in list.as_ref(py).iter()? { 36 | let py_item = child.serialize(py, item?, None)?; 37 | downcasted_list.append(py_item)?; 38 | } 39 | Ok(downcasted_list.into()) 40 | } 41 | None => as_list(value), 42 | } 43 | } 44 | }); 45 | 46 | impl_field_trait!(List); 47 | -------------------------------------------------------------------------------- /src/fields/method.rs: -------------------------------------------------------------------------------- 1 | use crate::fields::base::{BaseField, FieldTrait}; 2 | use pyo3::prelude::*; 3 | 4 | #[pyclass(subclass)] 5 | pub struct Method { 6 | pub base: BaseField, 7 | pub method_name: Option, 8 | } 9 | 10 | #[pymethods] 11 | impl Method { 12 | #[new] 13 | #[pyo3(signature=(method_name=None))] 14 | fn new(method_name: Option) -> Self { 15 | Method { 16 | base: BaseField::new(false, false, false, None, None, None, None, true), 17 | method_name, 18 | } 19 | } 20 | } 21 | 22 | impl FieldTrait for Method { 23 | fn method_getter(&self, py: Python, field_name: &str, parent: &PyAny) -> PyResult { 24 | let method_name: String = self 25 | .method_name 26 | .clone() 27 | .unwrap_or_else(|| format!("get_{}", field_name)); 28 | let method: Result, PyErr> = parent.into_py(py).getattr(py, method_name.as_str()); 29 | Ok(method?.to_object(py)) 30 | } 31 | fn is_method_field(&self) -> bool { 32 | self.base.is_method_field 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/fields/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod any; 2 | pub mod base; 3 | pub mod bool; 4 | pub mod bytes; 5 | pub mod date; 6 | pub mod datetime; 7 | pub mod decimal; 8 | pub mod dict; 9 | pub mod float; 10 | pub mod int; 11 | pub mod list; 12 | pub mod method; 13 | pub mod str; 14 | pub mod union; 15 | pub mod uuid; 16 | -------------------------------------------------------------------------------- /src/fields/str.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::generate_error_msg; 2 | use crate::errors::ValidationError; 3 | use crate::fields::base::BaseField; 4 | use pyo3::prelude::*; 5 | use pyo3::types::{PyBytes, PyString}; 6 | use std::borrow::Cow; 7 | 8 | #[pyclass(subclass)] 9 | pub struct Str { 10 | pub base: BaseField, 11 | } 12 | 13 | impl_py_methods!(Str, none, { 14 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 15 | if let Ok(py_str) = value.downcast::() { 16 | return Ok(py_str.into()); 17 | } 18 | if !self.is_strict() { 19 | if let Ok(py_bytes) = value.downcast::() { 20 | if let Ok(utf8_str) = std::str::from_utf8(py_bytes.as_bytes()) { 21 | return Ok(utf8_str.to_object(py)); 22 | } 23 | let cow: Cow = String::from_utf8_lossy(py_bytes.as_bytes()); 24 | return Ok(cow.into_owned().to_object(py)); 25 | } 26 | } 27 | Err(PyErr::new::(generate_error_msg( 28 | "Str", value, 29 | )?)) 30 | } 31 | }); 32 | 33 | impl_field_trait!(Str); 34 | -------------------------------------------------------------------------------- /src/fields/union.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{generate_error_msg, ValidationError}; 2 | use crate::fields::base::BaseField; 3 | use pyo3::intern; 4 | use pyo3::prelude::*; 5 | 6 | #[pyclass(subclass)] 7 | pub struct Union { 8 | pub base: BaseField, 9 | fields: Vec, 10 | } 11 | 12 | impl_py_methods!(Union, required, { fields: Vec }, { 13 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 14 | for field in &self.fields { 15 | if let Ok(py_field) = field.downcast::(py) { 16 | if let Ok(py_value) = 17 | py_field.call_method1(intern!(py, "serialize"), (value,)) 18 | { 19 | return Ok(py_value.to_object(py)); 20 | } 21 | } 22 | } 23 | Err(PyErr::new::(generate_error_msg( 24 | "Union", value, 25 | )?)) 26 | } 27 | }); 28 | 29 | impl_field_trait!(Union); 30 | -------------------------------------------------------------------------------- /src/fields/uuid.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::generate_error_msg; 2 | use crate::errors::ValidationError; 3 | use crate::fields::base::BaseField; 4 | use pyo3::prelude::*; 5 | use pyo3::types::{PyBytes, PyString}; 6 | use uuid::Uuid as RustUuid; 7 | 8 | #[pyclass(subclass)] 9 | pub struct Uuid { 10 | pub base: BaseField, 11 | } 12 | 13 | impl_py_methods!(Uuid, none, { 14 | fn serialize(&self, py: Python, value: &PyAny) -> PyResult { 15 | if let Ok(uuid_obj) = value.downcast::() { 16 | if uuid_obj 17 | .getattr("__class__")? 18 | .getattr("__name__")? 19 | .extract::()? 20 | == "UUID" 21 | { 22 | let uuid_str = uuid_obj.call_method0("__str__")?.extract::()?; 23 | return Ok(uuid_str.into_py(py)); 24 | } 25 | } 26 | if !self.is_strict() { 27 | if let Ok(py_str) = value.downcast::() { 28 | let s: &str = py_str.to_str()?; 29 | if let Ok(uuid) = RustUuid::parse_str(s) { 30 | return Ok(uuid.to_string().into_py(py)); 31 | } 32 | } 33 | if let Ok(py_bytes) = value.downcast::() { 34 | let bytes = py_bytes.as_bytes(); 35 | if bytes.len() == 16 { 36 | if let Ok(uuid) = RustUuid::from_slice(bytes) { 37 | return Ok(uuid.to_string().into_py(py)); 38 | } 39 | } 40 | } 41 | } 42 | Err(PyErr::new::(generate_error_msg( 43 | "Uuid", value, 44 | )?)) 45 | } 46 | }); 47 | 48 | impl_field_trait!(Uuid); 49 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "mimalloc")] 2 | #[global_allocator] 3 | static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; 4 | 5 | #[macro_use] 6 | mod macros; 7 | 8 | mod errors; 9 | mod fields; 10 | mod schema; 11 | 12 | use pyo3::prelude::*; 13 | 14 | #[pymodule] 15 | fn _schemars(_py: Python, m: &PyModule) -> PyResult<()> { 16 | m.add_class::()?; 17 | m.add_class::()?; 18 | m.add_class::()?; 19 | m.add_class::()?; 20 | m.add_class::()?; 21 | m.add_class::()?; 22 | m.add_class::()?; 23 | m.add_class::()?; 24 | m.add_class::()?; 25 | m.add_class::()?; 26 | m.add_class::()?; 27 | m.add_class::()?; 28 | m.add_class::()?; 29 | m.add_class::()?; 30 | m.add_class::()?; 31 | m.add_class::()?; 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! impl_field_trait { 3 | ($type:ty) => { 4 | use $crate::fields::base::FieldTrait; 5 | impl FieldTrait for $type { 6 | fn default(&self) -> Option { 7 | self.base.default.clone() 8 | } 9 | fn serialize( 10 | &self, 11 | py: Python, 12 | value: &PyAny, 13 | _parent: Option, 14 | ) -> PyResult { 15 | if let Some(callback) = &self.base.serialize_func { 16 | let result = callback.call1(py, (value,))?; 17 | return Ok(result); 18 | } 19 | 20 | return self.serialize(py, value); 21 | } 22 | fn is_write_only(&self) -> bool { 23 | self.base.write_only 24 | } 25 | fn source(&self) -> Option { 26 | self.base.source.clone() 27 | } 28 | fn call(&self) -> bool { 29 | self.base.call 30 | } 31 | fn is_method_field(&self) -> bool { 32 | self.base.is_method_field 33 | } 34 | fn alias(&self) -> Option { 35 | self.base.alias.clone() 36 | } 37 | } 38 | }; 39 | } 40 | 41 | #[macro_export] 42 | macro_rules! impl_py_methods { 43 | ($struct_name:ident, none, { $($method:item)* }) => { 44 | #[pymethods] 45 | impl $struct_name { 46 | #[allow(clippy::too_many_arguments)] 47 | #[new] 48 | #[pyo3(signature=(write_only=false, strict=false, call=false, default=None, source=None, serialize_func=None, alias=None))] 49 | fn new( 50 | write_only: bool, 51 | strict: bool, 52 | call: bool, 53 | default: Option, 54 | source: Option, 55 | serialize_func: Option, 56 | alias: Option, 57 | ) -> Self { 58 | $struct_name { 59 | base: BaseField::new(write_only, strict, call, default, source, serialize_func, alias, false), 60 | } 61 | } 62 | 63 | #[getter] 64 | fn is_strict(&self) -> bool { 65 | self.base.strict 66 | } 67 | 68 | $($method)* 69 | } 70 | }; 71 | 72 | ($struct_name:ident, optional, { $($field_name:ident: Option<$field_type:ty>),* $(,)? }, { $($method:item)* }) => { 73 | #[pymethods] 74 | impl $struct_name { 75 | #[allow(clippy::too_many_arguments)] 76 | #[new] 77 | #[pyo3(signature=($($field_name=None)*, write_only=false, strict=false, call=false, default=None, source=None, serialize_func=None, alias=None))] 78 | fn new( 79 | $( $field_name: Option<$field_type>, )* 80 | write_only: bool, 81 | strict: bool, 82 | call: bool, 83 | default: Option, 84 | source: Option, 85 | serialize_func: Option, 86 | alias: Option, 87 | 88 | ) -> Self { 89 | $struct_name { 90 | base: BaseField::new(write_only, strict, call, default, source, serialize_func, alias, false), 91 | $( $field_name, )* 92 | } 93 | } 94 | 95 | #[getter] 96 | fn is_strict(&self) -> bool { 97 | self.base.strict 98 | } 99 | 100 | $($method)* 101 | } 102 | }; 103 | 104 | ($struct_name:ident, required, { $($field_name:ident: $field_type:ty),* $(,)? }, { $($method:item)* }) => { 105 | #[pymethods] 106 | impl $struct_name { 107 | #[allow(clippy::too_many_arguments)] 108 | #[new] 109 | #[pyo3(signature=( $($field_name),*, write_only=false, strict=false, call=false, default=None, source= None, serialize_func= None, alias= None))] 110 | fn new( 111 | $( $field_name: $field_type, )* 112 | write_only: bool, 113 | strict: bool, 114 | call: bool, 115 | default: Option, 116 | source: Option, 117 | serialize_func: Option, 118 | alias: Option, 119 | ) -> Self { 120 | $struct_name { 121 | base: BaseField::new(write_only, strict, call, default, source, serialize_func, alias, false), 122 | $( $field_name, )* 123 | } 124 | } 125 | 126 | #[getter] 127 | fn is_strict(&self) -> bool { 128 | self.base.strict 129 | } 130 | 131 | $($method)* 132 | } 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::ValidationError; 2 | use crate::fields::base::BaseField; 3 | use crate::fields::base::Field; 4 | use crate::fields::base::FieldTrait; 5 | use pyo3::intern; 6 | use pyo3::prelude::*; 7 | use pyo3::types::PyDict; 8 | use std::collections::HashMap; 9 | 10 | #[pyclass(subclass, dict)] 11 | #[derive(Clone)] 12 | pub struct Schema { 13 | pub base: BaseField, 14 | fields: HashMap, 15 | #[pyo3(get, set)] 16 | context: HashMap, 17 | } 18 | impl_py_methods!(Schema, required, { fields: HashMap, context: HashMap}, { 19 | fn get_attr_value( 20 | &self, 21 | py: Python, 22 | mut instance: PyObject, 23 | field: Field, 24 | key: &str, 25 | ) -> PyResult { 26 | let attr = if let Some(source) = field.source(py)? { 27 | for attr_name in source.split('.') { 28 | instance = instance.getattr(py, attr_name)?; 29 | } 30 | instance 31 | } else { 32 | instance.getattr(py, key)? 33 | }; 34 | if field.call(py)? { 35 | attr.call0(py) 36 | } else { 37 | Ok(attr) 38 | } 39 | } 40 | fn serialize_attr_value( 41 | &self, 42 | py: Python, 43 | attr_value: PyObject, 44 | field: Field, 45 | ) -> PyResult { 46 | if attr_value.is_none(py) { 47 | field.default_value(py)?.map_or_else( 48 | || Ok(py.None()), 49 | |default_value| Ok(default_value.to_object(py)), 50 | ) 51 | } else { 52 | field.serialize(py, attr_value.as_ref(py), None) 53 | } 54 | } 55 | fn handle_method_field( 56 | &self, 57 | py: Python, 58 | key: &str, 59 | field: Field, 60 | instance: PyObject, 61 | parent: Option, 62 | ) -> PyResult { 63 | let method_result = field.method_getter(py, key, parent.clone().into_py(py).as_ref(py))?; 64 | let result = method_result.call1(py, (self.clone(),instance,))?; 65 | Ok(result) 66 | } 67 | 68 | fn add_error(&self, py: Python, errors: &PyDict, key: &str, error: PyObject) -> PyResult<()> { 69 | errors.set_item(key, error.call_method0(py, intern!(py, "__str__"))?) 70 | } 71 | 72 | pub fn serialize( 73 | &self, 74 | py: Python, 75 | instance: &PyAny, 76 | many: Option, 77 | parent: Option, 78 | ) -> PyResult { 79 | if instance.is_none() { 80 | return Ok(py.None()); 81 | } 82 | 83 | if let Some(callback) = &self.base.serialize_func { 84 | return callback.call1(py, (instance,)); 85 | } 86 | 87 | if many == Some(true) { 88 | if let Ok(iter) = instance.iter() { 89 | let mut results: Vec = Vec::with_capacity(iter.size_hint().0); 90 | for inst in iter { 91 | let serialized = self.serialize_one(py, inst?, parent.clone())?; 92 | results.push(serialized); 93 | } 94 | return Ok(results.into_py(py)); 95 | } else { 96 | return Err( 97 | pyo3::exceptions::PyTypeError::new_err("Expected an iterable"), 98 | ); 99 | } 100 | } 101 | self.serialize_one(py, instance, parent) 102 | } 103 | fn serialize_one( 104 | &self, 105 | py: Python, 106 | instance: &PyAny, 107 | parent: Option, 108 | ) -> PyResult { 109 | let serialized_data = PyDict::new(py); 110 | let errors = PyDict::new(py); 111 | for (key, field) in &self.fields { 112 | if field.is_write_only(py)? { 113 | continue; 114 | } 115 | 116 | let alias = field.alias(py)?.unwrap_or(key.to_string()); 117 | 118 | let instance_ref = instance.into(); 119 | 120 | if field.is_method_field(py)? { 121 | match self.handle_method_field( 122 | py, 123 | key, 124 | field.clone(), 125 | instance_ref, 126 | parent.clone(), 127 | ) { 128 | Ok(value) => serialized_data.set_item(alias, value)?, 129 | Err(e) => { 130 | self.add_error(py, errors, key, e.to_object(py))?; 131 | } 132 | } 133 | continue; 134 | } 135 | match self.get_attr_value(py, instance_ref, field.clone(), key) 136 | .and_then(|val| self.serialize_attr_value(py, val, field.clone())) { 137 | Ok(value) => serialized_data.set_item(alias, value)?, 138 | Err(e) => { 139 | self.add_error(py, errors, key, e.to_object(py))?; 140 | } 141 | } 142 | } 143 | 144 | if !errors.is_empty() { 145 | Err(PyErr::new::(errors.to_object(py))) 146 | } else { 147 | Ok(serialized_data.into()) 148 | } 149 | } 150 | 151 | }); 152 | 153 | impl FieldTrait for Schema { 154 | fn default(&self) -> Option { 155 | self.base.default.clone() 156 | } 157 | fn is_write_only(&self) -> bool { 158 | self.base.write_only 159 | } 160 | fn source(&self) -> Option { 161 | self.base.source.clone() 162 | } 163 | fn is_method_field(&self) -> bool { 164 | self.base.is_method_field 165 | } 166 | fn call(&self) -> bool { 167 | self.base.call 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tests/test_any_field.py: -------------------------------------------------------------------------------- 1 | import schemars 2 | 3 | def test_serialize_valid_any(): 4 | any_field = schemars.Any() 5 | valid_value = "Hello World" 6 | result = any_field.serialize(valid_value) 7 | assert result == valid_value -------------------------------------------------------------------------------- /tests/test_bool_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | 4 | @pytest.mark.parametrize("input_bool", [True, False]) 5 | def test_serialize_valid_boolean(input_bool): 6 | bool_field = schemars.Bool(strict=True) 7 | result = bool_field.serialize(input_bool) 8 | assert result == input_bool 9 | 10 | @pytest.mark.parametrize("input_str, expected", [("true", True), ("false", False)]) 11 | def test_serialize_string_non_strict(input_str, expected): 12 | bool_field = schemars.Bool() 13 | result = bool_field.serialize(input_str) 14 | assert result == expected 15 | 16 | @pytest.mark.parametrize("input_value, expected", [(1, True), (0, False)]) 17 | def test_serialize_int_float_non_strict(input_value, expected): 18 | bool_field = schemars.Bool() 19 | result = bool_field.serialize(input_value) 20 | assert result == expected 21 | 22 | @pytest.mark.parametrize("invalid_input", ["invalid", 2, 0.5]) 23 | def test_serialize_invalid_data_non_strict(invalid_input): 24 | bool_field = schemars.Bool() 25 | with pytest.raises(schemars.ValidationError): 26 | bool_field.serialize(invalid_input) 27 | 28 | @pytest.mark.parametrize("edge_case", ["yes", "no", "1", "0", "t", "f"]) 29 | def test_serialize_edge_cases(edge_case): 30 | bool_field = schemars.Bool() 31 | result = bool_field.serialize(edge_case) 32 | assert result in [True, False] 33 | -------------------------------------------------------------------------------- /tests/test_bytes_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | 4 | def test_serialize_valid_bytes(): 5 | bytes_field = schemars.Bytes(strict=True) 6 | test_bytes = b"Hello World" 7 | result = bytes_field.serialize(test_bytes) 8 | assert result == test_bytes 9 | 10 | def test_serialize_string_non_strict(): 11 | bytes_field = schemars.Bytes() 12 | test_str = "Hello World" 13 | result = bytes_field.serialize(test_str) 14 | assert result == test_str.encode() 15 | 16 | @pytest.mark.parametrize("invalid_input", [123, 42.0, True, [1, 2, 3]]) 17 | def test_serialize_invalid_data_non_strict(invalid_input): 18 | bytes_field = schemars.Bytes() 19 | with pytest.raises(schemars.ValidationError): 20 | bytes_field.serialize(invalid_input) 21 | 22 | @pytest.mark.parametrize("edge_case", ["", "😊", "a" * 1000]) 23 | def test_serialize_edge_cases(edge_case): 24 | bytes_field = schemars.Bytes() 25 | result = bytes_field.serialize(edge_case) 26 | assert result == edge_case.encode() 27 | -------------------------------------------------------------------------------- /tests/test_date_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | from datetime import date, datetime 4 | 5 | def test_serialize_date(): 6 | date_field = schemars.Date(strict=True) 7 | test_date = date(2021, 4, 5) 8 | result = date_field.serialize(test_date) 9 | assert result == test_date.isoformat() 10 | 11 | def test_serialize_datetime_non_strict(): 12 | date_field = schemars.Date() 13 | test_datetime = datetime(2021, 4, 5, 12, 30) 14 | result = date_field.serialize(test_datetime) 15 | assert result == test_datetime.date().isoformat() 16 | 17 | @pytest.mark.parametrize("input_value", ["2020-01-01", 1577836800]) 18 | def test_serialize_string_long_float_non_strict(input_value): 19 | date_field = schemars.Date() 20 | result = date_field.serialize(input_value) 21 | assert result == "2020-01-01" 22 | 23 | def test_serialize_with_custom_format(): 24 | date_field = schemars.Date(format="%Y/%m/%d", strict=True) 25 | test_date = date(2021, 4, 5) 26 | result = date_field.serialize(test_date) 27 | assert result == "2021/04/05" 28 | 29 | @pytest.mark.parametrize("invalid_input", ["invalid date", 123456, True]) 30 | def test_serialize_invalid_data(invalid_input): 31 | date_field = schemars.Date(strict=True) 32 | with pytest.raises(schemars.ValidationError): 33 | date_field.serialize(invalid_input) 34 | -------------------------------------------------------------------------------- /tests/test_datetime_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | from datetime import date, datetime 4 | 5 | def test_serialize_datetime(): 6 | datetime_field = schemars.DateTime(strict=True) 7 | test_datetime = datetime(2021, 4, 5, 15, 30) 8 | result = datetime_field.serialize(test_datetime) 9 | assert result == test_datetime.isoformat() 10 | 11 | def test_serialize_date_non_strict(): 12 | datetime_field = schemars.DateTime() 13 | test_date = date(2021, 4, 5) 14 | result = datetime_field.serialize(test_date) 15 | expected_datetime = datetime.combine(test_date, datetime.min.time()) 16 | assert result == expected_datetime.isoformat() 17 | 18 | @pytest.mark.parametrize("input_value", ["2021-04-05T16:50:00", 1617641400, 1617641400.0]) 19 | def test_serialize_string_long_float_non_strict(input_value): 20 | datetime_field = schemars.DateTime() 21 | result = datetime_field.serialize(input_value) 22 | expected_datetime = datetime(2021, 4, 5, 16, 50) 23 | assert result == expected_datetime.isoformat() 24 | 25 | def test_serialize_with_custom_format(): 26 | datetime_field = schemars.DateTime(format="%Y/%m/%d %H:%M", strict=True) 27 | test_datetime = datetime(2021, 4, 5, 15, 30) 28 | result = datetime_field.serialize(test_datetime) 29 | assert result == test_datetime.strftime("%Y/%m/%d %H:%M") 30 | 31 | @pytest.mark.parametrize("invalid_input", ["invalid datetime", 123456, True]) 32 | def test_serialize_invalid_data(invalid_input): 33 | datetime_field = schemars.DateTime(strict=True) 34 | with pytest.raises(schemars.ValidationError): 35 | datetime_field.serialize(invalid_input) 36 | -------------------------------------------------------------------------------- /tests/test_decimal_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | from decimal import Decimal as PyDecimal 4 | 5 | 6 | def test_serialize_decimal_compatible(): 7 | decimal_field = schemars.Decimal(strict=True) 8 | test_decimal = PyDecimal("10.5") 9 | result = decimal_field.serialize(test_decimal) 10 | assert result == str(test_decimal) 11 | 12 | 13 | def test_serialize_float_non_strict(): 14 | decimal_field = schemars.Decimal() 15 | test_float = 10.5 16 | result = decimal_field.serialize(test_float) 17 | assert result == str(PyDecimal(test_float)) 18 | 19 | 20 | def test_serialize_string_non_strict(): 21 | decimal_field = schemars.Decimal() 22 | test_str = "10.5" 23 | result = decimal_field.serialize(test_str) 24 | assert result == str(PyDecimal(test_str)) 25 | 26 | 27 | def test_serialize_integer_non_strict(): 28 | decimal_field = schemars.Decimal() 29 | test_int = 10 30 | result = decimal_field.serialize(test_int) 31 | assert result == str(PyDecimal(test_int)) 32 | 33 | 34 | @pytest.mark.parametrize("invalid_input", [True, "not a number", 1 + 1j]) 35 | def test_serialize_invalid_data_non_strict(invalid_input): 36 | decimal_field = schemars.Decimal() 37 | with pytest.raises(schemars.ValidationError): 38 | decimal_field.serialize(invalid_input) 39 | -------------------------------------------------------------------------------- /tests/test_dict_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | from typing import Mapping 4 | 5 | 6 | def test_serialize_valid_dict(): 7 | dict_field = schemars.Dict(strict=True) 8 | test_dict = {"key1": 1, "key2": 2} 9 | result = dict_field.serialize(test_dict) 10 | assert result == test_dict 11 | 12 | 13 | def test_serialize_mapping_non_strict(): 14 | class CustomDict(Mapping): 15 | def __init__(self): 16 | self._dict = {"key1": 1, "key2": 2} 17 | 18 | def __getitem__(self, key): 19 | return self._dict[key] 20 | 21 | def __iter__(self): 22 | return iter(self._dict) 23 | 24 | def __len__(self): 25 | return len(self._dict) 26 | 27 | dict_field = schemars.Dict() 28 | test_obj = CustomDict() 29 | result = dict_field.serialize(test_obj) 30 | assert result == {"key1": 1, "key2": 2} 31 | 32 | 33 | def test_serialize_non_dict_non_mapping_non_strict(): 34 | dict_field = schemars.Dict() 35 | with pytest.raises(schemars.ValidationError): 36 | dict_field.serialize(123) 37 | 38 | 39 | def test_serialize_with_child(): 40 | dict_field = schemars.Dict(child=schemars.Int()) 41 | test_dict = {"key1": 1, "key2": 2} 42 | result = dict_field.serialize(test_dict) 43 | assert result == test_dict 44 | 45 | 46 | def test_serialize_with_invalid_child(): 47 | dict_field = schemars.Dict(child=schemars.Str()) 48 | test_dict = {"key1": 1, "key2": 2} 49 | with pytest.raises(schemars.ValidationError): 50 | dict_field.serialize(test_dict) 51 | 52 | 53 | @pytest.mark.parametrize( 54 | "edge_case", [{}, {"key": "value", "nested": {"nested_key": 1}}, {"empty_dict": {}}] 55 | ) 56 | def test_serialize_edge_cases(edge_case): 57 | dict_field = schemars.Dict(strict=True) 58 | result = dict_field.serialize(edge_case) 59 | assert result == edge_case 60 | -------------------------------------------------------------------------------- /tests/test_float_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | 4 | def test_serialize_valid_float(): 5 | float_field = schemars.Float(strict=True) 6 | result = float_field.serialize(42.0) 7 | assert result == 42.0 8 | 9 | def test_serialize_non_float_strict(): 10 | float_field = schemars.Float(strict=True) 11 | with pytest.raises(schemars.ValidationError): 12 | float_field.serialize("42.0") 13 | 14 | @pytest.mark.parametrize("input_bool, expected", [(True, 1.0), (False, 0.0)]) 15 | def test_serialize_boolean_non_strict(input_bool, expected): 16 | float_field = schemars.Float() 17 | result = float_field.serialize(input_bool) 18 | assert result == expected 19 | 20 | def test_serialize_string_non_strict(): 21 | float_field = schemars.Float() 22 | result = float_field.serialize("42.0") 23 | assert result == 42.0 24 | 25 | def test_serialize_integer_non_strict(): 26 | float_field = schemars.Float() 27 | result = float_field.serialize(42) 28 | assert result == 42.0 29 | 30 | @pytest.mark.parametrize("invalid_input", ["not a number", "42.5a"]) 31 | def test_serialize_invalid_string_non_strict(invalid_input): 32 | float_field = schemars.Float() 33 | with pytest.raises(schemars.ValidationError): 34 | float_field.serialize(invalid_input) 35 | 36 | @pytest.mark.parametrize("edge_case", [1.79e308, -1.79e308, 0.0]) 37 | def test_serialize_edge_cases(edge_case): 38 | float_field = schemars.Float(strict=True) 39 | result = float_field.serialize(edge_case) 40 | assert result == edge_case 41 | -------------------------------------------------------------------------------- /tests/test_int_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | 4 | def test_serialize_valid_integer(): 5 | int_field = schemars.Int(strict=True) 6 | result = int_field.serialize(42) 7 | assert result == 42 8 | 9 | def test_serialize_non_integer_strict(): 10 | int_field = schemars.Int(strict=True) 11 | with pytest.raises(schemars.ValidationError): 12 | int_field.serialize("42") 13 | 14 | @pytest.mark.parametrize("input_bool, expected", [(True, 1), (False, 0)]) 15 | def test_serialize_boolean_non_strict(input_bool, expected): 16 | int_field = schemars.Int() 17 | result = int_field.serialize(input_bool) 18 | assert result == expected 19 | 20 | def test_serialize_string_non_strict(): 21 | int_field = schemars.Int() 22 | result = int_field.serialize("42") 23 | assert result == 42 24 | 25 | def test_serialize_float_non_strict(): 26 | int_field = schemars.Int() 27 | result = int_field.serialize(42.0) 28 | assert result == 42 29 | 30 | @pytest.mark.parametrize("invalid_input", ["not a number", 42.5]) 31 | def test_serialize_invalid_string_float_non_strict(invalid_input): 32 | int_field = schemars.Int() 33 | with pytest.raises(schemars.ValidationError): 34 | int_field.serialize(invalid_input) 35 | 36 | @pytest.mark.parametrize("edge_case", [2147483647, -2147483648, 0]) 37 | def test_serialize_edge_cases(edge_case): 38 | int_field = schemars.Int(strict=True) 39 | result = int_field.serialize(edge_case) 40 | assert result == edge_case 41 | -------------------------------------------------------------------------------- /tests/test_list_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | 4 | def test_serialize_valid_list(): 5 | list_field = schemars.List(strict=True) 6 | test_list = [1, 2, 3] 7 | result = list_field.serialize(test_list) 8 | assert result == test_list 9 | 10 | def test_serialize_non_list_iterable_non_strict(): 11 | list_field = schemars.List() 12 | test_tuple = (1, 2, 3) 13 | result = list_field.serialize(test_tuple) 14 | assert result == list(test_tuple) 15 | 16 | def test_serialize_non_iterable_non_strict(): 17 | list_field = schemars.List() 18 | with pytest.raises(TypeError): 19 | list_field.serialize(123) 20 | 21 | def test_serialize_with_valid_child_serializer(): 22 | list_field = schemars.List(child=schemars.Int()) 23 | test_list = [1, 2, 3] 24 | result = list_field.serialize(test_list) 25 | assert result == test_list 26 | 27 | def test_serialize_with_invalid_child_serializer(): 28 | list_field = schemars.List(child=schemars.Str()) 29 | test_list = [1, 2, 3] 30 | with pytest.raises(schemars.ValidationError): 31 | list_field.serialize(test_list) 32 | 33 | @pytest.mark.parametrize("edge_case", [[], [1, "a", [2, 3]], [[1], [2, 3]]]) 34 | def test_serialize_edge_cases(edge_case): 35 | list_field = schemars.List(strict=True) 36 | result = list_field.serialize(edge_case) 37 | assert result == edge_case 38 | -------------------------------------------------------------------------------- /tests/test_method_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | 4 | class Product: 5 | pass 6 | 7 | product = Product() 8 | 9 | 10 | def test_method_field_serialization(): 11 | class ProductSchema(schemars.Schema): 12 | method = schemars.Method() 13 | 14 | def get_method(self, obj): 15 | return "cool" 16 | 17 | product_schema = ProductSchema() 18 | result = product_schema.serialize(product) 19 | assert result == {"method": "cool"} 20 | 21 | def test_method_field_missing_method(): 22 | class AnotherSchema(schemars.Schema): 23 | method = schemars.Method() 24 | 25 | another_schema = AnotherSchema() 26 | with pytest.raises(schemars.ValidationError): 27 | another_schema.serialize(product) 28 | 29 | def test_method_name(): 30 | class ProductSchema(schemars.Schema): 31 | method = schemars.Method(method_name="test_method") 32 | 33 | def test_method(self, obj): 34 | return "cool" 35 | 36 | product_schema = ProductSchema() 37 | result = product_schema.serialize(product) 38 | assert result == {"method": "cool"} -------------------------------------------------------------------------------- /tests/test_schema.py: -------------------------------------------------------------------------------- 1 | import schemars 2 | 3 | 4 | def test_serialize_many(): 5 | class Product: 6 | def __init__(self, name): 7 | self.name = name 8 | 9 | class ProductSchema(schemars.Schema): 10 | name = schemars.Str() 11 | 12 | schema = ProductSchema() 13 | products = [ 14 | Product("Product 1"), 15 | Product("Product 2"), 16 | ] 17 | result = schema.serialize(products, many=True) 18 | assert result == [{"name": "Product 1"}, {"name": "Product 2"}] 19 | 20 | 21 | def test_serialize_with_default(): 22 | class Product: 23 | def __init__(self, name=None): 24 | self.name = name 25 | 26 | class ProductSchema(schemars.Schema): 27 | name = schemars.Str(default="Product 1") 28 | 29 | schema = ProductSchema() 30 | product = Product() 31 | result = schema.serialize(product) 32 | assert result == {"name": "Product 1"} 33 | 34 | 35 | def test_serialize_with_default_none(): 36 | class Product: 37 | def __init__(self, name=None): 38 | self.name = name 39 | 40 | class ProductSchema(schemars.Schema): 41 | name = schemars.Str(default=None) 42 | 43 | schema = ProductSchema() 44 | product = Product() 45 | result = schema.serialize(product) 46 | assert result == {"name": None} 47 | 48 | 49 | def test_serialize_with_write_only(): 50 | class Product: 51 | def __init__(self, name): 52 | self.name = name 53 | 54 | class ProductSchema(schemars.Schema): 55 | name = schemars.Str(write_only=True) 56 | 57 | schema = ProductSchema() 58 | product = Product("Product 1") 59 | result = schema.serialize(product) 60 | assert result == {} 61 | 62 | 63 | def test_serialize_with_source(): 64 | class User: 65 | def __init__(self, name, age): 66 | self.name = name 67 | self.age = age 68 | 69 | class Product: 70 | def __init__(self, user): 71 | self.user = user 72 | 73 | class ProductSchema(schemars.Schema): 74 | name = schemars.Str(source="user.name") 75 | age = schemars.Int(source="user.age") 76 | 77 | schema = ProductSchema() 78 | user = User("John", 30) 79 | product = Product(user) 80 | result = schema.serialize(product) 81 | assert result == {"name": "John", "age": 30} 82 | 83 | 84 | def test_serialize_with_call(): 85 | def custom_func(): 86 | return "test" 87 | 88 | def get_tags(): 89 | return [ 90 | Tag("tag1"), 91 | Tag("tag2"), 92 | ] 93 | 94 | class Product: 95 | @property 96 | def test(self): 97 | return custom_func 98 | 99 | @property 100 | def tags(self): 101 | return get_tags 102 | 103 | class Tag: 104 | def __init__(self, name): 105 | self.name = name 106 | 107 | class TagSchema(schemars.Schema): 108 | name = schemars.Str() 109 | 110 | class ProductSchema(schemars.Schema): 111 | test = schemars.Str(call=True) 112 | tags = TagSchema(many=True, call=True) 113 | 114 | schema = ProductSchema() 115 | product = Product() 116 | result = schema.serialize(product) 117 | assert result == {"test": "test", "tags": [{"name": "tag1"}, {"name": "tag2"}]} 118 | 119 | 120 | def test_serialize_with_serialize_func(): 121 | class ProductSchema(schemars.Schema): 122 | name = schemars.Str(serialize_func=lambda name: name.upper()) 123 | 124 | class Product: 125 | def __init__(self, name): 126 | self.name = name 127 | 128 | schema = ProductSchema() 129 | product = Product("Product 1") 130 | result = schema.serialize(product) 131 | assert result == {"name": "PRODUCT 1"} 132 | 133 | 134 | def test_serialize_with_nested(): 135 | class User: 136 | def __init__(self, name, age): 137 | self.name = name 138 | self.age = age 139 | 140 | class Product: 141 | def __init__(self, user): 142 | self.user = user 143 | 144 | class UserSchema(schemars.Schema): 145 | name = schemars.Str() 146 | age = schemars.Int() 147 | 148 | class ProductSchema(schemars.Schema): 149 | user = UserSchema() 150 | 151 | schema = ProductSchema() 152 | user = User("John", 30) 153 | product = Product(user) 154 | result = schema.serialize(product) 155 | assert result == {"user": {"name": "John", "age": 30}} 156 | 157 | 158 | def test_serialize_with_nested_many(): 159 | class User: 160 | def __init__(self, name, age): 161 | self.name = name 162 | self.age = age 163 | 164 | class Product: 165 | def __init__(self, users): 166 | self.users = users 167 | 168 | class UserSchema(schemars.Schema): 169 | name = schemars.Str() 170 | age = schemars.Int() 171 | 172 | class ProductSchema(schemars.Schema): 173 | users = UserSchema(many=True) 174 | 175 | schema = ProductSchema() 176 | user = User("John", 30) 177 | product = Product([user]) 178 | result = schema.serialize(product) 179 | assert result == {"users": [{"name": "John", "age": 30}]} 180 | 181 | 182 | def test_serialize_with_context(): 183 | class Product: 184 | def __init__(self, name): 185 | self.name = name 186 | 187 | class ProductSchema(schemars.Schema): 188 | name = schemars.Str() 189 | method = schemars.Method() 190 | 191 | def get_method(self, obj): 192 | return self.context.get("suffix") 193 | 194 | schema = ProductSchema(context={"suffix": "test"}) 195 | product = Product("Product 1") 196 | result = schema.serialize(product) 197 | assert result == {"name": "Product 1", "method": "test"} 198 | 199 | 200 | def test_serialize_with_inheritance(): 201 | class Product: 202 | def __init__(self, name, display_name, related_products): 203 | self.name = name 204 | self.display_name = display_name 205 | self.related_products = related_products 206 | 207 | class BaseProductSchema(schemars.Schema): 208 | name = schemars.Str() 209 | display_name = schemars.Str() 210 | 211 | class ProductSchema(BaseProductSchema): 212 | related_products = BaseProductSchema(many=True) 213 | 214 | schema = ProductSchema() 215 | product = Product("Product 1", "Product 1", [Product("Product 2", "Product 2", [])]) 216 | result = schema.serialize(product) 217 | assert result == { 218 | "name": "Product 1", 219 | "display_name": "Product 1", 220 | "related_products": [{"name": "Product 2", "display_name": "Product 2"}], 221 | } 222 | 223 | 224 | def test_serialize_with_custom_attributes(): 225 | class Product: 226 | pass 227 | 228 | class ProductSchema(schemars.Schema): 229 | method = schemars.Method() 230 | 231 | def get_method(self, obj): 232 | self.test = "test" 233 | return self.test 234 | 235 | schema = ProductSchema() 236 | product = Product() 237 | result = schema.serialize(product) 238 | assert result == {"method": "test"} 239 | 240 | def test_serialize_with_alias(): 241 | class Product: 242 | def __init__(self): 243 | self.name = "Product 1" 244 | 245 | class ProductSchema(schemars.Schema): 246 | name = schemars.Str(alias="my_name") 247 | 248 | schema = ProductSchema() 249 | product = Product() 250 | result = schema.serialize(product) 251 | assert result == {"my_name": "Product 1"} -------------------------------------------------------------------------------- /tests/test_str_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | 4 | def test_serialize_valid_string(): 5 | str_field = schemars.Str(strict=True) 6 | result = str_field.serialize("Hello World") 7 | assert result == "Hello World" 8 | 9 | def test_serialize_non_string_strict(): 10 | str_field = schemars.Str(strict=True) 11 | with pytest.raises(schemars.ValidationError): 12 | str_field.serialize(123) 13 | 14 | def test_serialize_bytes_non_strict(): 15 | str_field = schemars.Str() 16 | result = str_field.serialize(b"Hello World") 17 | assert result == "Hello World" 18 | 19 | def test_serialize_non_utf8_bytes_non_strict(): 20 | str_field = schemars.Str() 21 | result = str_field.serialize(b'\xff\xfe\xfd') 22 | assert result == '\ufffd\ufffd\ufffd' 23 | 24 | def test_serialize_non_string_non_bytes_non_strict(): 25 | str_field = schemars.Str() 26 | with pytest.raises(schemars.ValidationError): 27 | str_field.serialize([1, 2, 3]) 28 | 29 | @pytest.mark.parametrize("input_str", ["", "😊", "a" * 1000]) 30 | def test_serialize_edge_cases(input_str): 31 | str_field = schemars.Str(strict=True) 32 | result = str_field.serialize(input_str) 33 | assert result == input_str 34 | -------------------------------------------------------------------------------- /tests/test_union_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | 4 | def test_serialize_success_union_field(): 5 | union_field = schemars.Union(fields=[schemars.Str(), schemars.Int()]) 6 | test_str = "test" 7 | result = union_field.serialize(test_str) 8 | assert result == test_str 9 | 10 | test_int = 123 11 | result = union_field.serialize(test_int) 12 | assert result == test_int 13 | 14 | def test_serialize_failure_union_field(): 15 | union_field = schemars.Union(fields=[schemars.Str(), schemars.Int()]) 16 | with pytest.raises(schemars.ValidationError): 17 | union_field.serialize(1.23) 18 | 19 | @pytest.mark.parametrize("edge_case", ["123", 123]) 20 | def test_serialize_edge_cases_union_field(edge_case): 21 | union_field = schemars.Union(fields=[schemars.Str(), schemars.Int()]) 22 | result = union_field.serialize(edge_case) 23 | assert result == edge_case 24 | -------------------------------------------------------------------------------- /tests/test_uuid_field.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import schemars 3 | import uuid 4 | 5 | def test_serialize_valid_uuid(): 6 | uuid_field = schemars.Uuid(strict=True) 7 | valid_uuid = uuid.uuid4() 8 | result = uuid_field.serialize(valid_uuid) 9 | assert result == str(valid_uuid) 10 | 11 | def test_serialize_non_uuid_strict(): 12 | uuid_field = schemars.Uuid(strict=True) 13 | with pytest.raises(schemars.ValidationError): 14 | uuid_field.serialize(str(uuid.uuid4())) 15 | 16 | def test_serialize_uuid_string_non_strict(): 17 | uuid_field = schemars.Uuid() 18 | valid_uuid_str = str(uuid.uuid4()) 19 | result = uuid_field.serialize(valid_uuid_str) 20 | assert result == valid_uuid_str 21 | 22 | def test_serialize_uuid_bytes_non_strict(): 23 | uuid_field = schemars.Uuid() 24 | valid_uuid_bytes = uuid.uuid4().bytes 25 | result = uuid_field.serialize(valid_uuid_bytes) 26 | assert result == str(uuid.UUID(bytes=valid_uuid_bytes)) 27 | 28 | @pytest.mark.parametrize("invalid_input", ["not a uuid", 123, True, b'invalid']) 29 | def test_serialize_invalid_input_non_strict(invalid_input): 30 | uuid_field = schemars.Uuid() 31 | with pytest.raises(schemars.ValidationError): 32 | uuid_field.serialize(invalid_input) 33 | 34 | def test_serialize_edge_cases(): 35 | uuid_field = schemars.Uuid(strict=True) 36 | edge_cases = [uuid.UUID(int=0), uuid.UUID(int=(2**128)-1)] 37 | for edge_case in edge_cases: 38 | result = uuid_field.serialize(edge_case) 39 | assert result == str(edge_case) 40 | --------------------------------------------------------------------------------