├── .cargo-rdme.toml ├── .github └── workflows │ ├── fuzz.yml │ ├── general.yml │ ├── publish.yml │ ├── readme.yml │ └── release-pr.yml ├── .gitignore ├── .release-plz.toml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── eserde ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src │ ├── _macro_impl.rs │ ├── errors.rs │ ├── impl_.rs │ ├── json.rs │ ├── lib.rs │ ├── path │ │ ├── de.rs │ │ ├── mod.rs │ │ ├── path_.rs │ │ ├── tracker.rs │ │ └── wrap.rs │ └── reporter.rs └── tests │ ├── compile_fail.rs │ ├── compile_fail │ ├── default_unimplemented.rs │ ├── default_unimplemented.stderr │ ├── malformed_serde_deserialize_with.rs │ ├── malformed_serde_deserialize_with.stderr │ ├── meta_item_attribute_syntax_bad.rs │ ├── meta_item_attribute_syntax_bad.stderr │ ├── union.rs │ └── union.stderr │ ├── default.rs │ ├── deserialize_with.rs │ ├── happy │ ├── deserialize.rs │ ├── main.rs │ └── path.rs │ ├── integration │ ├── contract.rs │ ├── enum_repr.rs │ ├── enums.rs │ ├── enums_deny_unknown_fields.rs │ ├── enums_flattened.rs │ ├── flatten.rs │ ├── main.rs │ └── structs.rs │ ├── json_types.rs │ └── std_types.rs ├── eserde_axum ├── CHANGELOG.md ├── Cargo.toml └── src │ ├── details.rs │ ├── json │ ├── json_.rs │ ├── mod.rs │ └── rejections.rs │ └── lib.rs ├── eserde_derive ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT └── src │ ├── attr.rs │ ├── emit.rs │ ├── filter_attributes.rs │ ├── lib.rs │ ├── model.rs │ └── unsupported.rs ├── fuzz ├── .gitignore ├── Cargo.toml ├── fuzz_targets │ ├── contract.rs │ ├── contract_json.rs │ ├── enum_repr.rs │ ├── enum_repr_json.rs │ ├── enums.rs │ ├── enums_deny_unknown_fields.rs │ ├── enums_deny_unknown_fields_json.rs │ ├── enums_flattened.rs │ ├── enums_flattened_json.rs │ ├── enums_json.rs │ ├── extra.rs │ ├── extra_json.rs │ ├── flatten.rs │ ├── flatten_json.rs │ ├── structs.rs │ └── structs_json.rs └── src │ └── lib.rs └── test_helper ├── Cargo.toml └── src ├── contract.rs ├── enum_repr.rs ├── enums.rs ├── enums_deny_unknown_fields.rs ├── enums_flattened.rs ├── extra.rs ├── flatten.rs ├── json.rs ├── lib.rs ├── structs.rs └── test_helper.rs /.cargo-rdme.toml: -------------------------------------------------------------------------------- 1 | workspace-project = "eserde" 2 | 3 | [intralinks] 4 | strip-links = true 5 | -------------------------------------------------------------------------------- /.github/workflows/fuzz.yml: -------------------------------------------------------------------------------- 1 | name: "Fuzz all fuzz targets" 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | duration_seconds: 7 | description: "The amount of time each target is fuzzed" 8 | required: true 9 | default: "10s" 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | fuzz: 16 | name: Fuzz 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions-rust-lang/setup-rust-toolchain@v1 21 | with: 22 | toolchain: nightly 23 | - name: Install cargo-fuzz 24 | run: cargo install --locked cargo-fuzz 25 | - name: Fuzz all targets for ${{ github.event.inputs.duration_seconds }} 26 | run: for target in $(cargo fuzz list); do echo "=== Fuzzing target $target"; cargo +nightly fuzz run --release $target -- -max_total_time=${{ github.event.inputs.duration_seconds }}; echo "=== Done fuzzing target $target"; done 27 | -------------------------------------------------------------------------------- /.github/workflows/general.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | test: 16 | name: Test 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions-rust-lang/setup-rust-toolchain@v1 21 | - name: Build tests 22 | run: cargo test --no-run 23 | - name: Run tests 24 | run: cargo test 25 | 26 | fmt: 27 | name: Format 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: actions-rust-lang/setup-rust-toolchain@v1 32 | with: 33 | components: rustfmt 34 | - name: Enforce formatting 35 | run: cargo fmt --check 36 | 37 | lint: 38 | name: Lint 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: actions-rust-lang/setup-rust-toolchain@v1 43 | with: 44 | components: clippy 45 | - name: Linting 46 | run: cargo clippy 47 | 48 | coverage: 49 | name: Code coverage 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout repository 53 | uses: actions/checkout@v4 54 | - uses: actions-rust-lang/setup-rust-toolchain@v1 55 | with: 56 | components: llvm-tools-preview 57 | - name: Install cargo-llvm-cov 58 | uses: taiki-e/install-action@cargo-llvm-cov 59 | - name: Generate code coverage 60 | run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info 61 | - name: Generate report 62 | run: cargo llvm-cov report --html --output-dir coverage --ignore-filename-regex test_helper 63 | - uses: actions/upload-artifact@v4 64 | with: 65 | name: "Coverage report" 66 | path: coverage/ 67 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish crates to crates.io" 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | release-plz: 13 | name: Release-plz 14 | runs-on: "ubuntu-latest" 15 | if: "${{ startsWith(github.event.head_commit.message, 'chore: release') || startsWith(github.event.head_commit.message, 'chore(eserde_axum): release') }}" 16 | steps: 17 | - name: Generate GitHub token 18 | uses: actions/create-github-app-token@v1 19 | id: generate-token 20 | with: 21 | app-id: ${{ secrets.RELEASER_APP_ID }} 22 | private-key: ${{ secrets.RELEASER_PRIVATE_KEY }} 23 | - name: Checkout repository 24 | uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | token: ${{ steps.generate-token.outputs.token }} 28 | - name: Install Rust toolchain 29 | uses: dtolnay/rust-toolchain@stable 30 | - name: Set git identity 31 | run: | 32 | git config --global user.name "eserde-releaser[bot]" 33 | git config --global user.email "eserde@mainmatter.com" 34 | - name: Run release-plz 35 | uses: MarcoIeni/release-plz-action@v0.5 36 | with: 37 | command: "release" 38 | project_manifest: "Cargo.toml" 39 | config: ".release-plz.toml" 40 | env: 41 | GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} 42 | RUST_LOG: "debug" 43 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/readme.yml: -------------------------------------------------------------------------------- 1 | name: README 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | test: 16 | name: README is up to date 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions-rust-lang/setup-rust-toolchain@v1 21 | - uses: taiki-e/install-action@cargo-binstall 22 | - run: | 23 | cargo binstall --force cargo-rdme 24 | - name: Check README 25 | run: | 26 | cargo rdme --check 27 | -------------------------------------------------------------------------------- /.github/workflows/release-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Open a release PR" 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | release-plz: 12 | name: Release-plz 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Generate GitHub token 16 | uses: actions/create-github-app-token@v1 17 | id: generate-token 18 | with: 19 | app-id: ${{ secrets.RELEASER_APP_ID }} 20 | private-key: ${{ secrets.RELEASER_PRIVATE_KEY }} 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | with: 24 | fetch-depth: 0 25 | token: ${{ steps.generate-token.outputs.token }} 26 | - name: Install Rust toolchain 27 | uses: dtolnay/rust-toolchain@stable 28 | - name: Run release-plz 29 | uses: MarcoIeni/release-plz-action@v0.5 30 | with: 31 | command: "release-pr" 32 | project_manifest: "Cargo.toml" 33 | config: ".release-plz.toml" 34 | env: 35 | RUST_LOG: "debug" 36 | GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.release-plz.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | changelog_update = false 3 | git_release_enable = false 4 | git_tag_enable = false 5 | semver_check = true 6 | pr_labels = ["release"] 7 | 8 | [changelog] 9 | header = """# Changelog 10 | 11 | All notable changes to this project will be documented in this file. 12 | 13 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 14 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 15 | 16 | ## [Unreleased] 17 | """ 18 | 19 | body = """ 20 | 21 | ## [{{ version | trim_start_matches(pat="v") }}]\ 22 | {%- if release_link -%}\ 23 | ({{ release_link }})\ 24 | {% endif %} \ 25 | - {{ timestamp | date(format="%Y-%m-%d") }} 26 | {% for group, commits in commits | group_by(attribute="group") %} 27 | {% if group != "other" %} 28 | ### {{ group | striptags | trim | upper_first }} 29 | {% for commit in commits %} 30 | {%- if commit.scope -%} 31 | - *({{commit.scope}})* {% if commit.breaking %}[**breaking**] {% endif %}\ 32 | {{ commit.message }}{{ self::username(commit=commit) }}\ 33 | {%- if commit.links %} \ 34 | ({% for link in commit.links %}[{{link.text}}]({{link.href}}) {% endfor -%})\ 35 | {% endif %} 36 | {% else -%} 37 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}{{ self::username(commit=commit) }}{{ self::pr(commit=commit) }} 38 | {% endif -%} 39 | {% endfor -%} 40 | {% endif %} 41 | {% endfor %} 42 | {%- if remote.contributors %} 43 | ### Contributors 44 | {% for contributor in remote.contributors %} 45 | * @{{ contributor.username }} 46 | {%- endfor %} 47 | {% endif -%} 48 | {%- macro username(commit) -%} 49 | {% if commit.remote.username %} (by @{{ commit.remote.username }}){% endif -%} 50 | {% endmacro -%} 51 | {%- macro pr(commit) -%} 52 | {% if commit.remote.pr_number %} - #{{ commit.remote.pr_number }}{% endif -%} 53 | {% endmacro -%} 54 | """ 55 | 56 | commit_parsers = [ 57 | { message = "^feat", group = "⛰️ Features" }, 58 | { message = "^fix", group = "🐛 Bug Fixes" }, 59 | { message = "^deprecated", group = " ⚠️ Deprecations" }, 60 | { message = "^perf", group = "⚡ Performance" }, 61 | { message = "^doc", group = "📚 Documentation" }, 62 | { body = ".*security", group = "🛡️ Security" }, 63 | { message = "^polish", group = "🫧 Polishing" }, 64 | { message = "^test", group = "🧪 Testing" }, 65 | { message = "^.*", skip = true }, 66 | ] 67 | 68 | [[package]] 69 | name = "eserde" 70 | changelog_update = true 71 | changelog_include = ["eserde_derive"] 72 | changelog_path = "CHANGELOG.md" 73 | git_tag_name = "{{ version }}" 74 | git_tag_enable = true 75 | git_release_enable = true 76 | 77 | [[package]] 78 | name = "eserde_axum" 79 | changelog_update = true 80 | changelog_include = ["eserde_derive"] 81 | changelog_path = "eserde_axum/CHANGELOG.md" 82 | git_tag_name = "eserde_axum-{{ version }}" 83 | git_tag_enable = true 84 | git_release_enable = true 85 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.1.6](https://github.com/mainmatter/eserde/compare/0.1.5...0.1.6) - 2025-03-12 11 | 12 | 13 | ### ⛰️ Features 14 | - implement `EDeserialize` for rest of `std`/`serde_json` types, make `impl_edeserialize_compat!` public, fix #26 #27 #37 ([#36](https://github.com/mainmatter/eserde/pull/36)) (by @MingweiSamuel) - #36 15 | - support `#[serde(with = "..")]` on fields, #18 ([#40](https://github.com/mainmatter/eserde/pull/40)) (by @MingweiSamuel) - #40 16 | 17 | 18 | 19 | ### 🐛 Bug Fixes 20 | - allow `!Default` for `#[serde(default = "..")]` fields ([#35](https://github.com/mainmatter/eserde/pull/35)) (by @MingweiSamuel) - #35 21 | 22 | 23 | ### Contributors 24 | 25 | * @MingweiSamuel 26 | 27 | ## [0.1.5](https://github.com/mainmatter/eserde/compare/0.1.4...0.1.5) - 2025-03-05 28 | 29 | 30 | ### ⛰️ Features 31 | - support `#[serde(deserialize_with = "..")]`, `#[serde(default = "..")]` for fields, fix #21 ([#23](https://github.com/mainmatter/eserde/pull/23)) (by @MingweiSamuel) - #23 32 | 33 | 34 | ### Contributors 35 | 36 | * @MingweiSamuel 37 | 38 | ## [0.1.4](https://github.com/mainmatter/eserde/compare/0.1.3...0.1.4) - 2025-03-03 39 | 40 | 41 | ### 🐛 Bug Fixes 42 | - handle generic params with bounds ([#28](https://github.com/mainmatter/eserde/pull/28)) (by @MingweiSamuel) - #28 43 | 44 | 45 | ### Contributors 46 | 47 | * @MingweiSamuel 48 | 49 | ## [0.1.3](https://github.com/mainmatter/eserde/compare/0.1.2...0.1.3) - 2025-03-03 50 | 51 | 52 | ### 🐛 Bug Fixes 53 | - error message ordering (by @MingweiSamuel) - #25 54 | - ensure `parse_nested_meta` properly handles values, fix [#24](https://github.com/mainmatter/eserde/pull/24) (by @MingweiSamuel) - #25 55 | 56 | 57 | 58 | ### 🧪 Testing 59 | - add basic trybuild tests (by @MingweiSamuel) - #25 60 | 61 | 62 | ### Contributors 63 | 64 | * @MingweiSamuel 65 | * @hdoordt 66 | 67 | ## [0.1.2](https://github.com/mainmatter/eserde/compare/0.1.1...0.1.2) - 2025-02-14 68 | 69 | 70 | ### ⛰️ Features 71 | - Introduce `eserde_axum`, to provide `axum` extractors built on top of `eserde`. (by @LukeMathWalker) - #11 72 | 73 | 74 | 75 | ### 📚 Documentation 76 | - Expand `eserde`'s crate documentation to mention `eserde_axum` as well as the underlying deserialization mechanism. (by @LukeMathWalker) - #11 77 | 78 | 79 | ### Contributors 80 | 81 | * @LukeMathWalker 82 | 83 | ## [0.1.1](https://github.com/mainmatter/eserde/compare/0.1.0...0.1.1) - 2025-02-13 84 | 85 | 86 | ### 📚 Documentation 87 | - Enable the unstable rustdoc feature required to show, on docs.rs, what feature flags must be enabled for specific items. (by @LukeMathWalker) 88 | 89 | 90 | ### Contributors 91 | 92 | * @LukeMathWalker 93 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Cutting a release 2 | 3 | We rely on [`release-plz`](https://release-plz.dev/docs) for our release automation.\ 4 | To cut a new release, trigger the [Release PR workflow](https://github.com/mainmatter/eserde/actions/workflows/release-pr.yml).\ 5 | It'll open a new PR for you to review, including changelog updates. When you merge it, 6 | the [Publish workflow](https://github.com/mainmatter/eserde/actions/workflows/publish.yml) will kick off, publishing the 7 | crates to [crates.io](https://crates.io). 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["eserde_derive", "eserde", "eserde_axum", "fuzz", "test_helper"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | edition = "2021" 7 | repository = "https://github.com/mainmatter/eserde" 8 | license = "Apache-2.0 OR MIT" 9 | version = "0.1.6" 10 | 11 | [workspace.dependencies] 12 | arbitrary = "1.4.1" 13 | axum = "0.8" 14 | axum-core = "0.5" 15 | bytes = "1" 16 | eserde = { path = "eserde" } 17 | eserde_test_helper = { path = "test_helper" } 18 | http = "1" 19 | indexmap = "2" 20 | insta = "1.42.1" 21 | itertools = "0.14" 22 | itoa = "1.0" 23 | libfuzzer-sys = "0.4" 24 | mime = { version = "0.3.17" } 25 | proc-macro2 = "1" 26 | quote = "1" 27 | serde = "1" 28 | serde_json = "1" 29 | serde_path_to_error = "0.1" 30 | syn = "2" 31 | tracing = "0.1" 32 | trybuild = "1" 33 | uuid = "1" 34 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![crates.io](https://img.shields.io/crates/v/eserde.svg)](https://crates.io/crates/eserde) 2 | [![Documentation](https://img.shields.io/docsrs/eserde)](https://docs.rs/eserde) 3 | [![Downloads crates.io](https://img.shields.io/crates/d/eserde.svg?label=crates.io%20downloads)](https://crates.io/crates/eserde) 4 | 5 | 6 | 7 | # eserde 8 | 9 | Don't stop at the first deserialization error. 10 | 11 | > ℹ️ This is a [Mainmatter](https://mainmatter.com/rust-consulting/) project. 12 | > Check out our [landing page](https://mainmatter.com/rust-consulting/) if you're looking for Rust consulting or training! 13 | 14 | ## The problem 15 | 16 | [`serde`](https://serde.rs) is **the** Rust library for (de)serialization.\ 17 | There's a catch, though: `serde` is designed to abort deserialization as soon as an error occurs. 18 | This becomes an issue when relying on `serde` for deserializing user-provided payloads—e.g. a 19 | request body for a REST API.\ 20 | There may be _several_ errors in the submitted payload, but [`serde_json`](https://crates.io/crates/serde_json) 21 | will only report the first one it encounters before stopping deserialization. 22 | The API consumer is then forced into a slow and frustrating feedback loop: 23 | 24 | 1. Send request 25 | 2. Receive a single error back 26 | 3. Fix the error 27 | 4. Back to 1., until there are no more errors to be fixed 28 | 29 | That's a poor developer experience. We should do better!\ 30 | We should report _multiple_ errors at once, thus reducing the number of API interactions 31 | required to converge to a well-formed payload. 32 | 33 | That's the problem `eserde` was born to solve. 34 | 35 | ## Case study: an invalid JSON payload 36 | 37 | Let's consider this schema as our reference example: 38 | 39 | ```rust 40 | #[derive(Debug, serde::Deserialize)] 41 | struct Package { 42 | version: Version, 43 | source: String, 44 | } 45 | 46 | #[derive(Debug, eserde::Deserialize)] 47 | struct Version { 48 | major: u32, 49 | minor: u32, 50 | patch: u32, 51 | } 52 | ``` 53 | 54 | We'll try to deserialize an invalid JSON payload into it via `serde_json`: 55 | 56 | ```rust 57 | let payload = r#" 58 | { 59 | "version": { 60 | "major": 1, 61 | "minor": "2" 62 | }, 63 | "source": null 64 | }"#; 65 | let error = serde_json::from_str::(&payload).unwrap_err(); 66 | assert_eq!( 67 | error.to_string(), 68 | r#"invalid type: string "2", expected u32 at line 5 column 24"# 69 | ); 70 | ``` 71 | 72 | Only the first error is returned, as expected. But we know there's more than that!\ 73 | We're missing the `patch` field in the `Version` struct and the `source` field can't 74 | be null.\ 75 | Let's switch to `eserde`: 76 | 77 | ```rust 78 | #[derive(Debug, eserde::Deserialize)] 79 | // ^^^^^^^^^^^^^^^^^^^ 80 | // Using `eserde::Deserialize` 81 | // instead of `serde::Deserialize`! 82 | struct Package { 83 | version: Version, 84 | source: String, 85 | } 86 | 87 | #[derive(Debug, eserde::Deserialize)] 88 | struct Version { 89 | major: u32, 90 | minor: u32, 91 | patch: u32, 92 | } 93 | 94 | let payload = r#" 95 | { 96 | "version": { 97 | "major": 1, 98 | "minor": "2" 99 | }, 100 | "source": null 101 | }"#; 102 | let errors = eserde::json::from_str::(&payload).unwrap_err(); 103 | // ^^^^^^^^^^^^ 104 | // We're not using `serde_json` directly here! 105 | assert_eq!( 106 | errors.to_string(), 107 | r#"Something went wrong during deserialization: 108 | - version.minor: invalid type: string "2", expected u32 at line 5 column 24 109 | - version: missing field `patch` 110 | - source: invalid type: null, expected a string at line 7 column 22 111 | "# 112 | ); 113 | ``` 114 | 115 | Much better, isn't it?\ 116 | We can now inform the users _in one go_ that they have to fix three different schema violations. 117 | 118 | ## Adopting `eserde` 119 | 120 | To use `eserde` in your projects, add the following dependencies to your `Cargo.toml`: 121 | 122 | ```toml 123 | [dependencies] 124 | eserde = { version = "0.1" } 125 | serde = "1" 126 | ``` 127 | 128 | You then have to: 129 | 130 | - Replace all instances of `#[derive(serde::Deserialize)]` with `#[derive(eserde::Deserialize)]` 131 | - Switch to an `eserde`-based deserialization function 132 | 133 | ### JSON 134 | 135 | `eserde` provides first-class support for JSON deserialization, gated behind the `json` Cargo feature. 136 | 137 | ```toml 138 | [dependencies] 139 | # Activating the `json` feature 140 | eserde = { version = "0.1", features = ["json"] } 141 | serde = "1" 142 | ``` 143 | 144 | If you're working with JSON: 145 | - Replace `serde_json::from_str` with `eserde::json::from_str` 146 | - Replace `serde_json::from_slice` with `eserde::json::from_slice` 147 | 148 | `eserde::json` doesn't support deserializing from a reader, i.e. there is no equivalent to 149 | `serde_json::from_reader`. 150 | 151 | There is also an `axum` integration, [`eserde_axum`](https://docs.rs/eserde_axum). 152 | It provides an `eserde`-powered JSON extractor as a drop-in replacement for `axum`'s built-in 153 | one. 154 | 155 | ### Other formats 156 | 157 | The approach used by `eserde` is compatible, in principle, with all existing `serde`-based 158 | deserializers.\ 159 | Refer to [the source code of `eserde::json::from_str`](https://github.com/mainmatter/eserde/blob/main/eserde/src/json.rs) 160 | as a blueprint to follow for building an `eserde`-powered deserialization function 161 | for another format. 162 | 163 | ## Compatibility 164 | 165 | `eserde` is designed to be maximally compatible with `serde`. 166 | 167 | [`derive(eserde::Deserialize)`](Deserialize) will implement both 168 | `serde::Deserialize` and `eserde::EDeserialize`, honoring the behaviour of all 169 | the `serde` attributes it supports. 170 | 171 | If one of your fields doesn't implement `eserde::EDeserialize`, you can annotate it with 172 | `#[eserde(compat)]` to fall back to `serde`'s default deserialization logic for that 173 | portion of the input. 174 | 175 | ```rust 176 | #[derive(eserde::Deserialize)] 177 | struct Point { 178 | // 👇 Use the `eserde::compat` attribute if you need to use 179 | // a field type that doesn't implement `eserde::EDeserialize` 180 | // and you can't derive `eserde::EDeserialize` for it (e.g. a third-party type) 181 | #[eserde(compat)] 182 | x: Latitude, 183 | // [...] 184 | } 185 | ``` 186 | 187 | Check out the documentation of `eserde`'s derive macro for more details. 188 | 189 | ## Under the hood 190 | 191 | But how does `eserde` actually work? Let's keep using JSON as an example—the same applies to other data formats.\ 192 | We try to deserialize the input via `serde_json`. If deserialization succeeds, we return the deserialized value to the caller. 193 | 194 | ```rust 195 | // The source code for `eserde::json::from_str`. 196 | pub fn from_str<'a, T>(s: &'a str) -> Result 197 | where 198 | T: EDeserialize<'a>, 199 | { 200 | let mut de = serde_json::Deserializer::from_str(s); 201 | let error = match T::deserialize(&mut de) { 202 | Ok(v) => { 203 | return Ok(v); 204 | } 205 | Err(e) => e, 206 | }; 207 | // [...] 208 | } 209 | ``` 210 | 211 | Nothing new on the happy path—it's the very same thing you're doing today in your own applications with vanilla `serde`. 212 | We diverge on the unhappy path.\ 213 | Instead of returning to the caller the error reported by `serde_json`, we do another pass over the input using 214 | `eserde::EDeserialize::deserialize_for_errors`: 215 | 216 | ```rust 217 | pub fn from_str<'a, T>(s: &'a str) -> Result 218 | where 219 | T: EDeserialize<'a>, 220 | { 221 | // [...] The code above [...] 222 | let _guard = ErrorReporter::start_deserialization(); 223 | 224 | let mut de = serde_json::Deserializer::from_str(s); 225 | let de = path::Deserializer::new(&mut de); 226 | 227 | let errors = match T::deserialize_for_errors(de) { 228 | Ok(_) => vec![], 229 | Err(_) => ErrorReporter::take_errors(), 230 | }; 231 | let errors = if errors.is_empty() { 232 | vec![DeserializationError { 233 | path: None, 234 | details: error.to_string(), 235 | }] 236 | } else { 237 | errors 238 | }; 239 | 240 | Err(DeserializationErrors::from(errors)) 241 | } 242 | ``` 243 | 244 | `EDeserialize::deserialize_for_errors` accumulates deserialization errors in a thread-local buffer, 245 | initialized by `ErrorReporter::start_deserialization` and retrieved later on 246 | by `ErrorReporter::take_errors`. 247 | 248 | This underlying complexity is encapsulated into `eserde::json`'s functions, but it's beneficial to have a mental model of 249 | what's happening under the hood if you're planning to adopt `eserde`. 250 | 251 | ## Limitations and downsides 252 | 253 | `eserde` is a new library—there may be issues and bugs that haven't been uncovered yet. 254 | Test it thoroughly before using it in production. If you encounter any problems, please 255 | open an issue on our [GitHub repository](https://github.com/mainmatter/eserde). 256 | 257 | Apart from defects, there are some downsides inherent in `eserde`'s design: 258 | 259 | - The input needs to be visited twice, hence it can't deserialize from a non-replayable reader. 260 | - The input needs to be visited twice, hence it's going to be _slower_ than a single `serde::Deserialize` 261 | pass. 262 | - `#[derive(eserde::Deserialize)]` generates more code than `serde::Deserialize` (roughly twice as much), 263 | so it'll have a bigger impact than vanilla `serde` on your compilation times. 264 | 265 | We believe the trade-off is worthwhile for user-facing payloads, but you should walk in with your 266 | eyes wide open. 267 | 268 | ## Future plans 269 | 270 | We plan to add first-class support for more data formats, in particular YAML and TOML. They are frequently 271 | used for configuration files, another scenario where batch error reporting would significantly improve 272 | our developer experience. 273 | 274 | We plan to incrementally support more and more `#[serde]` attributes, 275 | thus minimising the friction to adopting `eserde` in your codebase. 276 | 277 | We plan to add first-class support for validation, with a syntax similar to [`garde`](https://docs.rs/garde/latest/garde/) 278 | and [`validator`](https://docs.rs/validator/latest/validator/). 279 | The key difference: validation would be performed _as part of_ the deserialization process. No need to 280 | remember to call `.validate()` afterwards. 281 | 282 | 283 | 284 | # License 285 | 286 | Copyright © 2025- Mainmatter GmbH (https://mainmatter.com), released under the 287 | [MIT](./LICENSE-MIT) and [Apache](./LICENSE-APACHE) licenses. 288 | -------------------------------------------------------------------------------- /eserde/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "eserde" 3 | authors = ["Luca Palmieri "] 4 | edition.workspace = true 5 | version.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | readme = "README.md" 9 | keywords = ["serde", "serialization", "deserialization"] 10 | categories = ["encoding"] 11 | description = "Like `serde`, but it doesn't stop at the first deserialization error" 12 | 13 | [features] 14 | default = ["derive"] 15 | derive = ["serde/derive"] 16 | json = ["dep:serde_json"] 17 | 18 | [package.metadata.docs.rs] 19 | features = ["derive", "json"] 20 | 21 | [dependencies] 22 | serde = { workspace = true } 23 | eserde_derive = { path = "../eserde_derive", version = "0.1" } 24 | serde_json = { workspace = true, optional = true } 25 | itoa = { workspace = true } 26 | 27 | # This cfg cannot be enabled, but it still forces Cargo to keep eserde_derive's 28 | # version in lockstep with eserde's, even if someone depends on the two crates 29 | # separately with eserde's "derive" feature disabled. Every eserde_derive release 30 | # is compatible with exactly one eserde release because the generated code 31 | # involves nonpublic APIs which are not bound by semver. 32 | [target.'cfg(any())'.dependencies] 33 | eserde_derive = { version = "=0.1.6", path = "../eserde_derive" } 34 | 35 | [dev-dependencies] 36 | eserde = { workspace = true, features = ["json"] } 37 | eserde_test_helper = { workspace = true } 38 | insta = { workspace = true } 39 | itertools = { workspace = true } 40 | serde_path_to_error = { workspace = true } 41 | trybuild = { workspace = true } 42 | -------------------------------------------------------------------------------- /eserde/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /eserde/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /eserde/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /eserde/src/_macro_impl.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{reporter::ErrorReporter, EDeserialize}; 4 | 5 | #[derive(Debug)] 6 | pub struct MissingFieldError(&'static str); 7 | 8 | impl std::fmt::Display for MissingFieldError { 9 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 10 | write!(f, "missing field `{}`", self.0) 11 | } 12 | } 13 | 14 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Default)] 15 | pub enum MaybeInvalidOrMissing { 16 | Valid(PhantomData), 17 | Invalid, 18 | #[default] 19 | Missing, 20 | } 21 | 22 | impl MaybeInvalidOrMissing { 23 | pub fn push_error_if_missing(&self, field_name: &'static str) { 24 | if let Self::Missing = self { 25 | ErrorReporter::report(MissingFieldError(field_name)); 26 | } 27 | } 28 | } 29 | 30 | /// Used by `#[eserde(compat)]` fields (NO `#[serde(default)]`). 31 | impl<'de, T> serde::Deserialize<'de> for MaybeInvalidOrMissing 32 | where 33 | T: serde::Deserialize<'de>, 34 | { 35 | fn deserialize(deserializer: D) -> Result, D::Error> 36 | where 37 | D: serde::Deserializer<'de>, 38 | { 39 | let v = match T::deserialize(deserializer) { 40 | Ok(_) => Self::Valid(Default::default()), 41 | Err(error) => { 42 | ErrorReporter::report(error); 43 | Self::Invalid 44 | } 45 | }; 46 | Ok(v) 47 | } 48 | } 49 | 50 | /// Used by `#[serde(deserialize_with = "..")]` field (NO `#[serde(default)]`). 51 | pub fn maybe_invalid_or_missing<'de, D, T>( 52 | deserializer: D, 53 | ) -> Result, D::Error> 54 | where 55 | D: serde::Deserializer<'de>, 56 | T: EDeserialize<'de>, 57 | { 58 | let v = match T::deserialize_for_errors(deserializer) { 59 | Ok(_) => MaybeInvalidOrMissing::Valid(Default::default()), 60 | Err(_) => MaybeInvalidOrMissing::Invalid, 61 | }; 62 | Ok(v) 63 | } 64 | 65 | pub enum MaybeInvalid { 66 | Valid(PhantomData), 67 | Invalid, 68 | } 69 | 70 | impl Default for MaybeInvalid { 71 | fn default() -> Self { 72 | MaybeInvalid::Valid(PhantomData) 73 | } 74 | } 75 | 76 | impl MaybeInvalid { 77 | /// Added for simplicity in order to avoid having to distinguish in the macro 78 | /// between `MaybeInvalid` and `MaybeInvalidOrMissing`. 79 | /// To be removed in the future. 80 | pub fn push_error_if_missing(&self, _field_name: &'static str) {} 81 | } 82 | 83 | /// Used by `#[eserde(compat)]` `#[serde(default)]` fields. 84 | impl<'de, T> serde::Deserialize<'de> for MaybeInvalid 85 | where 86 | T: serde::Deserialize<'de>, 87 | { 88 | fn deserialize(deserializer: D) -> Result, D::Error> 89 | where 90 | D: serde::Deserializer<'de>, 91 | { 92 | let v = match T::deserialize(deserializer) { 93 | Ok(_) => Self::Valid(Default::default()), 94 | Err(error) => { 95 | ErrorReporter::report(error); 96 | Self::Invalid 97 | } 98 | }; 99 | Ok(v) 100 | } 101 | } 102 | 103 | /// Used by `#[serde(default, deserialize_with = "..")]` fields. 104 | pub fn maybe_invalid<'de, D, T>(deserializer: D) -> Result, D::Error> 105 | where 106 | D: serde::Deserializer<'de>, 107 | T: EDeserialize<'de>, 108 | { 109 | let v = match T::deserialize_for_errors(deserializer) { 110 | Ok(_) => MaybeInvalid::Valid(Default::default()), 111 | Err(_) => MaybeInvalid::Invalid, 112 | }; 113 | Ok(v) 114 | } 115 | -------------------------------------------------------------------------------- /eserde/src/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::path::Path; 2 | 3 | /// A collection of errors encountered while trying to deserialize a type. 4 | /// 5 | /// Use [`.iter()`](Self::iter) to iterator over the underlying [`DeserializationError`]. 6 | #[derive(Debug)] 7 | pub struct DeserializationErrors(Vec); 8 | 9 | impl From> for DeserializationErrors { 10 | fn from(errors: Vec) -> Self { 11 | DeserializationErrors(errors) 12 | } 13 | } 14 | 15 | impl DeserializationErrors { 16 | /// Iterate over references to the underlying [`DeserializationError`]s. 17 | /// 18 | /// Use [`.into_iter()`](Self::into_iter) if you need owned [`DeserializationError`]s 19 | /// from the iterator. 20 | pub fn iter(&self) -> impl ExactSizeIterator { 21 | self.0.iter() 22 | } 23 | 24 | /// The number of errors in the collection. 25 | pub fn len(&self) -> usize { 26 | self.0.len() 27 | } 28 | 29 | /// Returns `true` if the collection contains no errors. 30 | pub fn is_empty(&self) -> bool { 31 | self.0.is_empty() 32 | } 33 | } 34 | 35 | impl IntoIterator for DeserializationErrors { 36 | type Item = DeserializationError; 37 | type IntoIter = std::vec::IntoIter; 38 | 39 | fn into_iter(self) -> Self::IntoIter { 40 | self.0.into_iter() 41 | } 42 | } 43 | 44 | impl std::fmt::Display for DeserializationErrors { 45 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 46 | writeln!(f, "Something went wrong during deserialization:")?; 47 | for error in self.iter() { 48 | writeln!(f, "- {error}")?; 49 | } 50 | Ok(()) 51 | } 52 | } 53 | 54 | // Due to the design of `std`'s `Error` trait, we really don't have a good 55 | // story for combining multiple errors into a single error. 56 | // In particular, it's unclear/ill-defined what should be returned from 57 | // `source`. 58 | // We leave it to `None`, but that really sucks... 59 | impl std::error::Error for DeserializationErrors {} 60 | 61 | #[derive(Debug)] 62 | /// An error that occurred during deserialization. 63 | pub struct DeserializationError { 64 | pub(crate) path: Option, 65 | pub(crate) details: String, 66 | } 67 | 68 | impl DeserializationError { 69 | /// An explanation of what went wrong during deserialization. 70 | pub fn message(&self) -> &str { 71 | self.details.as_ref() 72 | } 73 | 74 | /// The input path at which the error occurred, when available. 75 | /// 76 | /// E.g. if the error occurred while deserializing the sub-field `foo` of the top-level 77 | /// field `bar`, the path would be `bar.foo`. 78 | pub fn path(&self) -> Option<&Path> { 79 | self.path.as_ref() 80 | } 81 | } 82 | 83 | impl std::fmt::Display for DeserializationError { 84 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 85 | if let Some(path) = &self.path { 86 | if !path.is_empty() { 87 | write!(f, "{}: ", path)?; 88 | } 89 | } 90 | write!(f, "{}", self.details) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /eserde/src/json.rs: -------------------------------------------------------------------------------- 1 | //! Deserialize JSON documents. 2 | //! 3 | //! # Example 4 | //! 5 | //! ```rust 6 | //! #[derive(serde::Serialize, eserde::Deserialize)] 7 | //! struct Person { 8 | //! name: String, 9 | //! age: u8, 10 | //! phones: Vec, 11 | //! } 12 | //! 13 | //! # fn main() { 14 | //! // Some JSON input data as a &str. Maybe this comes from the user. 15 | //! let data = r#" 16 | //! { 17 | //! "name": "John Doe", 18 | //! "age": 43, 19 | //! "phones": [ 20 | //! "+44 1234567", 21 | //! "+44 2345678" 22 | //! ] 23 | //! }"#; 24 | //! 25 | //! // Try to parse the string of data into a `Person` object. 26 | //! match eserde::json::from_str::(data) { 27 | //! Ok(p) => { 28 | //! println!("Please call {} at the number {}", p.name, p.phones[0]); 29 | //! } 30 | //! Err(errors) => { 31 | //! println!("Something went wrong during deserialization"); 32 | //! for error in errors.iter() { 33 | //! println!("{error}") 34 | //! } 35 | //! } 36 | //! } 37 | //! # } 38 | //! ``` 39 | //! 40 | //! # Implementation 41 | //! 42 | //! This module relies on [`serde_json`](https://crates.io/crates/serde_json) as 43 | //! the underlying deserializer. 44 | //! 45 | //! All deserializers in this module follow the same two-pass approach. 46 | //! Start by using `serde::Deserialize` to try to deserialize the target type. 47 | //! If it succeeds, return `Ok(value)`. 48 | //! If it fails, use `eserde::EDeserialize` to visit the input again and 49 | //! accumulate as many deserialization errors as possible. 50 | //! The errors are then returned as a vector in the `Err` variant. 51 | //! 52 | //! # Limitations 53 | //! 54 | //! ## Input must be buffered in memory 55 | //! 56 | //! We don't support deserializing from a reader, since it doesn't allow 57 | //! us to perform two passes over the input.\ 58 | //! We are restricted to input types that are buffered in memory (byte slices, 59 | //! string slices, etc.). 60 | use crate::{ 61 | impl_edeserialize_compat, path, reporter::ErrorReporter, DeserializationError, 62 | DeserializationErrors, EDeserialize, 63 | }; 64 | 65 | /// Deserialize an instance of type `T` from a string of JSON text. 66 | /// 67 | /// # Example 68 | /// 69 | /// ```rust 70 | /// #[derive(eserde::Deserialize, Debug)] 71 | /// struct User { 72 | /// fingerprint: String, 73 | /// location: String, 74 | /// } 75 | /// 76 | /// # fn main() { 77 | /// // The type of `j` is `&str` 78 | /// let j = " 79 | /// { 80 | /// \"fingerprint\": \"0xF9BA143B95FF6D82\", 81 | /// \"location\": \"Menlo Park, CA\" 82 | /// }"; 83 | /// 84 | /// let u: User = eserde::json::from_str(j).unwrap(); 85 | /// println!("{:#?}", u); 86 | /// # } 87 | /// ``` 88 | pub fn from_str<'a, T>(s: &'a str) -> Result 89 | where 90 | T: EDeserialize<'a>, 91 | { 92 | let mut de = serde_json::Deserializer::from_str(s); 93 | let error = match T::deserialize(&mut de) { 94 | Ok(v) => { 95 | return Ok(v); 96 | } 97 | Err(e) => e, 98 | }; 99 | let _guard = ErrorReporter::start_deserialization(); 100 | 101 | let mut de = serde_json::Deserializer::from_str(s); 102 | let de = path::Deserializer::new(&mut de); 103 | 104 | let errors = match T::deserialize_for_errors(de) { 105 | Ok(_) => vec![], 106 | Err(_) => ErrorReporter::take_errors(), 107 | }; 108 | let errors = if errors.is_empty() { 109 | vec![DeserializationError { 110 | path: None, 111 | details: error.to_string(), 112 | }] 113 | } else { 114 | errors 115 | }; 116 | 117 | Err(DeserializationErrors::from(errors)) 118 | } 119 | 120 | /// Deserialize an instance of type `T` from bytes of JSON text. 121 | /// 122 | /// # Example 123 | /// 124 | /// ```rust 125 | /// #[derive(eserde::Deserialize, Debug)] 126 | /// struct User { 127 | /// fingerprint: String, 128 | /// location: String, 129 | /// } 130 | /// 131 | /// # fn main() { 132 | /// // The type of `j` is `&[u8]` 133 | /// let j = b" 134 | /// { 135 | /// \"fingerprint\": \"0xF9BA143B95FF6D82\", 136 | /// \"location\": \"Menlo Park, CA\" 137 | /// }"; 138 | /// 139 | /// let u: User = eserde::json::from_slice(j).unwrap(); 140 | /// println!("{:#?}", u); 141 | /// # } 142 | /// ``` 143 | pub fn from_slice<'a, T>(s: &'a [u8]) -> Result 144 | where 145 | T: EDeserialize<'a>, 146 | { 147 | let mut de = serde_json::Deserializer::from_slice(s); 148 | let error = match T::deserialize(&mut de) { 149 | Ok(v) => { 150 | return Ok(v); 151 | } 152 | Err(e) => e, 153 | }; 154 | let _guard = ErrorReporter::start_deserialization(); 155 | 156 | let mut de = serde_json::Deserializer::from_slice(s); 157 | let de = path::Deserializer::new(&mut de); 158 | 159 | let errors = match T::deserialize_for_errors(de) { 160 | Ok(_) => vec![], 161 | Err(_) => ErrorReporter::take_errors(), 162 | }; 163 | let errors = if errors.is_empty() { 164 | vec![DeserializationError { 165 | path: None, 166 | details: error.to_string(), 167 | }] 168 | } else { 169 | errors 170 | }; 171 | 172 | Err(DeserializationErrors::from(errors)) 173 | } 174 | 175 | impl_edeserialize_compat! { 176 | serde_json::value::Number, 177 | serde_json::value::Value, 178 | serde_json::value::Map, 179 | } 180 | -------------------------------------------------------------------------------- /eserde/src/path/mod.rs: -------------------------------------------------------------------------------- 1 | //! Represent locations in structured data through a hierarchical path structure. 2 | //! 3 | //! The main type is [`Path`], which represents a full path sequence 4 | //! composed of individual [`Segment`]s. 5 | //! 6 | //! ## Design 7 | //! 8 | //! The design for this module was inspired by the approach followed in 9 | //! [`serde_path_to_error`](https://crates.io/crates/serde_path_to_error). 10 | mod de; 11 | mod path_; 12 | mod tracker; 13 | mod wrap; 14 | 15 | #[allow(unused)] 16 | pub(crate) use de::Deserializer; 17 | pub use path_::{Path, Segment, Segments}; 18 | pub(crate) use tracker::PathTracker; 19 | -------------------------------------------------------------------------------- /eserde/src/path/path_.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | use std::slice; 3 | 4 | /// Logical path to the error location. 5 | /// 6 | /// The path can target specific positions in sequences (`[0]`), mappings (`foo`), 7 | /// and enum variants (`Bar`). Multiple levels can be chained together 8 | /// with periods, for example `foo[0].bar`. 9 | /// 10 | /// Use `path.to_string()` to get a string representation of the path with 11 | /// segments separated by periods, or use [`path.iter()`](Path::iter) to iterate over 12 | /// individual segments of the path. 13 | #[derive(Clone, Debug, Default)] 14 | pub struct Path { 15 | segments: Vec, 16 | } 17 | 18 | impl From> for Path { 19 | fn from(segments: Vec) -> Self { 20 | Self { segments } 21 | } 22 | } 23 | 24 | /// Single segment of a path. 25 | #[derive(Clone, Debug, PartialEq, Eq)] 26 | pub enum Segment { 27 | /// An index into a sequence. 28 | /// 29 | /// Represented with the pattern `[0]`. 30 | Seq { 31 | /// The index pointing at the problematic element. 32 | index: usize, 33 | }, 34 | /// A key for a map or struct type. 35 | Map { 36 | /// The name of the key. 37 | key: String, 38 | }, 39 | /// A variant within an enum type. 40 | Enum { 41 | /// The name of the variant. 42 | variant: String, 43 | }, 44 | } 45 | 46 | impl Path { 47 | /// Returns `true` if there are no segments in this path, 48 | /// `false` otherwise. 49 | pub fn is_empty(&self) -> bool { 50 | self.segments.is_empty() 51 | } 52 | 53 | /// Returns an iterator with element type [`&Segment`][Segment]. 54 | pub fn iter(&self) -> Segments { 55 | Segments { 56 | iter: self.segments.iter(), 57 | } 58 | } 59 | 60 | pub(crate) fn segments(&self) -> &[Segment] { 61 | &self.segments 62 | } 63 | } 64 | 65 | impl<'a> IntoIterator for &'a Path { 66 | type Item = &'a Segment; 67 | type IntoIter = Segments<'a>; 68 | 69 | fn into_iter(self) -> Self::IntoIter { 70 | self.iter() 71 | } 72 | } 73 | 74 | /// Iterator over segments of a path. 75 | /// 76 | /// Returned by [`Path::iter`]. 77 | pub struct Segments<'a> { 78 | iter: slice::Iter<'a, Segment>, 79 | } 80 | 81 | impl<'a> Iterator for Segments<'a> { 82 | type Item = &'a Segment; 83 | 84 | fn next(&mut self) -> Option { 85 | self.iter.next() 86 | } 87 | 88 | fn size_hint(&self) -> (usize, Option) { 89 | self.iter.size_hint() 90 | } 91 | } 92 | 93 | impl DoubleEndedIterator for Segments<'_> { 94 | fn next_back(&mut self) -> Option { 95 | self.iter.next_back() 96 | } 97 | } 98 | 99 | impl ExactSizeIterator for Segments<'_> { 100 | fn len(&self) -> usize { 101 | self.iter.len() 102 | } 103 | } 104 | 105 | impl Display for Path { 106 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 107 | if self.segments.is_empty() { 108 | return formatter.write_str("."); 109 | } 110 | 111 | let mut separator = ""; 112 | for segment in self { 113 | if !matches!(segment, Segment::Seq { .. }) { 114 | formatter.write_str(separator)?; 115 | } 116 | write!(formatter, "{}", segment)?; 117 | separator = "."; 118 | } 119 | 120 | Ok(()) 121 | } 122 | } 123 | 124 | impl Display for Segment { 125 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 126 | match self { 127 | Segment::Seq { index } => write!(formatter, "[{}]", index), 128 | Segment::Map { key } | Segment::Enum { variant: key } => { 129 | write!(formatter, "{}", key) 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /eserde/src/path/tracker.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use super::{Path, Segment}; 4 | 5 | pub struct PathTracker; 6 | 7 | impl PathTracker { 8 | pub fn init() { 9 | CURRENT_PATH.with(|path| { 10 | if path.borrow().is_none() { 11 | *path.borrow_mut() = Some(Vec::new()); 12 | } 13 | }); 14 | } 15 | 16 | pub fn push(segment: Segment) { 17 | CURRENT_PATH.with_borrow_mut(|path| path.as_mut().map(|segments| segments.push(segment))); 18 | } 19 | 20 | pub fn pop() { 21 | CURRENT_PATH.with_borrow_mut(|path| { 22 | path.as_mut().map(|segments| { 23 | segments.pop(); 24 | }) 25 | }); 26 | } 27 | 28 | pub fn current_path() -> Option { 29 | let segments = CURRENT_PATH.with_borrow(|segments| segments.clone()); 30 | segments.map(|segments| segments.into()) 31 | } 32 | 33 | /// Stashes the current path for error handling. 34 | /// 35 | /// The replacement only takes place if the current path is not `None` and: 36 | /// 37 | /// - There is nothing stashed, or 38 | /// - The current path is not a strict subsequence of the currently stashed path 39 | pub fn stash_current_path_for_error() { 40 | PATH_ON_ERROR.with_borrow_mut(|stashed| { 41 | let current = Self::current_path(); 42 | let Some(stashed) = stashed else { 43 | *stashed = current; 44 | return; 45 | }; 46 | let Some(current) = current else { 47 | return; 48 | }; 49 | if !stashed.segments().starts_with(current.segments()) { 50 | *stashed = current; 51 | } 52 | }); 53 | } 54 | 55 | pub fn unstash_current_path_for_error() -> Option { 56 | PATH_ON_ERROR.take() 57 | } 58 | 59 | pub fn try_unset() { 60 | let _ = CURRENT_PATH.try_with(|s| { 61 | if let Ok(mut s) = s.try_borrow_mut() { 62 | *s = None; 63 | } 64 | }); 65 | } 66 | } 67 | 68 | thread_local! { 69 | /// The path to the value we're currently trying to deserialize. 70 | static CURRENT_PATH: RefCell>> = const { RefCell::new(None) }; 71 | 72 | /// A snapshot of the current path, captured when an error occurred. 73 | /// 74 | /// For types that implement [`EDeserialize`], this is not necessary 75 | /// since we manually capture the current path when reporting the error. 76 | /// 77 | /// That's not the case for types that only implement `serde`'s Serialize 78 | /// though. In those cases, we need to save the current path here 79 | /// in order to retrieve it later when we get back into `eserde`'s territory 80 | /// and try to report the error. 81 | static PATH_ON_ERROR: RefCell> = const { RefCell::new(None) }; 82 | } 83 | -------------------------------------------------------------------------------- /eserde/src/path/wrap.rs: -------------------------------------------------------------------------------- 1 | // Wrapper that attaches context to a `Visitor`, `SeqAccess` or `EnumAccess`. 2 | pub struct Wrap { 3 | pub(crate) delegate: X, 4 | } 5 | 6 | // Wrapper that attaches context to a `VariantAccess`. 7 | pub struct WrapVariant { 8 | pub(crate) delegate: X, 9 | pub(crate) pop_path_segment_before_exit: bool, 10 | } 11 | 12 | impl Wrap { 13 | pub(crate) fn new(delegate: X) -> Self { 14 | Wrap { delegate } 15 | } 16 | } 17 | 18 | impl WrapVariant { 19 | pub(crate) fn new(delegate: X, pop_path_segment_before_exit: bool) -> Self { 20 | WrapVariant { 21 | delegate, 22 | pop_path_segment_before_exit, 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /eserde/src/reporter.rs: -------------------------------------------------------------------------------- 1 | //! Tools for collecting and reporting errors that occurred during deserialization. 2 | //! 3 | //! The main entrypoint is the [`ErrorReporter`] type. 4 | //! 5 | //! # Audience 6 | //! 7 | //! This module should only be necessary if you're implementing `eserde` support for 8 | //! a new data format. 9 | //! As an application developer, you should never need to work with the types in this 10 | //! module directly. 11 | use std::{cell::RefCell, fmt::Display}; 12 | 13 | use crate::{path::PathTracker, DeserializationError}; 14 | 15 | /// The entrypoint for reporting errors that occurred during [`EDeserialize::deserialize_for_errors`](crate::EDeserialize::deserialize_for_errors). 16 | /// 17 | /// Check out [`ErrorReporter::start_deserialization`] for more information. 18 | pub struct ErrorReporter; 19 | 20 | impl ErrorReporter { 21 | #[must_use = "The guard returned by this method must be kept alive for the duration of the whole \ 22 | deserialization operation to ensure that errors are correctly reported."] 23 | /// Kick-off a deserialization operation. 24 | /// 25 | /// This method must be invoked before calling [`EDeserialize::deserialize_for_errors`](crate::EDeserialize::deserialize_for_errors), 26 | /// otherwise the deserializer will panic when trying to report errors. 27 | /// 28 | /// The returned guard must be kept alive for the duration of the whole deserialization operation. 29 | /// Once the guard is dropped, the deserialization operation is considered finished and the accumulated 30 | /// errors are cleared. 31 | /// 32 | /// In most cases, you don't need to call this method directly, as it's usually taken care of by the 33 | /// format-specific functions provided by `eserde`, such as [`eserde::json::from_str`](crate::json::from_str). 34 | pub fn start_deserialization() -> ErrorReporterGuard { 35 | PathTracker::init(); 36 | DESERIALIZATION_ERRORS.set(Some(Vec::new())); 37 | ErrorReporterGuard 38 | } 39 | 40 | /// Report an error that occurred during deserialization. 41 | /// 42 | /// # Panics 43 | /// 44 | /// This method will panic if called outside of a deserialization operation. 45 | /// Check out [`ErrorReporter::start_deserialization`] for more information. 46 | pub fn report(e: E) { 47 | let path = match PathTracker::unstash_current_path_for_error() { 48 | Some(p) => Some(p), 49 | None => PathTracker::current_path(), 50 | }; 51 | let error = DeserializationError { 52 | path, 53 | details: e.to_string(), 54 | }; 55 | let success = DESERIALIZATION_ERRORS.with_borrow_mut(|v| { 56 | if let Some(v) = v { 57 | v.push(error); 58 | true 59 | } else { 60 | false 61 | } 62 | }); 63 | if !success { 64 | // TODO: Should we panic directly inside the `with_borrow_mut` closure? 65 | // Or does that have some weird side-effects? 66 | panic!("Attempted to report an error outside of a deserialization operation. \ 67 | You can't call `ErrorReporter::report_error` without first calling `ErrorReporter::start_deserialization`. \ 68 | This error may be triggered by a top-level invocation of `EDeserialize::deserialize_for_errors` without \ 69 | a preceding call to `ErrorReporter::start_deserialization`. \ 70 | This initialization step is usually taken care of by the format-specific functions provided by `eserde`, \ 71 | such as `eserde::json::from_str`. If you're implementing your own deserialization logic, you \ 72 | need to take care of this initialization step yourself."); 73 | }; 74 | } 75 | 76 | /// Retrieve all errors that occurred during deserialization up to this point. 77 | /// 78 | /// The buffer is cleared after this call—i.e. subsequent calls to this method will return 79 | /// an empty vector until new errors are reported. 80 | /// 81 | /// # Panics 82 | /// 83 | /// This method will panic if called outside of a deserialization operation. 84 | /// Check out [`ErrorReporter::start_deserialization`] for more information. 85 | pub fn take_errors() -> Vec { 86 | DESERIALIZATION_ERRORS.with_borrow_mut(|v| v.replace(Vec::new())) 87 | .expect( 88 | "Attempted to collect deserialization errors outside of a deserialization operation. \ 89 | You can't call `ErrorReporter::take_errors` without first calling `ErrorReporter::start_deserialization`. \ 90 | This error may be triggered by a top-level invocation of `EDeserialize::deserialize_for_errors` without \ 91 | a preceding call to `ErrorReporter::start_deserialization`. \ 92 | This initialization step is usually taken care of by the format-specific functions provided by `eserde`, \ 93 | such as `eserde::json::from_str`. If you're implementing your own deserialization logic, you \ 94 | need to take care of this initialization step yourself.") 95 | } 96 | 97 | /// Retrieve the number of errors that occurred during deserialization up to this point. 98 | /// 99 | /// # Panics 100 | /// 101 | /// This method will panic if called outside of a deserialization operation. 102 | /// Check out [`ErrorReporter::start_deserialization`] for more information. 103 | pub fn n_errors() -> usize { 104 | DESERIALIZATION_ERRORS.with_borrow(|v| v.as_ref().map(|v| v.len())) 105 | .expect( 106 | "Attempted to count the number of deserialization errors outside of a deserialization operation. \ 107 | You can't call `ErrorReporter::take_errors` without first calling `ErrorReporter::start_deserialization`. \ 108 | This error may be triggered by a top-level invocation of `EDeserialize::deserialize_for_errors` without \ 109 | a preceding call to `ErrorReporter::start_deserialization`. \ 110 | This initialization step is usually taken care of by the format-specific functions provided by `eserde`, \ 111 | such as `eserde::json::from_str`. If you're implementing your own deserialization logic, you \ 112 | need to take care of this initialization step yourself.") 113 | } 114 | } 115 | 116 | #[non_exhaustive] 117 | /// Guard returned by [`ErrorReporter::start_deserialization`]. 118 | /// 119 | /// As long as this guard is alive, you're within the context of a single deserialization operation. 120 | /// All errors that occur during deserialization (either of a top-level value or of a nested value) 121 | /// will be accumulated in a single buffer, which is then retrieved at the end of the operation. 122 | /// 123 | /// Once this guard is dropped, the buffer is cleared and the deserialize operation is considered 124 | /// finished. 125 | pub struct ErrorReporterGuard; 126 | 127 | impl Drop for ErrorReporterGuard { 128 | fn drop(&mut self) { 129 | let _ = DESERIALIZATION_ERRORS.try_with(|c| { 130 | if let Ok(mut v) = c.try_borrow_mut() { 131 | *v = None; 132 | } 133 | }); 134 | PathTracker::try_unset(); 135 | } 136 | } 137 | 138 | thread_local! { 139 | /// Errors that occurred during deserialization. 140 | /// 141 | /// # Why a thread-local? 142 | /// 143 | /// We use a thread-local since we are constrained by the signature of `serde`'s `Deserialize` 144 | /// trait, so we can't pass down a `&mut Vec<_>` to accumulate errors. 145 | static DESERIALIZATION_ERRORS: RefCell>> = const { RefCell::new(None) }; 146 | } 147 | -------------------------------------------------------------------------------- /eserde/tests/compile_fail.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_all() { 3 | let t = trybuild::TestCases::new(); 4 | t.compile_fail("tests/compile_fail/*.rs"); 5 | } 6 | -------------------------------------------------------------------------------- /eserde/tests/compile_fail/default_unimplemented.rs: -------------------------------------------------------------------------------- 1 | #[derive(eserde::Deserialize)] 2 | struct Foo { 3 | #[serde(default)] 4 | field: NoDefault, 5 | } 6 | 7 | #[derive(eserde::Deserialize)] 8 | struct NoDefault; 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /eserde/tests/compile_fail/default_unimplemented.stderr: -------------------------------------------------------------------------------- 1 | error[E0277]: the trait bound `NoDefault: Default` is not satisfied 2 | --> tests/compile_fail/default_unimplemented.rs:3:5 3 | | 4 | 3 | #[serde(default)] 5 | | ^ the trait `Default` is not implemented for `NoDefault` 6 | | 7 | help: consider annotating `NoDefault` with `#[derive(Default)]` 8 | | 9 | 8 + #[derive(Default)] 10 | 9 | struct NoDefault; 11 | | 12 | 13 | error[E0277]: the trait bound `NoDefault: Default` is not satisfied 14 | --> tests/compile_fail/default_unimplemented.rs:1:10 15 | | 16 | 1 | #[derive(eserde::Deserialize)] 17 | | ^^^^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `NoDefault` 18 | | 19 | = note: this error originates in the derive macro `::eserde::_serde::Deserialize` (in Nightly builds, run with -Z macro-backtrace for more info) 20 | help: consider annotating `NoDefault` with `#[derive(Default)]` 21 | | 22 | 8 + #[derive(Default)] 23 | 9 | struct NoDefault; 24 | | 25 | -------------------------------------------------------------------------------- /eserde/tests/compile_fail/malformed_serde_deserialize_with.rs: -------------------------------------------------------------------------------- 1 | use std::num::TryFromIntError; 2 | 3 | #[derive(eserde::Deserialize)] 4 | struct Coord { 5 | #[serde(alias = "0", deserialize_with = u64_to_u8())] 6 | x: Result, 7 | #[serde(alias = "1", deserialize_with = false)] 8 | y: Result, 9 | } 10 | 11 | fn u64_to_u8<'de, D>(deserializer: D) -> Result, D::Error> 12 | where 13 | D: serde::Deserializer<'de>, 14 | { 15 | let long: u64 = serde::de::Deserialize::deserialize(deserializer)?; 16 | Ok(u8::try_from(long)) 17 | } 18 | 19 | fn main() {} 20 | -------------------------------------------------------------------------------- /eserde/tests/compile_fail/malformed_serde_deserialize_with.stderr: -------------------------------------------------------------------------------- 1 | error: expected serde deserialize_with attribute to be a string: `deserialize_with = "..."` 2 | --> tests/compile_fail/malformed_serde_deserialize_with.rs:5:45 3 | | 4 | 5 | #[serde(alias = "0", deserialize_with = u64_to_u8())] 5 | | ^^^^^^^^^^^ 6 | 7 | error: expected serde deserialize_with attribute to be a string: `deserialize_with = "..."` 8 | --> tests/compile_fail/malformed_serde_deserialize_with.rs:7:45 9 | | 10 | 7 | #[serde(alias = "1", deserialize_with = false)] 11 | | ^^^^^ 12 | 13 | error[E0277]: the trait bound `TryFromIntError: Deserialize<'_>` is not satisfied 14 | --> tests/compile_fail/malformed_serde_deserialize_with.rs:5:5 15 | | 16 | 5 | #[serde(alias = "0", deserialize_with = u64_to_u8())] 17 | | ^ the trait `Deserialize<'_>` is not implemented for `TryFromIntError` 18 | | 19 | = note: for local types consider adding `#[derive(serde::Deserialize)]` to your `TryFromIntError` type 20 | = note: for types from other crates check whether the crate offers a `serde` feature flag 21 | = help: the following other types implement trait `Deserialize<'de>`: 22 | &'a [u8] 23 | &'a std::path::Path 24 | &'a str 25 | () 26 | (T,) 27 | (T0, T1) 28 | (T0, T1, T2) 29 | (T0, T1, T2, T3) 30 | and $N others 31 | = note: required for `Result` to implement `Deserialize<'_>` 32 | = note: required for `Result` to implement `EDeserialize<'_>` 33 | note: required by a bound in `maybe_invalid_or_missing` 34 | --> src/_macro_impl.rs 35 | | 36 | | pub fn maybe_invalid_or_missing<'de, D, T>( 37 | | ------------------------ required by a bound in this function 38 | ... 39 | | T: EDeserialize<'de>, 40 | | ^^^^^^^^^^^^^^^^^ required by this bound in `maybe_invalid_or_missing` 41 | 42 | error[E0277]: the trait bound `TryFromIntError: Deserialize<'_>` is not satisfied 43 | --> tests/compile_fail/malformed_serde_deserialize_with.rs:7:5 44 | | 45 | 7 | #[serde(alias = "1", deserialize_with = false)] 46 | | ^ the trait `Deserialize<'_>` is not implemented for `TryFromIntError` 47 | | 48 | = note: for local types consider adding `#[derive(serde::Deserialize)]` to your `TryFromIntError` type 49 | = note: for types from other crates check whether the crate offers a `serde` feature flag 50 | = help: the following other types implement trait `Deserialize<'de>`: 51 | &'a [u8] 52 | &'a std::path::Path 53 | &'a str 54 | () 55 | (T,) 56 | (T0, T1) 57 | (T0, T1, T2) 58 | (T0, T1, T2, T3) 59 | and $N others 60 | = note: required for `Result` to implement `Deserialize<'_>` 61 | = note: required for `Result` to implement `EDeserialize<'_>` 62 | note: required by a bound in `maybe_invalid_or_missing` 63 | --> src/_macro_impl.rs 64 | | 65 | | pub fn maybe_invalid_or_missing<'de, D, T>( 66 | | ------------------------ required by a bound in this function 67 | ... 68 | | T: EDeserialize<'de>, 69 | | ^^^^^^^^^^^^^^^^^ required by this bound in `maybe_invalid_or_missing` 70 | 71 | error[E0599]: no function or associated item named `deserialize` found for struct `__ImplDeserializeForCoord` in the current scope 72 | --> tests/compile_fail/malformed_serde_deserialize_with.rs:3:10 73 | | 74 | 3 | #[derive(eserde::Deserialize)] 75 | | ^^^^^^^^^^^^^^^^^^^ function or associated item not found in `__ImplDeserializeForCoord` 76 | 4 | struct Coord { 77 | | ------------ function or associated item `deserialize` not found for this struct 78 | | 79 | = help: items from traits can only be used if the trait is implemented and in scope 80 | = note: the following traits define an item `deserialize`, perhaps you need to implement one of them: 81 | candidate #1: `Deserialize` 82 | candidate #2: `DeserializeSeed` 83 | = note: this error originates in the derive macro `eserde::Deserialize` (in Nightly builds, run with -Z macro-backtrace for more info) 84 | -------------------------------------------------------------------------------- /eserde/tests/compile_fail/meta_item_attribute_syntax_bad.rs: -------------------------------------------------------------------------------- 1 | #[derive(eserde::Deserialize)] 2 | struct Foo { 3 | #[serde(default - this parses but is not Meta Item Attribute Syntax, serde errors "expected `,`")] 4 | field: u32, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /eserde/tests/compile_fail/meta_item_attribute_syntax_bad.stderr: -------------------------------------------------------------------------------- 1 | error: expected `,` 2 | --> tests/compile_fail/meta_item_attribute_syntax_bad.rs:3:21 3 | | 4 | 3 | #[serde(default - this parses but is not Meta Item Attribute Syntax, serde errors "expected `,`")] 5 | | ^ 6 | 7 | error: duplicate serde attribute `default` 8 | --> tests/compile_fail/meta_item_attribute_syntax_bad.rs:3:5 9 | | 10 | 3 | #[serde(default - this parses but is not Meta Item Attribute Syntax, serde errors "expected `,`")] 11 | | ^ 12 | 13 | error[E0277]: the trait bound `__ImplEDeserializeForFoo: Deserialize<'_>` is not satisfied 14 | --> tests/compile_fail/meta_item_attribute_syntax_bad.rs:2:8 15 | | 16 | 2 | struct Foo { 17 | | ^^^ the trait `Deserialize<'_>` is not implemented for `__ImplEDeserializeForFoo` 18 | | 19 | = note: for local types consider adding `#[derive(serde::Deserialize)]` to your `__ImplEDeserializeForFoo` type 20 | = note: for types from other crates check whether the crate offers a `serde` feature flag 21 | = help: the following other types implement trait `Deserialize<'de>`: 22 | &'a [u8] 23 | &'a std::path::Path 24 | &'a str 25 | () 26 | (T,) 27 | (T0, T1) 28 | (T0, T1, T2) 29 | (T0, T1, T2, T3) 30 | and $N others 31 | 32 | error[E0599]: no function or associated item named `deserialize` found for struct `__ImplDeserializeForFoo` in the current scope 33 | --> tests/compile_fail/meta_item_attribute_syntax_bad.rs:1:10 34 | | 35 | 1 | #[derive(eserde::Deserialize)] 36 | | ^^^^^^^^^^^^^^^^^^^ function or associated item not found in `__ImplDeserializeForFoo` 37 | 2 | struct Foo { 38 | | ---------- function or associated item `deserialize` not found for this struct 39 | | 40 | = help: items from traits can only be used if the trait is implemented and in scope 41 | = note: the following traits define an item `deserialize`, perhaps you need to implement one of them: 42 | candidate #1: `Deserialize` 43 | candidate #2: `DeserializeSeed` 44 | = note: this error originates in the derive macro `eserde::Deserialize` (in Nightly builds, run with -Z macro-backtrace for more info) 45 | -------------------------------------------------------------------------------- /eserde/tests/compile_fail/union.rs: -------------------------------------------------------------------------------- 1 | #[derive(eserde::Deserialize)] 2 | union MyUnion { 3 | f1: u32, 4 | f2: f32, 5 | } 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /eserde/tests/compile_fail/union.stderr: -------------------------------------------------------------------------------- 1 | error: proc-macro derive panicked 2 | --> tests/compile_fail/union.rs:1:10 3 | | 4 | 1 | #[derive(eserde::Deserialize)] 5 | | ^^^^^^^^^^^^^^^^^^^ 6 | | 7 | = help: message: not implemented 8 | -------------------------------------------------------------------------------- /eserde/tests/default.rs: -------------------------------------------------------------------------------- 1 | #[derive(eserde::Deserialize, Debug, PartialEq, Eq)] 2 | #[serde(deny_unknown_fields)] 3 | struct Foo { 4 | #[serde(rename = "route", default = "default_route")] 5 | route_0: String, 6 | #[serde(default = "default_route")] 7 | route_1: String, 8 | #[serde(default = "NoDefault::new")] 9 | no_default: NoDefault, 10 | } 11 | 12 | fn default_route() -> String { 13 | "/".to_owned() 14 | } 15 | 16 | // Has no `#[derive(Default)]`. 17 | #[derive(eserde::Deserialize, Debug, PartialEq, Eq)] 18 | struct NoDefault; 19 | impl NoDefault { 20 | fn new() -> Self { 21 | NoDefault 22 | } 23 | } 24 | 25 | #[test] 26 | fn test_happy() { 27 | assert_eq!( 28 | Foo { 29 | route_0: "/".to_owned(), 30 | route_1: "/".to_owned(), 31 | no_default: NoDefault, 32 | }, 33 | eserde::json::from_str(r#"{}"#).unwrap() 34 | ); 35 | 36 | assert_eq!( 37 | Foo { 38 | route_0: "/dev/null".to_owned(), 39 | route_1: "/".to_owned(), 40 | no_default: NoDefault, 41 | }, 42 | eserde::json::from_str(r#"{"route": "/dev/null", "no_default": null}"#).unwrap() 43 | ); 44 | } 45 | 46 | #[test] 47 | fn test_fail() { 48 | let x = eserde::json::from_str::( 49 | r#"{"route": 0, "route_1": true, "no_default": "5", "route_2": 5.5}"#, 50 | ); 51 | assert!(x.is_err(), "Expected Err: {:?}", x); 52 | let errs = x.unwrap_err(); 53 | insta::assert_snapshot!(errs, @r###" 54 | Something went wrong during deserialization: 55 | - route: invalid type: integer `0`, expected a string at line 1 column 11 56 | - route_1: invalid type: boolean `true`, expected a string at line 1 column 28 57 | - no_default: invalid type: string "5", expected unit struct __ImplEDeserializeForNoDefault at line 1 column 47 58 | - route_2: unknown field `route_2`, expected one of `route`, `route_1`, `no_default` at line 1 column 58 59 | "###); 60 | } 61 | -------------------------------------------------------------------------------- /eserde/tests/deserialize_with.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | #[derive(eserde::Deserialize, serde::Serialize, Debug, PartialEq, Eq)] 4 | #[serde(deny_unknown_fields)] 5 | struct DeserializeWith { 6 | #[serde(rename = "number", deserialize_with = "deserialize_u8")] 7 | num: Result, 8 | 9 | #[serde(with = "serde_to_from_str")] 10 | ip: Result, 11 | } 12 | 13 | fn deserialize_u8<'de, D>(deserializer: D) -> Result, D::Error> 14 | where 15 | D: serde::Deserializer<'de>, 16 | { 17 | let long: u64 = serde::de::Deserialize::deserialize(deserializer)?; 18 | Ok(u8::try_from(long).map_err(|_| long)) 19 | } 20 | 21 | mod serde_to_from_str { 22 | pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> 23 | where 24 | T: std::str::FromStr, 25 | D: serde::Deserializer<'de>, 26 | { 27 | let s: String = serde::de::Deserialize::deserialize(deserializer)?; 28 | Ok(s.parse().map_err(|_| s)) 29 | } 30 | 31 | pub fn serialize(value: &Result, serializer: S) -> Result 32 | where 33 | T: std::string::ToString, 34 | S: serde::Serializer, 35 | { 36 | match value { 37 | Ok(v) => serializer.serialize_str(&v.to_string()), 38 | Err(e) => serializer.serialize_str(e), 39 | } 40 | } 41 | } 42 | 43 | #[test] 44 | fn test_happy() { 45 | assert_eq!( 46 | DeserializeWith { 47 | num: Ok(255), 48 | ip: Ok(IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1))), 49 | }, 50 | eserde::json::from_str::(r#"{"number": 255, "ip": "127.0.0.1"}"#).unwrap(), 51 | ); 52 | assert_eq!( 53 | DeserializeWith { 54 | num: Err(256), 55 | ip: Err("localhost".to_owned()), 56 | }, 57 | eserde::json::from_str::(r#"{"number": 256, "ip": "localhost"}"#).unwrap(), 58 | ); 59 | } 60 | 61 | #[test] 62 | fn test_fail() { 63 | let x = eserde::json::from_str::( 64 | r#"{"number": "foo", "ip": 100.50, "foo": "bar"}"#, 65 | ); 66 | assert!(x.is_err(), "Expected Err: {:?}", x); 67 | let errs = x.unwrap_err(); 68 | insta::assert_snapshot!(errs, @r###" 69 | Something went wrong during deserialization: 70 | - number: invalid type: string "foo", expected u64 at line 1 column 16 71 | - ip: invalid type: floating point `100.5`, expected a string at line 1 column 30 72 | - foo: unknown field `foo`, expected `number` or `ip` at line 1 column 37 73 | "###); 74 | } 75 | -------------------------------------------------------------------------------- /eserde/tests/happy/deserialize.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use itertools::Itertools; 4 | use std::borrow::Cow; 5 | 6 | #[derive(eserde::Deserialize)] 7 | struct NamedStruct { 8 | #[serde(default)] 9 | a: Option, 10 | b: TupleStructOneField, 11 | c: Vec, 12 | } 13 | 14 | #[derive(eserde::Deserialize)] 15 | struct GenericStruct { 16 | // #[eserde(compat)] 17 | a: T, 18 | #[eserde(compat)] 19 | b: S, 20 | } 21 | 22 | #[derive(eserde::Deserialize)] 23 | struct LifetimeGenericStruct<'a, 'b, 'c, 'd, 'e: 'a> { 24 | #[serde(borrow)] 25 | a: Cow<'a, str>, 26 | // `&str` and `&[u8]` are special-cased by `serde` 27 | // and treated as if `#[serde(borrow)]` was applied. 28 | b: &'b str, 29 | c: &'c [u8], 30 | d: Cow<'d, str>, 31 | // Check that we don't add `borrow` twice, angering `serde` 32 | #[serde(borrow)] 33 | e: &'e str, 34 | } 35 | 36 | #[derive(eserde::Deserialize)] 37 | struct TupleStructOneField(#[serde(default)] Option); 38 | 39 | #[derive(eserde::Deserialize)] 40 | struct TupleStructMultipleFields(Option, u32, #[serde(default)] u64); 41 | 42 | #[derive(eserde::Deserialize)] 43 | enum CLikeEnumOneVariant { 44 | A, 45 | } 46 | 47 | #[derive(eserde::Deserialize)] 48 | enum CLikeEnumMultipleVariants { 49 | A, 50 | B, 51 | } 52 | 53 | #[derive(eserde::Deserialize)] 54 | enum EnumWithBothNamedAndTupleVariants { 55 | Named { a: u32 }, 56 | NamedMultiple { a: u32, b: u64 }, 57 | Tuple(u32), 58 | TupleMultiple(u32, u64), 59 | Unit, 60 | } 61 | 62 | // #[test] 63 | // fn deserialize() { 64 | // let payloads = [ 65 | // r#"{ 66 | // "b": 5, 67 | // "c": [[1, 2, 3], [4, 5, 6]] 68 | // }"#, 69 | // r#"{ 70 | // "a": 5, 71 | // "b": null, 72 | // "c": [[null, 2, 3], [4, 5, 6]] 73 | // }"#, 74 | // ]; 75 | // for payload in payloads { 76 | // assert!( 77 | // serde_json::from_str::(payload).is_ok(), 78 | // "Failed to deserialize: {}", 79 | // payload 80 | // ); 81 | // } 82 | // } 83 | 84 | #[test] 85 | fn deser_for_errors() { 86 | #[derive(Debug, eserde::Deserialize)] 87 | struct TopLevelStruct { 88 | a: LeafStruct, 89 | b: u64, 90 | c: String, 91 | #[eserde(compat)] 92 | d: IncompatibleLeafStruct, 93 | } 94 | 95 | #[derive(Debug, eserde::Deserialize)] 96 | struct LeafStruct { 97 | #[serde(default)] 98 | a2: Option, 99 | } 100 | 101 | #[derive(Debug, serde::Deserialize)] 102 | struct IncompatibleLeafStruct { 103 | #[serde(default)] 104 | a2: Option, 105 | } 106 | 107 | let payload = r#"{ 108 | "a": { "a2": -5 }, 109 | "c": 8 110 | }"#; 111 | 112 | let value = eserde::json::from_str::(payload); 113 | let error = value.unwrap_err(); 114 | let error_repr = error.into_iter().map(|e| e.to_string()).join("\n"); 115 | insta::assert_snapshot!(error_repr, @r###" 116 | a.a2: invalid value: integer `-5`, expected u32 at line 2 column 19 117 | c: invalid type: integer `8`, expected a string at line 3 column 10 118 | missing field `b` 119 | missing field `d` 120 | "###); 121 | 122 | let value = serde_json::from_str::(payload); 123 | let error_repr = value.unwrap_err().to_string(); 124 | insta::assert_snapshot!(error_repr, @"invalid value: integer `-5`, expected u32 at line 2 column 19"); 125 | } 126 | -------------------------------------------------------------------------------- /eserde/tests/happy/main.rs: -------------------------------------------------------------------------------- 1 | mod deserialize; 2 | mod path; 3 | -------------------------------------------------------------------------------- /eserde/tests/happy/path.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unreadable_literal, dead_code)] 2 | use eserde::{Deserialize, EDeserialize}; 3 | use std::collections::BTreeMap as Map; 4 | use std::fmt::Debug; 5 | 6 | #[track_caller] 7 | fn test<'de, T>(json: &'de str, expected: &str) 8 | where 9 | T: EDeserialize<'de> + Debug, 10 | { 11 | let result: Result = eserde::json::from_str(json); 12 | let errors = result.unwrap_err(); 13 | assert_eq!(errors.len(), 1, "{}", errors); 14 | let error = errors.iter().next().unwrap(); 15 | let path = error.path().expect("No path on error"); 16 | assert_eq!(path.to_string(), expected, "The full error:\n\t{}", error); 17 | } 18 | 19 | #[track_caller] 20 | fn test_many<'de, T>(json: &'de str, expected: &[&str]) 21 | where 22 | T: EDeserialize<'de> + Debug, 23 | { 24 | let result: Result = eserde::json::from_str(json); 25 | let errors = result.unwrap_err(); 26 | assert_eq!( 27 | errors.len(), 28 | expected.len(), 29 | "The number of errors does not match the expected number. Reported errors:\n- {}", 30 | errors 31 | .iter() 32 | .map(|e| e.to_string()) 33 | .collect::>() 34 | .join("\n- ") 35 | ); 36 | for (error, expected) in errors.into_iter().zip(expected.into_iter()) { 37 | let path = error.path().expect("No path on error"); 38 | assert_eq!(&path.to_string(), expected, "The full error:\n\t{}", error); 39 | } 40 | } 41 | 42 | #[test] 43 | fn test_struct() { 44 | #[derive(Deserialize, Debug)] 45 | struct Package { 46 | name: String, 47 | dependencies: Map, 48 | } 49 | 50 | #[derive(Deserialize, Debug)] 51 | struct Dependency { 52 | version: String, 53 | } 54 | 55 | let j = r#"{ 56 | "name": "demo", 57 | "dependencies": { 58 | "serde": { 59 | "version": 1 60 | } 61 | } 62 | }"#; 63 | 64 | test::(j, "dependencies.serde.version"); 65 | } 66 | 67 | #[test] 68 | fn test_vec() { 69 | #[derive(Deserialize, Debug)] 70 | struct Package { 71 | dependencies: Vec, 72 | } 73 | 74 | #[derive(Deserialize, Debug)] 75 | struct Dependency { 76 | name: String, 77 | version: String, 78 | } 79 | 80 | let j = r#"{ 81 | "dependencies": [ 82 | { 83 | "name": "serde", 84 | "version": "1.0" 85 | }, 86 | { 87 | "name": "serde_json", 88 | "version": 1 89 | } 90 | ] 91 | }"#; 92 | 93 | test::(j, "dependencies[1].version"); 94 | } 95 | 96 | #[test] 97 | fn test_option() { 98 | #[derive(Deserialize, Debug)] 99 | struct Package { 100 | dependency: Option, 101 | } 102 | 103 | #[derive(Deserialize, Debug)] 104 | struct Dependency { 105 | version: String, 106 | } 107 | 108 | let j = r#"{ 109 | "dependency": { 110 | "version": 1 111 | } 112 | }"#; 113 | 114 | test::(j, "dependency.version"); 115 | } 116 | 117 | #[test] 118 | fn test_struct_variant() { 119 | #[derive(Deserialize, Debug)] 120 | struct Package { 121 | dependency: Dependency, 122 | } 123 | 124 | #[derive(Deserialize, Debug)] 125 | enum Dependency { 126 | Struct { version: String }, 127 | } 128 | 129 | let j = r#"{ 130 | "dependency": { 131 | "Struct": { 132 | "version": 1 133 | } 134 | } 135 | }"#; 136 | 137 | test::(j, "dependency.Struct.version"); 138 | } 139 | 140 | #[test] 141 | fn test_tuple_variant() { 142 | #[derive(Deserialize, Debug)] 143 | struct Package { 144 | dependency: Dependency, 145 | } 146 | 147 | #[derive(Deserialize, Debug)] 148 | enum Dependency { 149 | Tuple(String, String), 150 | } 151 | 152 | let j = r#"{ 153 | "dependency": { 154 | "Tuple": ["serde", 1] 155 | } 156 | }"#; 157 | 158 | test::(j, "dependency.Tuple[1]"); 159 | } 160 | 161 | #[test] 162 | fn test_multiple_tuple_variant() { 163 | #[derive(Deserialize, Debug)] 164 | struct Package { 165 | dependency: Dependency, 166 | another_dependency: Dependency, 167 | } 168 | 169 | #[derive(Deserialize, Debug)] 170 | enum Dependency { 171 | Tuple(String, String), 172 | Single(String), 173 | } 174 | 175 | let j = r#"{ 176 | "dependency": { 177 | "Tuple": ["serde", 1] 178 | }, 179 | "another_dependency": { 180 | "Single": 2 181 | } 182 | }"#; 183 | 184 | test_many::(j, &["dependency.Tuple[1]", "another_dependency.Single"]); 185 | } 186 | 187 | #[test] 188 | fn test_unknown_field() { 189 | #[derive(Deserialize, Debug)] 190 | struct Package { 191 | dependency: Dependency, 192 | } 193 | 194 | #[derive(Deserialize, Debug)] 195 | #[serde(deny_unknown_fields)] 196 | struct Dependency { 197 | version: String, 198 | } 199 | 200 | let j = r#"{ 201 | "dependency": { 202 | "version": "1.0", 203 | "name": "serde", 204 | } 205 | }"#; 206 | 207 | test_many::(j, &["dependency.name", "dependency"]); 208 | } 209 | 210 | #[test] 211 | fn test_invalid_length() { 212 | #[derive(Deserialize, Debug)] 213 | struct Package { 214 | dependency: Dependency, 215 | } 216 | 217 | #[derive(Deserialize, Debug)] 218 | struct Dependency(String, String); 219 | 220 | let j = r#"{ 221 | "dependency": ["serde"] 222 | }"#; 223 | 224 | test::(j, "dependency"); 225 | } 226 | 227 | #[test] 228 | fn test_syntax_error() { 229 | #[derive(Deserialize, Debug)] 230 | struct Package { 231 | dependency: Dependency, 232 | } 233 | 234 | #[derive(Deserialize, Debug)] 235 | struct Dependency { 236 | version: String, 237 | } 238 | 239 | let j = r#"{ 240 | "dependency": { 241 | "error": * 242 | }"#; 243 | 244 | test_many::(j, &["dependency.error", "."]); 245 | } 246 | 247 | #[test] 248 | fn test_u128() { 249 | #[derive(Deserialize, Debug)] 250 | struct Container { 251 | n: u128, 252 | } 253 | 254 | let j = r#"{ 255 | "n": 130033514578017493995102500318550798591 256 | }"#; 257 | 258 | let de = &mut serde_json::Deserializer::from_str(j); 259 | let container: Container = serde_path_to_error::deserialize(de).expect("failed to deserialize"); 260 | 261 | assert_eq!(container.n, 130033514578017493995102500318550798591u128); 262 | } 263 | 264 | #[test] 265 | fn test_map_nonstring_key() { 266 | #[derive(Deserialize, Debug)] 267 | struct Dependency { 268 | version: String, 269 | } 270 | 271 | let j = r#"{ 272 | "100": { 273 | "version": false 274 | } 275 | }"#; 276 | 277 | test::>(j, "100.version"); 278 | } 279 | 280 | #[ignore = "Reports the top-level path, '.', rather than the content path. Fails for `serde_path_to_error` too."] 281 | // Investigation: why does this happen? 282 | // It is due to the way internally tagged enum are deserialized. 283 | // We first look for the tag field and **stash** the content inside a `serde::__private::de::Content`. 284 | // If the tag is found, we then try to deserialize the content using the appropriate variant. 285 | // At that point though, the current path no longer reflects where we are, thus leading to this issue. 286 | // A possible fix would be to use a custom `Content` variant that includes the original path. 287 | // Unfortunately, that would require changing the code generated by `serde::Deserialize`, and there's 288 | // no easy way to accomplish it as far as I'm aware beyond forking the derive macro. 289 | #[test] 290 | fn test_internally_tagged_enum() { 291 | #[derive(Debug, Deserialize)] 292 | #[serde(tag = "type")] 293 | pub enum TestEnum { 294 | B { value: u32 }, 295 | } 296 | 297 | let j = r#" 298 | { 299 | "type": "B", 300 | "value": "500" 301 | }"#; 302 | 303 | test::(j, "value"); 304 | } 305 | 306 | #[test] 307 | fn test_adjacent_tagged_enum() { 308 | #[derive(Debug, Deserialize)] 309 | #[serde(tag = "type", content = "content")] 310 | pub enum TestEnum { 311 | A(u32), 312 | B(u64), 313 | } 314 | 315 | let j = r#" 316 | { 317 | "type": "A", 318 | "content": "500" 319 | }"#; 320 | 321 | test::(j, "content"); 322 | } 323 | -------------------------------------------------------------------------------- /eserde/tests/integration/contract.rs: -------------------------------------------------------------------------------- 1 | use eserde_test_helper::assert_from_json_inline; 2 | use eserde_test_helper::contract::*; 3 | use eserde_test_helper::test_helper::TestHelper; 4 | 5 | #[test] 6 | fn struct_deny_unknown_fields() { 7 | let test = TestHelper::::new_serialized( 8 | r#"{"DEFAULT":true,"SKIP-SERIALIZING-IF":true,"renamed":false,"OPTION":false}"#, 9 | ); 10 | assert_from_json_inline!(test, @r#" 11 | Err( 12 | DeserializationErrors( 13 | [ 14 | DeserializationError { 15 | path: Some( 16 | Path { 17 | segments: [], 18 | }, 19 | ), 20 | details: "missing field `write_only`", 21 | }, 22 | ], 23 | ), 24 | ) 25 | "#); 26 | } 27 | 28 | #[test] 29 | fn struct_allow_unknown_fields() { 30 | let test = TestHelper::::new_serialized( 31 | r#"{"DEFAULT":false,"renamed":false,"OPTION":null}"#, 32 | ); 33 | assert_from_json_inline!(test, @r#" 34 | Err( 35 | DeserializationErrors( 36 | [ 37 | DeserializationError { 38 | path: Some( 39 | Path { 40 | segments: [], 41 | }, 42 | ), 43 | details: "missing field `write_only`", 44 | }, 45 | DeserializationError { 46 | path: Some( 47 | Path { 48 | segments: [], 49 | }, 50 | ), 51 | details: "missing field `skip_serializing_if`", 52 | }, 53 | ], 54 | ), 55 | ) 56 | "#); 57 | } 58 | 59 | #[test] 60 | fn tuple_struct() { 61 | let test = 62 | TestHelper::::new_serialized(r#"["XVUgTUKTJ7J8r","yZwcp1Ge","nf9hN3"]"#); 63 | assert_from_json_inline!(test, @r#" 64 | Ok( 65 | TupleStruct( 66 | "XVUgTUKTJ7J8r", 67 | false, 68 | "yZwcp1Ge", 69 | "nf9hN3", 70 | ), 71 | ) 72 | "#); 73 | } 74 | 75 | #[test] 76 | fn externally_tagged_enum() { 77 | let test = TestHelper::::new_serialized(r#""renamed_unit""#); 78 | assert_from_json_inline!(test, @r" 79 | Ok( 80 | RenamedUnit, 81 | ) 82 | "); 83 | } 84 | 85 | #[test] 86 | fn internally_tagged_enum() { 87 | let test = TestHelper::::new_serialized(r#"{"tag":"renamed_unit"}"#); 88 | assert_from_json_inline!(test, @r" 89 | Ok( 90 | RenamedUnit, 91 | ) 92 | "); 93 | } 94 | 95 | #[test] 96 | fn adjacently_tagged_enum() { 97 | let test = TestHelper::::new_serialized(r#"{"tag":"renamed_unit"}"#); 98 | assert_from_json_inline!(test, @r" 99 | Ok( 100 | RenamedUnit, 101 | ) 102 | "); 103 | } 104 | -------------------------------------------------------------------------------- /eserde/tests/integration/enum_repr.rs: -------------------------------------------------------------------------------- 1 | use eserde_test_helper::assert_from_json_inline; 2 | use eserde_test_helper::enum_repr::*; 3 | use eserde_test_helper::test_helper::TestHelper; 4 | 5 | #[test] 6 | fn enum_repr() { 7 | let test = TestHelper::::new_serialized(r#"5"#); 8 | assert_from_json_inline!(test, @r" 9 | Ok( 10 | Enum( 11 | Five, 12 | ), 13 | ) 14 | "); 15 | } 16 | -------------------------------------------------------------------------------- /eserde/tests/integration/enums.rs: -------------------------------------------------------------------------------- 1 | use eserde_test_helper::assert_from_json_inline; 2 | use eserde_test_helper::enums::*; 3 | use eserde_test_helper::test_helper::TestHelper; 4 | 5 | #[test] 6 | fn externally_tagged_enum() { 7 | let test = TestHelper::::new_serialized(r#"{"unitStructNewType":null}"#); 8 | assert_from_json_inline!(test, @r" 9 | Ok( 10 | UnitStructNewType( 11 | UnitStruct, 12 | ), 13 | ) 14 | "); 15 | } 16 | 17 | #[test] 18 | fn internally_tagged_enum() { 19 | let test = TestHelper::::new_serialized(r#"{"tag":"UnitOne"}"#); 20 | assert_from_json_inline!(test, @r" 21 | Ok( 22 | UnitOne, 23 | ) 24 | "); 25 | } 26 | 27 | #[test] 28 | fn adjacently_tagged_enum() { 29 | let test = 30 | TestHelper::::new_serialized(r#"{"tag":"Tuple","content":[-427070648,true]}"#); 31 | assert_from_json_inline!(test, @r" 32 | Ok( 33 | Tuple( 34 | -427070648, 35 | true, 36 | ), 37 | ) 38 | "); 39 | } 40 | 41 | #[test] 42 | fn untagged_enum() { 43 | let test = TestHelper::::new_serialized(r#"[-521833035,true]"#); 44 | assert_from_json_inline!(test, @r" 45 | Ok( 46 | UntaggedWrapper( 47 | StructNewType( 48 | Struct { 49 | foo: -521833035, 50 | bar: true, 51 | }, 52 | ), 53 | ), 54 | ) 55 | "); 56 | } 57 | 58 | #[test] 59 | fn renamed() { 60 | let test = 61 | TestHelper::::new_serialized(r#"{"struct_variant":{"FIELD":"8AtP50nUcNy1f"}}"#); 62 | assert_from_json_inline!(test, @r#" 63 | Ok( 64 | StructVariant { 65 | field: "8AtP50nUcNy1f", 66 | }, 67 | ) 68 | "#); 69 | } 70 | -------------------------------------------------------------------------------- /eserde/tests/integration/enums_deny_unknown_fields.rs: -------------------------------------------------------------------------------- 1 | use eserde_test_helper::assert_from_json_inline; 2 | use eserde_test_helper::enums_deny_unknown_fields::*; 3 | use eserde_test_helper::test_helper::TestHelper; 4 | 5 | #[test] 6 | fn externally_tagged_enum() { 7 | let test = 8 | TestHelper::::new_serialized(r#"{"Struct":{"foo":1899123746,"bar":true}}"#); 9 | assert_from_json_inline!(test, @r" 10 | Ok( 11 | Struct { 12 | foo: 1899123746, 13 | bar: true, 14 | }, 15 | ) 16 | "); 17 | } 18 | 19 | #[test] 20 | fn internally_tagged_enum() { 21 | let test = 22 | TestHelper::::new_serialized(r#"{"tag":"Struct","foo":-1133362929,"bar":true}"#); 23 | assert_from_json_inline!(test, @r" 24 | Ok( 25 | Struct { 26 | foo: -1133362929, 27 | bar: true, 28 | }, 29 | ) 30 | "); 31 | } 32 | 33 | #[test] 34 | fn adjacently_tagged_enum() { 35 | let test = TestHelper::::new_serialized( 36 | r#"{"tag":"Struct","content":{"foo":1566241236,"bar":false}}"#, 37 | ); 38 | assert_from_json_inline!(test, @r" 39 | Ok( 40 | Struct { 41 | foo: 1566241236, 42 | bar: false, 43 | }, 44 | ) 45 | "); 46 | } 47 | -------------------------------------------------------------------------------- /eserde/tests/integration/enums_flattened.rs: -------------------------------------------------------------------------------- 1 | use eserde_test_helper::assert_from_json_inline; 2 | use eserde_test_helper::enums_flattened::*; 3 | use eserde_test_helper::test_helper::TestHelper; 4 | 5 | #[test] 6 | fn enums_flattened() { 7 | let test = TestHelper::::new_serialized( 8 | r#"{"f":0.6193613,"S":"iq4m5jByT","U":2413626873,"S2":"5B6FXFBEm","U2":2852593204,"S3":"HzE1mKd6Gv6L6DX"}"#, 9 | ); 10 | assert_from_json_inline!(test, @r#" 11 | Ok( 12 | Container { 13 | f: 0.6193613, 14 | e1: S( 15 | "iq4m5jByT", 16 | ), 17 | e2: U( 18 | 2413626873, 19 | ), 20 | e3: S2( 21 | "5B6FXFBEm", 22 | ), 23 | e4: U2( 24 | 2852593204, 25 | ), 26 | e5: S3( 27 | "HzE1mKd6Gv6L6DX", 28 | ), 29 | }, 30 | ) 31 | "#); 32 | } 33 | 34 | #[test] 35 | fn enums_flattened_deny_unknown_fields() { 36 | let test = TestHelper::::new_serialized( 37 | r#"{"f":0.6193613,"S":"iq4m5jByT","U":2413626873,"S2":"5B6FXFBEm","U2":2852593204,"S3":"HzE1mKd6Gv6L6DX"}"#, 38 | ); 39 | assert_from_json_inline!(test, @r#" 40 | Ok( 41 | ContainerDenyUnknownFields { 42 | f: 0.6193613, 43 | e1: S( 44 | "iq4m5jByT", 45 | ), 46 | e2: U( 47 | 2413626873, 48 | ), 49 | e3: S2( 50 | "5B6FXFBEm", 51 | ), 52 | e4: U2( 53 | 2852593204, 54 | ), 55 | e5: S3( 56 | "HzE1mKd6Gv6L6DX", 57 | ), 58 | }, 59 | ) 60 | "#); 61 | } 62 | -------------------------------------------------------------------------------- /eserde/tests/integration/flatten.rs: -------------------------------------------------------------------------------- 1 | use eserde_test_helper::assert_from_json_inline; 2 | use eserde_test_helper::flatten::*; 3 | use eserde_test_helper::test_helper::TestHelper; 4 | 5 | #[test] 6 | fn flattened_struct() { 7 | let test = TestHelper::::new_serialized( 8 | r#"{"f":0.10925001,"b":false,"s":"oJ654XjRD","v":[293213948,-698189839,-1145449804,100811548,2092713542,-162305236,1550235965]}"#, 9 | ); 10 | assert_from_json_inline!(test, @r#" 11 | Ok( 12 | Deep1 { 13 | f: 0.10925001, 14 | deep2: Deep2 { 15 | b: false, 16 | deep3: Some( 17 | Deep3 { 18 | s: "oJ654XjRD", 19 | }, 20 | ), 21 | }, 22 | v: [ 23 | 293213948, 24 | -698189839, 25 | -1145449804, 26 | 100811548, 27 | 2092713542, 28 | -162305236, 29 | 1550235965, 30 | ], 31 | }, 32 | ) 33 | "#); 34 | } 35 | 36 | #[test] 37 | fn flattened_value() { 38 | let test = TestHelper::::new_serialized( 39 | r#"{"flag":true,"rh6rGx64R5z8bXM53JB":null,"tKCHK":0.45199126013011237,"zRNzOrvBGBkR":-942429839.0,"zvPAta":-1473060449.0}"#, 40 | ); 41 | assert_from_json_inline!(test, @r#" 42 | Ok( 43 | FlattenValue { 44 | flag: true, 45 | value: JsonValue( 46 | Object { 47 | "rh6rGx64R5z8bXM53JB": Null, 48 | "tKCHK": Number(0.4519912601301124), 49 | "zRNzOrvBGBkR": Number(-942429839.0), 50 | "zvPAta": Number(-1473060449.0), 51 | }, 52 | ), 53 | }, 54 | ) 55 | "#); 56 | } 57 | 58 | #[test] 59 | fn flattened_map() { 60 | let test = TestHelper::::new_serialized( 61 | r#"{"flag":false,"0Zdvgwzntg1":null,"2xmy0O":-201987135.0,"815o0wQYoQXKwQ4sk35":{"c5OSxU6Sg1RkLzA3LI":null,"cGlIGCxZqgKd":true,"m0BahDL9nqdE":null},"CWli7oV":false,"RPpRcK3uDfcPMcMMSl":-1046786077.0,"TtDt8QISh":null,"UeOlw3LZgVq01vyUfeJ":"SYS4VXt0E9Baiqqx0","VnVAc8m2IzWxHqUyE":"yIyXTZG"}"#, 62 | ); 63 | assert_from_json_inline!(test, @r#" 64 | Ok( 65 | FlattenMap { 66 | flag: false, 67 | value: { 68 | "0Zdvgwzntg1": JsonValue( 69 | Null, 70 | ), 71 | "2xmy0O": JsonValue( 72 | Number(-201987135.0), 73 | ), 74 | "815o0wQYoQXKwQ4sk35": JsonValue( 75 | Object { 76 | "c5OSxU6Sg1RkLzA3LI": Null, 77 | "cGlIGCxZqgKd": Bool(true), 78 | "m0BahDL9nqdE": Null, 79 | }, 80 | ), 81 | "CWli7oV": JsonValue( 82 | Bool(false), 83 | ), 84 | "RPpRcK3uDfcPMcMMSl": JsonValue( 85 | Number(-1046786077.0), 86 | ), 87 | "TtDt8QISh": JsonValue( 88 | Null, 89 | ), 90 | "UeOlw3LZgVq01vyUfeJ": JsonValue( 91 | String("SYS4VXt0E9Baiqqx0"), 92 | ), 93 | "VnVAc8m2IzWxHqUyE": JsonValue( 94 | String("yIyXTZG"), 95 | ), 96 | }, 97 | }, 98 | ) 99 | "#); 100 | } 101 | -------------------------------------------------------------------------------- /eserde/tests/integration/main.rs: -------------------------------------------------------------------------------- 1 | mod contract; 2 | mod enum_repr; 3 | mod enums; 4 | mod enums_deny_unknown_fields; 5 | mod enums_flattened; 6 | mod flatten; 7 | mod structs; 8 | -------------------------------------------------------------------------------- /eserde/tests/integration/structs.rs: -------------------------------------------------------------------------------- 1 | use eserde_test_helper::assert_from_json_inline; 2 | use eserde_test_helper::structs::*; 3 | use eserde_test_helper::test_helper::TestHelper; 4 | 5 | #[test] 6 | fn unit() { 7 | let test = TestHelper::::new_serialized(r#"null"#); 8 | assert_from_json_inline!(test, @r" 9 | Ok( 10 | UnitStruct, 11 | ) 12 | "); 13 | } 14 | 15 | #[test] 16 | fn normal() { 17 | let test = TestHelper::::new_serialized(r#"{"foo":"fvsTNa45C","bar":false}"#); 18 | assert_from_json_inline!(test, @r#" 19 | Ok( 20 | NormalStruct { 21 | foo: "fvsTNa45C", 22 | bar: false, 23 | }, 24 | ) 25 | "#); 26 | } 27 | 28 | #[test] 29 | fn newtype() { 30 | let test = TestHelper::::new_serialized(r#""F71VZOS""#); 31 | assert_from_json_inline!(test, @r#" 32 | Ok( 33 | NewType( 34 | "F71VZOS", 35 | ), 36 | ) 37 | "#); 38 | } 39 | 40 | #[test] 41 | fn tuple() { 42 | let test = TestHelper::::new_serialized(r#"["FPoREowVSC0CjkC",false]"#); 43 | assert_from_json_inline!(test, @r#" 44 | Ok( 45 | TupleStruct( 46 | "FPoREowVSC0CjkC", 47 | false, 48 | ), 49 | ) 50 | "#); 51 | } 52 | 53 | #[test] 54 | fn renamed_fields() { 55 | let test = TestHelper::::new_serialized( 56 | r#"{"camelCase":-1608793701,"new_name":-663097910}"#, 57 | ); 58 | assert_from_json_inline!(test, @r" 59 | Ok( 60 | RenamedFields { 61 | camel_case: -1608793701, 62 | old_name: -663097910, 63 | }, 64 | ) 65 | "); 66 | } 67 | 68 | #[test] 69 | fn deny_unknown_fields() { 70 | let test = TestHelper::::new_serialized( 71 | r#"{"foo":"3nrBBXVgrpwpQ9tDK8","bar":false}"#, 72 | ); 73 | assert_from_json_inline!(test, @r#" 74 | Ok( 75 | DenyUnknownFields { 76 | foo: "3nrBBXVgrpwpQ9tDK8", 77 | bar: false, 78 | }, 79 | ) 80 | "#); 81 | } 82 | -------------------------------------------------------------------------------- /eserde/tests/json_types.rs: -------------------------------------------------------------------------------- 1 | //! Test `serde_json::value` types. 2 | #![allow(dead_code)] 3 | 4 | use std::collections::HashMap; 5 | 6 | #[test] 7 | fn test() { 8 | use serde_json::value::{Map, Number, Value}; 9 | 10 | const PAYLOAD: &str = r#"{ 11 | "a": -0.0, 12 | "b": true, 13 | "c": 8, 14 | "d": { "a2": false }, 15 | "e": "foo" 16 | }"#; 17 | 18 | assert!(serde_json::from_str::(PAYLOAD).is_ok()); 19 | assert!(serde_json::from_str::>(PAYLOAD).is_ok()); 20 | 21 | let result = eserde::json::from_str::>(PAYLOAD); 22 | let errors = result.unwrap_err(); 23 | // Note the errors degrade after the `"d": {` due to the curly brace. 24 | insta::assert_snapshot!(errors, @r###" 25 | Something went wrong during deserialization: 26 | - b: invalid type: boolean `true`, expected a JSON number at line 3 column 17 27 | - d: invalid type: map, expected a JSON number at line 5 column 15 28 | - expected `,` or `}` at line 5 column 16 29 | "### 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /eserde/tests/std_types.rs: -------------------------------------------------------------------------------- 1 | //! Test error reporting for `std` types. 2 | #![allow(dead_code)] 3 | 4 | use eserde::DeserializationErrors; 5 | 6 | #[derive(Debug, eserde::Deserialize)] 7 | struct TopLevelStruct { 8 | a: LeafStruct, 9 | b: u64, 10 | c: String, 11 | #[eserde(compat)] 12 | d: IncompatibleLeafStruct, 13 | } 14 | 15 | #[derive(Debug, eserde::Deserialize)] 16 | struct LeafStruct { 17 | #[serde(default)] 18 | a2: Option, 19 | } 20 | 21 | #[derive(Debug, serde::Deserialize)] 22 | struct IncompatibleLeafStruct { 23 | #[serde(default)] 24 | a2: Option, 25 | } 26 | 27 | #[test] 28 | fn test_transparent() { 29 | const PAYLOAD: &str = r#"{ 30 | "a": { "a2": -5 }, 31 | "c": 8, 32 | "d": { "a2": false } 33 | }"#; 34 | 35 | insta::allow_duplicates! { 36 | // Fully transparent, all should behave the same. 37 | check(eserde::json::from_str::(PAYLOAD)); 38 | check(eserde::json::from_str::>(PAYLOAD)); 39 | check(eserde::json::from_str::>(PAYLOAD)); 40 | check(eserde::json::from_str::>(PAYLOAD)); 41 | // If `PAYLOAD` is not `"null"`, `Option` should behave the same. 42 | check(eserde::json::from_str::>(PAYLOAD)); 43 | } 44 | fn check(result: Result) { 45 | let errors = result.unwrap_err(); 46 | insta::assert_snapshot!(errors, @r###" 47 | Something went wrong during deserialization: 48 | - a.a2: invalid value: integer `-5`, expected u32 at line 2 column 23 49 | - c: invalid type: integer `8`, expected a string at line 3 column 14 50 | - d.a2: invalid type: boolean `false`, expected u32 at line 4 column 26 51 | - missing field `b` 52 | "### 53 | ); 54 | } 55 | } 56 | 57 | #[test] 58 | fn test_seqs() { 59 | const PAYLOAD: &str = r#"[ 60 | { 61 | "a": { "a2": 15 }, 62 | "b": 42, 63 | "c": "foo", 64 | "d": { "a2": 100 } 65 | }, 66 | { 67 | "a": { "a2": -5 }, 68 | "c": 8 69 | }, 70 | { 71 | "a": { "a2": 15 }, 72 | "b": 42, 73 | "c": "foo", 74 | "d": {} 75 | }, 76 | { 77 | "a": { "a2": -5 }, 78 | "c": 8 79 | } 80 | ]"#; 81 | 82 | insta::allow_duplicates! { 83 | check(eserde::json::from_str::<[TopLevelStruct; 4]>(PAYLOAD)); 84 | check(eserde::json::from_str::>(PAYLOAD)); 85 | check(eserde::json::from_str::>(PAYLOAD)); 86 | check(eserde::json::from_str::>(PAYLOAD)); 87 | check(eserde::json::from_str::>(PAYLOAD)); 88 | } 89 | fn check(result: Result) { 90 | let errors = result.unwrap_err(); 91 | insta::assert_snapshot!(errors, @r###" 92 | Something went wrong during deserialization: 93 | - [1].a.a2: invalid value: integer `-5`, expected u32 at line 9 column 27 94 | - [1].c: invalid type: integer `8`, expected a string at line 10 column 18 95 | - [1]: missing field `b` 96 | - [1]: missing field `d` 97 | - [3].a.a2: invalid value: integer `-5`, expected u32 at line 19 column 27 98 | - [3].c: invalid type: integer `8`, expected a string at line 20 column 18 99 | - [3]: missing field `b` 100 | - [3]: missing field `d` 101 | "### 102 | ); 103 | } 104 | 105 | // Input is too long. 106 | let errors = eserde::json::from_str::<[TopLevelStruct; 3]>(PAYLOAD).unwrap_err(); 107 | insta::assert_snapshot!(errors, @r###" 108 | Something went wrong during deserialization: 109 | - [1].a.a2: invalid value: integer `-5`, expected u32 at line 9 column 27 110 | - [1].c: invalid type: integer `8`, expected a string at line 10 column 18 111 | - [1]: missing field `b` 112 | - [1]: missing field `d` 113 | - [3].a.a2: invalid value: integer `-5`, expected u32 at line 19 column 27 114 | - [3].c: invalid type: integer `8`, expected a string at line 20 column 18 115 | - [3]: missing field `b` 116 | - [3]: missing field `d` 117 | - expected sequence of 3 elements, found 4 elements. 118 | "###); 119 | 120 | // Input is too short. 121 | let errors = eserde::json::from_str::<[TopLevelStruct; 5]>(PAYLOAD).unwrap_err(); 122 | insta::assert_snapshot!(errors, @r###" 123 | Something went wrong during deserialization: 124 | - [1].a.a2: invalid value: integer `-5`, expected u32 at line 9 column 27 125 | - [1].c: invalid type: integer `8`, expected a string at line 10 column 18 126 | - [1]: missing field `b` 127 | - [1]: missing field `d` 128 | - [3].a.a2: invalid value: integer `-5`, expected u32 at line 19 column 27 129 | - [3].c: invalid type: integer `8`, expected a string at line 20 column 18 130 | - [3]: missing field `b` 131 | - [3]: missing field `d` 132 | - expected sequence of 5 elements, found 4 elements. 133 | "###); 134 | } 135 | 136 | #[test] 137 | fn test_map_basic() { 138 | const PAYLOAD: &str = r#"{"a": true, "b": 5.5, "c": -5, "d": {}}"#; 139 | 140 | insta::allow_duplicates! { 141 | check(eserde::json::from_str::>(PAYLOAD)); 142 | check(eserde::json::from_str::>(PAYLOAD)); 143 | } 144 | fn check(x: Result) { 145 | let errs = x.unwrap_err(); 146 | insta::assert_snapshot!(errs, @r###" 147 | Something went wrong during deserialization: 148 | - a: invalid type: boolean `true`, expected u64 at line 1 column 10 149 | - b: invalid type: floating point `5.5`, expected u64 at line 1 column 20 150 | - c: invalid value: integer `-5`, expected u64 at line 1 column 29 151 | - d: invalid type: map, expected u64 at line 1 column 36 152 | - expected `,` or `}` at line 1 column 37 153 | "###); 154 | } 155 | } 156 | 157 | #[test] 158 | fn test_map_nested() { 159 | const PAYLOAD: &str = r#"{ 160 | "foo": { 161 | "a": { "a2": 15 }, 162 | "b": 42, 163 | "c": "foo", 164 | "d": { "a2": 100 } 165 | }, 166 | "bar": { 167 | "a": { "a2": -5 }, 168 | "c": 8 169 | }, 170 | "baz": { 171 | "a": { "a2": 15 }, 172 | "b": 42, 173 | "c": "foo", 174 | "d": {} 175 | }, 176 | "bing": { 177 | "a": { "a2": -5 }, 178 | "c": 8 179 | } 180 | }"#; 181 | 182 | insta::allow_duplicates! { 183 | check(eserde::json::from_str::>(PAYLOAD)); 184 | check(eserde::json::from_str::>(PAYLOAD)); 185 | } 186 | fn check(x: Result) { 187 | let errs = x.unwrap_err(); 188 | insta::assert_snapshot!(errs, @r###" 189 | Something went wrong during deserialization: 190 | - bar.a.a2: invalid value: integer `-5`, expected u32 at line 9 column 27 191 | - bar.c: invalid type: integer `8`, expected a string at line 10 column 18 192 | - bar: missing field `b` 193 | - bar: missing field `d` 194 | - bing.a.a2: invalid value: integer `-5`, expected u32 at line 19 column 27 195 | - bing.c: invalid type: integer `8`, expected a string at line 20 column 18 196 | - bing: missing field `b` 197 | - bing: missing field `d` 198 | "###); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /eserde_axum/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.1.5](https://github.com/mainmatter/eserde/compare/eserde_axum-0.1.4...eserde_axum-0.1.5) - 2025-03-12 11 | 12 | 13 | ### ⛰️ Features 14 | - support `#[serde(with = "..")]` on fields, #18 ([#40](https://github.com/mainmatter/eserde/pull/40)) (by @MingweiSamuel) - #40 15 | 16 | 17 | ### Contributors 18 | 19 | * @MingweiSamuel 20 | 21 | ## [0.1.4](https://github.com/mainmatter/eserde/compare/eserde_axum-0.1.3...eserde_axum-0.1.4) - 2025-03-05 22 | 23 | 24 | ### ⛰️ Features 25 | - support `#[serde(deserialize_with = "..")]`, `#[serde(default = "..")]` for fields, fix #21 ([#23](https://github.com/mainmatter/eserde/pull/23)) (by @MingweiSamuel) - #23 26 | 27 | 28 | ### Contributors 29 | 30 | * @MingweiSamuel 31 | 32 | ## [0.1.3](https://github.com/mainmatter/eserde/compare/eserde_axum-0.1.2...eserde_axum-0.1.3) - 2025-03-03 33 | 34 | 35 | ### 🐛 Bug Fixes 36 | - handle generic params with bounds ([#28](https://github.com/mainmatter/eserde/pull/28)) (by @MingweiSamuel) - #28 37 | 38 | 39 | ### Contributors 40 | 41 | * @MingweiSamuel 42 | 43 | ## [0.1.2](https://github.com/mainmatter/eserde/compare/eserde_axum-0.1.1...eserde_axum-0.1.2) - 2025-03-03 44 | 45 | 46 | ### 🐛 Bug Fixes 47 | - error message ordering (by @MingweiSamuel) - #25 48 | - ensure `parse_nested_meta` properly handles values, fix [#24](https://github.com/mainmatter/eserde/pull/24) (by @MingweiSamuel) - #25 49 | 50 | 51 | ### Contributors 52 | 53 | * @MingweiSamuel 54 | 55 | ## [0.1.1](https://github.com/mainmatter/eserde/compare/eserde_axum-0.1.0...eserde_axum-0.1.1) - 2025-02-14 56 | 57 | 58 | ### 📚 Documentation 59 | - Enable unstable rustdoc feature to provide feature-flag information docs.rs (by @LukeMathWalker) - #14 60 | 61 | 62 | ### Contributors 63 | 64 | * @LukeMathWalker 65 | 66 | ## [0.1.0](https://github.com/mainmatter/eserde/releases/tag/eserde_axum-0.1.0) - 2025-02-14 67 | 68 | 69 | ### ⛰️ Features 70 | - Introduce `eserde_axum`, to provide `axum` extractors built on top of `eserde`. (by @LukeMathWalker) - #11 71 | 72 | 73 | ### Contributors 74 | 75 | * @LukeMathWalker 76 | -------------------------------------------------------------------------------- /eserde_axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "eserde_axum" 3 | edition.workspace = true 4 | repository.workspace = true 5 | license.workspace = true 6 | version = "0.1.5" 7 | readme = false 8 | keywords = ["serde", "serialization", "deserialization", "http", "web"] 9 | categories = ["encoding"] 10 | description = "`axum` extractors built on `eserde` to improve error responses" 11 | 12 | [features] 13 | default = ["json"] 14 | json = ["eserde/json", "dep:mime"] 15 | 16 | [dependencies] 17 | axum-core = { workspace = true } 18 | bytes = { workspace = true } 19 | eserde = { path = "../eserde", version = "0.1" } 20 | http = { workspace = true } 21 | mime = { workspace = true, optional = true } 22 | serde = { workspace = true } 23 | serde_json = { workspace = true } 24 | tracing = { workspace = true } 25 | 26 | [dev-dependencies] 27 | axum = { workspace = true } 28 | uuid = { workspace = true, features = ["serde"] } 29 | -------------------------------------------------------------------------------- /eserde_axum/src/details.rs: -------------------------------------------------------------------------------- 1 | //! Types to represent a problem detail error response. 2 | //! 3 | //! See [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html) for more details. 4 | use std::borrow::Cow; 5 | 6 | use bytes::{BufMut, BytesMut}; 7 | use http::{header::CONTENT_TYPE, HeaderName, HeaderValue, StatusCode}; 8 | 9 | #[derive(serde::Serialize)] 10 | pub(crate) struct ProblemDetails { 11 | #[serde(rename = "type")] 12 | pub(crate) type_: Cow<'static, str>, 13 | pub(crate) status: u16, 14 | pub(crate) title: Cow<'static, str>, 15 | pub(crate) detail: Cow<'static, str>, 16 | #[serde(flatten)] 17 | #[serde(skip_serializing_if = "Option::is_none")] 18 | pub(crate) extensions: Option, 19 | } 20 | 21 | #[derive(serde::Serialize)] 22 | pub(crate) struct ValidationErrors { 23 | pub(crate) errors: Vec, 24 | } 25 | 26 | #[derive(serde::Serialize)] 27 | pub(crate) struct ValidationError { 28 | pub(crate) detail: String, 29 | #[serde(flatten)] 30 | pub(crate) source: Source, 31 | } 32 | 33 | /// The request part where the problem occurred. 34 | #[derive(serde::Serialize)] 35 | #[serde(tag = "source", rename_all = "snake_case")] 36 | pub(crate) enum Source { 37 | Body { 38 | /// A [JSON pointer](https://www.rfc-editor.org/info/rfc6901) targeted 39 | /// at the problematic body property. 40 | pointer: Option, 41 | }, 42 | Header { 43 | /// The name of the problematic header. 44 | name: Cow<'static, str>, 45 | }, 46 | } 47 | 48 | impl axum_core::response::IntoResponse for ProblemDetails 49 | where 50 | Extension: serde::Serialize, 51 | { 52 | fn into_response(self) -> axum_core::response::Response { 53 | // Use a small initial capacity of 128 bytes like serde_json::to_vec 54 | // https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189 55 | let mut buf = BytesMut::with_capacity(128).writer(); 56 | match serde_json::to_writer(&mut buf, &self) { 57 | Ok(()) => ( 58 | [(CONTENT_TYPE, APPLICATION_PROBLEM_JSON)], 59 | buf.into_inner().freeze(), 60 | ) 61 | .into_response(), 62 | Err(_) => INTERNAL_SERVER_ERROR.into_response(), 63 | } 64 | } 65 | } 66 | 67 | pub(crate) const APPLICATION_PROBLEM_JSON: HeaderValue = 68 | HeaderValue::from_static("application/problem+json"); 69 | 70 | pub(crate) const INTERNAL_SERVER_ERROR: (StatusCode, [(HeaderName, HeaderValue); 1], &[u8]) = ( 71 | StatusCode::INTERNAL_SERVER_ERROR, 72 | [(CONTENT_TYPE, APPLICATION_PROBLEM_JSON)], 73 | INTERNAL_SERVER_ERROR_PROBLEM, 74 | ); 75 | 76 | pub(crate) const INTERNAL_SERVER_ERROR_PROBLEM: &[u8] = br#"{ 77 | "type": "internal_server_error", 78 | "title": "Internal Server Error", 79 | "detail": "Something went wrong when processing your request. Please try again later." 80 | "status": 500 81 | }"#; 82 | 83 | pub(crate) struct InvalidRequest(ProblemDetails); 84 | 85 | impl InvalidRequest { 86 | pub(crate) fn new(errors: ValidationErrors) -> Self { 87 | Self(ProblemDetails { 88 | type_: "invalid_request".into(), 89 | status: Self::status().as_u16(), 90 | title: "The request is invalid".into(), 91 | extensions: Some(errors), 92 | detail: "The request is either malformed or doesn't match the expected schema".into(), 93 | }) 94 | } 95 | 96 | pub(crate) fn status() -> StatusCode { 97 | StatusCode::BAD_REQUEST 98 | } 99 | 100 | pub(crate) fn into_inner(self) -> ProblemDetails { 101 | self.0 102 | } 103 | } 104 | 105 | impl axum_core::response::IntoResponse for InvalidRequest { 106 | fn into_response(self) -> axum_core::response::Response { 107 | self.into_inner().into_response() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /eserde_axum/src/json/json_.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | use std::ops::DerefMut; 3 | 4 | use crate::details::INTERNAL_SERVER_ERROR; 5 | 6 | use super::*; 7 | use axum_core::extract::FromRequest; 8 | use axum_core::extract::Request; 9 | use axum_core::response::{IntoResponse, Response}; 10 | use bytes::{BufMut, Bytes, BytesMut}; 11 | use eserde::EDeserialize; 12 | use http::header::{self, HeaderMap, HeaderValue}; 13 | use serde::{de::DeserializeOwned, Serialize}; 14 | 15 | /// JSON Extractor / Response. 16 | /// 17 | /// When used as an extractor, it can deserialize request bodies into some type that 18 | /// implements [`serde::de::DeserializeOwned`] and [`eserde::EDeserialize`]. 19 | /// The request will be rejected (and a [`JsonRejection`] will be returned) if: 20 | /// 21 | /// - The request doesn't have a `Content-Type: application/json` (or similar) header. 22 | /// - The body doesn't contain syntactically valid JSON or it couldn't be deserialized into the target type. 23 | /// - Buffering the request body fails. 24 | /// 25 | /// ⚠️ Since parsing JSON requires consuming the request body, the `Json` extractor must be 26 | /// *last* if there are multiple extractors in a handler. 27 | /// See ["the order of extractors"][order-of-extractors] 28 | /// 29 | /// [order-of-extractors]: https://docs.rs/axum/latest/axum/extract/index.html#the-order-of-extractors 30 | /// 31 | /// See [`JsonRejection`] for more details. 32 | /// 33 | /// # Extractor example 34 | /// 35 | /// ```rust,no_run 36 | /// use axum::{routing::post, Router}; 37 | /// use eserde_axum::Json; 38 | /// 39 | /// #[derive(eserde::Deserialize)] 40 | /// struct CreateUser { 41 | /// email: String, 42 | /// password: String, 43 | /// } 44 | /// 45 | /// async fn create_user(Json(payload): Json) { 46 | /// // payload is a `CreateUser` 47 | /// } 48 | /// 49 | /// let app = Router::new().route("/users", post(create_user)); 50 | /// # let _: Router = app; 51 | /// ``` 52 | /// 53 | /// When used as a response, it can serialize any type that implements [`serde::Serialize`] to 54 | /// `JSON`, and will automatically set `Content-Type: application/json` header. 55 | /// 56 | /// If the [`Serialize`] implementation decides to fail 57 | /// or if a map with non-string keys is used, 58 | /// a 500 response will be issued. 59 | /// 60 | /// # Response example 61 | /// 62 | /// ``` 63 | /// use axum::{ 64 | /// extract::Path, 65 | /// routing::get, 66 | /// Router, 67 | /// }; 68 | /// use eserde_axum::Json; 69 | /// use serde::Serialize; 70 | /// use uuid::Uuid; 71 | /// 72 | /// #[derive(Serialize)] 73 | /// struct User { 74 | /// id: Uuid, 75 | /// username: String, 76 | /// } 77 | /// 78 | /// async fn get_user(Path(user_id) : Path) -> Json { 79 | /// let user = find_user(user_id).await; 80 | /// Json(user) 81 | /// } 82 | /// 83 | /// async fn find_user(user_id: Uuid) -> User { 84 | /// // ... 85 | /// # unimplemented!() 86 | /// } 87 | /// 88 | /// let app = Router::new().route("/users/{id}", get(get_user)); 89 | /// # let _: Router = app; 90 | /// ``` 91 | #[derive(Debug, Clone, Copy, Default)] 92 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 93 | #[must_use] 94 | pub struct Json(pub T); 95 | 96 | impl FromRequest for Json 97 | where 98 | T: DeserializeOwned, 99 | T: for<'de> EDeserialize<'de>, 100 | S: Send + Sync, 101 | { 102 | type Rejection = JsonRejection; 103 | 104 | async fn from_request(req: Request, state: &S) -> Result { 105 | check_json_content_type(req.headers())?; 106 | let bytes = Bytes::from_request(req, state).await?; 107 | Self::from_bytes(&bytes) 108 | } 109 | } 110 | 111 | /// Check that the `Content-Type` header is set to `application/json`, or another 112 | /// `application/*+json` MIME type. 113 | /// 114 | /// Return an error otherwise. 115 | fn check_json_content_type(headers: &HeaderMap) -> Result<(), JsonRejection> { 116 | let Some(content_type) = headers.get(http::header::CONTENT_TYPE) else { 117 | return Err(MissingJsonContentType.into()); 118 | }; 119 | let Ok(content_type) = content_type.to_str() else { 120 | return Err(MissingJsonContentType.into()); 121 | }; 122 | 123 | let Ok(mime) = content_type.parse::() else { 124 | return Err(JsonContentTypeMismatch { 125 | actual: content_type.to_string(), 126 | } 127 | .into()); 128 | }; 129 | 130 | let is_json_content_type = mime.type_() == "application" 131 | && (mime.subtype() == "json" || mime.suffix().is_some_and(|name| name == "json")); 132 | if !is_json_content_type { 133 | return Err(JsonContentTypeMismatch { 134 | actual: content_type.to_string(), 135 | } 136 | .into()); 137 | } 138 | Ok(()) 139 | } 140 | 141 | impl Deref for Json { 142 | type Target = T; 143 | 144 | fn deref(&self) -> &Self::Target { 145 | &self.0 146 | } 147 | } 148 | 149 | impl DerefMut for Json { 150 | fn deref_mut(&mut self) -> &mut Self::Target { 151 | &mut self.0 152 | } 153 | } 154 | 155 | impl From for Json { 156 | fn from(inner: T) -> Self { 157 | Self(inner) 158 | } 159 | } 160 | 161 | impl Json 162 | where 163 | T: DeserializeOwned, 164 | T: for<'de> EDeserialize<'de>, 165 | { 166 | /// Construct a `Json` from a byte slice. Most users should prefer to use the `FromRequest` impl 167 | /// but special cases may require first extracting a `Request` into `Bytes` then optionally 168 | /// constructing a `Json`. 169 | pub fn from_bytes(bytes: &[u8]) -> Result { 170 | match eserde::json::from_slice(bytes) { 171 | Ok(value) => Ok(Json(value)), 172 | Err(errors) => Err(JsonError::new(errors).into()), 173 | } 174 | } 175 | } 176 | 177 | impl IntoResponse for Json 178 | where 179 | T: Serialize, 180 | { 181 | fn into_response(self) -> Response { 182 | // Use a small initial capacity of 128 bytes like serde_json::to_vec 183 | // https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189 184 | let mut buf = BytesMut::with_capacity(128).writer(); 185 | match serde_json::to_writer(&mut buf, &self.0) { 186 | Ok(()) => ( 187 | [( 188 | header::CONTENT_TYPE, 189 | HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), 190 | )], 191 | buf.into_inner().freeze(), 192 | ) 193 | .into_response(), 194 | Err(_) => INTERNAL_SERVER_ERROR.into_response(), 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /eserde_axum/src/json/mod.rs: -------------------------------------------------------------------------------- 1 | //! Supporting types for the [`Json`] extractor. 2 | mod json_; 3 | mod rejections; 4 | 5 | #[doc(hidden)] 6 | pub use json_::Json; 7 | pub use rejections::*; 8 | -------------------------------------------------------------------------------- /eserde_axum/src/json/rejections.rs: -------------------------------------------------------------------------------- 1 | use axum_core::extract::rejection::{BytesRejection, FailedToBufferBody}; 2 | use eserde::DeserializationErrors; 3 | use http::header::CONTENT_TYPE; 4 | 5 | use crate::details::{ 6 | InvalidRequest, ProblemDetails, Source, ValidationError, ValidationErrors, 7 | INTERNAL_SERVER_ERROR, 8 | }; 9 | 10 | #[doc(hidden)] 11 | macro_rules! __log_rejection { 12 | ( 13 | rejection_type = $ty:ident, 14 | status = $status:expr, 15 | ) => { 16 | { 17 | tracing::event!( 18 | target: "eserde_axum::json::rejection", 19 | tracing::Level::TRACE, 20 | status = $status.as_u16(), 21 | rejection_type = ::std::any::type_name::<$ty>(), 22 | "rejecting request", 23 | ); 24 | } 25 | }; 26 | } 27 | 28 | #[derive(Debug)] 29 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 30 | /// Rejection type for [`Json`](super::Json). 31 | /// 32 | /// This rejection is used if the request body couldn't be deserialized 33 | /// into the target type. 34 | pub struct JsonError(pub(crate) DeserializationErrors); 35 | 36 | impl JsonError { 37 | pub(crate) fn new(err: DeserializationErrors) -> Self { 38 | Self(err) 39 | } 40 | } 41 | 42 | impl axum_core::response::IntoResponse for JsonError { 43 | fn into_response(self) -> axum_core::response::Response { 44 | let errors = self 45 | .0 46 | .iter() 47 | .map(|e| { 48 | let pointer = e.path().map(|path| { 49 | path.iter().fold(String::new(), |mut acc, part| { 50 | acc.push('/'); 51 | acc.push_str(&part.to_string()); 52 | acc 53 | }) 54 | }); 55 | ValidationError { 56 | detail: e.message().into(), 57 | source: Source::Body { pointer }, 58 | } 59 | }) 60 | .collect(); 61 | let response = InvalidRequest::new(ValidationErrors { errors }); 62 | __log_rejection!( 63 | rejection_type = JsonError, 64 | status = InvalidRequest::status(), 65 | ); 66 | response.into_response() 67 | } 68 | } 69 | 70 | impl std::fmt::Display for JsonError { 71 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 72 | f.write_str("Failed to deserialize the request JSON body into the target schema:\n")?; 73 | for e in self.0.iter() { 74 | writeln!(f, "- {}", e)?; 75 | } 76 | Ok(()) 77 | } 78 | } 79 | 80 | impl std::error::Error for JsonError { 81 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 82 | Some(&self.0) 83 | } 84 | } 85 | 86 | #[derive(Debug)] 87 | #[non_exhaustive] 88 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 89 | /// Rejection type for [`Json`](super::Json) used if the `Content-Type` 90 | /// header is missing. 91 | pub struct MissingJsonContentType; 92 | 93 | impl axum_core::response::IntoResponse for MissingJsonContentType { 94 | fn into_response(self) -> axum_core::response::Response { 95 | let error = ValidationError { 96 | detail: "Expected request with `Content-Type: application/json`, but no `Content-Type` header was found".into(), 97 | source: Source::Header { 98 | name: CONTENT_TYPE.as_str().into(), 99 | }, 100 | }; 101 | let response = InvalidRequest::new(ValidationErrors { 102 | errors: vec![error], 103 | }); 104 | __log_rejection!( 105 | rejection_type = MissingJsonContentType, 106 | status = InvalidRequest::status(), 107 | ); 108 | response.into_response() 109 | } 110 | } 111 | impl std::fmt::Display for MissingJsonContentType { 112 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 113 | write!(f, "Expected request with `Content-Type: application/json`") 114 | } 115 | } 116 | impl std::error::Error for MissingJsonContentType {} 117 | 118 | impl Default for MissingJsonContentType { 119 | fn default() -> Self { 120 | Self 121 | } 122 | } 123 | 124 | #[derive(Debug)] 125 | #[non_exhaustive] 126 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 127 | /// Rejection type for [`Json`](super::Json) used if the `Content-Type` 128 | /// header has an incorrect value. 129 | pub struct JsonContentTypeMismatch { 130 | pub(crate) actual: String, 131 | } 132 | 133 | impl axum_core::response::IntoResponse for JsonContentTypeMismatch { 134 | fn into_response(self) -> axum_core::response::Response { 135 | let error = ValidationError { 136 | detail: format!( 137 | "Expected request with `Content-Type: application/json` or `application/*+json`, but found `{}`", 138 | self.actual 139 | ), 140 | source: Source::Header { 141 | name: CONTENT_TYPE.as_str().into(), 142 | }, 143 | }; 144 | let response = InvalidRequest::new(ValidationErrors { 145 | errors: vec![error], 146 | }); 147 | __log_rejection!( 148 | rejection_type = JsonContentTypeMismatch, 149 | status = InvalidRequest::status(), 150 | ); 151 | response.into_response() 152 | } 153 | } 154 | 155 | impl std::fmt::Display for JsonContentTypeMismatch { 156 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 157 | write!( 158 | f, 159 | "Expected request with `Content-Type: application/json` or `application/*+json`, but found `{}`", 160 | self.actual 161 | ) 162 | } 163 | } 164 | 165 | impl std::error::Error for JsonContentTypeMismatch {} 166 | 167 | /// Rejection used for [`Json`](super::Json). 168 | /// 169 | /// Contains one variant for each way the [`Json`](super::Json) extractor 170 | /// can fail. 171 | /// 172 | /// All error responses follow the problem details specification, 173 | /// as outlined in [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html). 174 | #[derive(Debug)] 175 | #[non_exhaustive] 176 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 177 | pub enum JsonRejection { 178 | #[allow(missing_docs)] 179 | JsonError(JsonError), 180 | #[allow(missing_docs)] 181 | MissingJsonContentType(MissingJsonContentType), 182 | #[allow(missing_docs)] 183 | JsonContentTypeMismatch(JsonContentTypeMismatch), 184 | #[allow(missing_docs)] 185 | BytesRejection(BytesRejection), 186 | } 187 | impl axum_core::response::IntoResponse for JsonRejection { 188 | fn into_response(self) -> axum_core::response::Response { 189 | match self { 190 | Self::JsonError(inner) => inner.into_response(), 191 | Self::MissingJsonContentType(inner) => inner.into_response(), 192 | Self::JsonContentTypeMismatch(inner) => inner.into_response(), 193 | Self::BytesRejection(inner) => { 194 | let mut response = None; 195 | #[allow(clippy::single_match)] 196 | match inner { 197 | BytesRejection::FailedToBufferBody(failed_to_buffer_body) => { 198 | match failed_to_buffer_body { 199 | FailedToBufferBody::LengthLimitError(length_limit_error) => { 200 | let details: ProblemDetails<()> = ProblemDetails { 201 | type_: "content_too_large".into(), 202 | status: length_limit_error.status().as_u16(), 203 | title: "The content is too large".into(), 204 | detail: length_limit_error.body_text().into(), 205 | extensions: None, 206 | }; 207 | response = Some(details.into_response()); 208 | } 209 | FailedToBufferBody::UnknownBodyError(unknown_body_error) => { 210 | let details: ProblemDetails<()> = ProblemDetails { 211 | type_: "body_buffering_error".into(), 212 | status: unknown_body_error.status().as_u16(), 213 | title: "Failed to buffer the body".into(), 214 | detail: unknown_body_error.body_text().into(), 215 | extensions: None, 216 | }; 217 | response = Some(details.into_response()); 218 | } 219 | _ => {} 220 | } 221 | } 222 | _ => {} 223 | } 224 | response.unwrap_or_else(|| INTERNAL_SERVER_ERROR.into_response()) 225 | } 226 | } 227 | } 228 | } 229 | 230 | impl From for JsonRejection { 231 | fn from(inner: JsonError) -> Self { 232 | Self::JsonError(inner) 233 | } 234 | } 235 | impl From for JsonRejection { 236 | fn from(inner: MissingJsonContentType) -> Self { 237 | Self::MissingJsonContentType(inner) 238 | } 239 | } 240 | impl From for JsonRejection { 241 | fn from(inner: JsonContentTypeMismatch) -> Self { 242 | Self::JsonContentTypeMismatch(inner) 243 | } 244 | } 245 | impl From for JsonRejection { 246 | fn from(inner: BytesRejection) -> Self { 247 | Self::BytesRejection(inner) 248 | } 249 | } 250 | impl std::fmt::Display for JsonRejection { 251 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 252 | match self { 253 | Self::JsonError(inner) => write!(f, "{inner}"), 254 | Self::MissingJsonContentType(inner) => write!(f, "{inner}"), 255 | Self::JsonContentTypeMismatch(inner) => write!(f, "{inner}"), 256 | Self::BytesRejection(inner) => write!(f, "{inner}"), 257 | } 258 | } 259 | } 260 | impl std::error::Error for JsonRejection { 261 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 262 | match self { 263 | Self::JsonError(inner) => inner.source(), 264 | Self::MissingJsonContentType(inner) => inner.source(), 265 | Self::JsonContentTypeMismatch(inner) => inner.source(), 266 | Self::BytesRejection(inner) => inner.source(), 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /eserde_axum/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # eserde_axum 2 | //! 3 | //! A collection of [`axum`] extractors built on top of [`eserde`] to 4 | //! provide exhaustive error reports when deserialization fails. 5 | //! They are designed to be drop-in replacement for their official [`axum`] 6 | //! counterpart. 7 | //! 8 | //! Check out [`Json`] for working with JSON payloads. 9 | //! 10 | //! [`axum`]: https://docs.rs/axum 11 | //! [`eserde`]: https://docs.rs/eserde 12 | #![deny(missing_docs)] 13 | #![cfg_attr(docsrs, feature(doc_cfg))] 14 | 15 | #[cfg(feature = "json")] 16 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 17 | pub mod json; 18 | 19 | #[cfg(feature = "json")] 20 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 21 | pub use json::Json; 22 | 23 | pub(crate) mod details; 24 | -------------------------------------------------------------------------------- /eserde_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "eserde_derive" 3 | authors = ["Luca Palmieri "] 4 | edition.workspace = true 5 | version.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | readme = false 9 | keywords = ["serde", "serialization", "deserialization"] 10 | categories = ["encoding"] 11 | description = "A derive macro for the eserde crate" 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | syn = { workspace = true, features = ["extra-traits", "full"] } 18 | quote = { workspace = true } 19 | proc-macro2 = { workspace = true } 20 | indexmap = { workspace = true } 21 | -------------------------------------------------------------------------------- /eserde_derive/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /eserde_derive/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /eserde_derive/src/attr.rs: -------------------------------------------------------------------------------- 1 | use std::ops::ControlFlow::{self, Break, Continue}; 2 | 3 | use proc_macro2::Span; 4 | use quote::ToTokens; 5 | use syn::{meta::ParseNestedMeta, punctuated::Punctuated, Error, Expr, Meta, Path, Result, Token}; 6 | 7 | /// Represents a single meta item within an attribute. 8 | /// 9 | /// For example, `default` and `rename = "foo"` within `#[serde(default, rename = "foo")]`). 10 | #[derive(Clone, Debug)] 11 | pub struct MetaItem { 12 | pub key: Path, 13 | pub value: Option<(Token![=], Expr)>, 14 | } 15 | impl MetaItem { 16 | pub fn parse(parser: ParseNestedMeta) -> Result { 17 | let key = parser.path; 18 | let value = if parser.input.peek(Token![=]) { 19 | let eq = parser.input.parse().unwrap(); 20 | let expr = parser.input.parse()?; 21 | Some((eq, expr)) 22 | } else { 23 | None 24 | }; 25 | Ok(Self { key, value }) 26 | } 27 | } 28 | impl ToTokens for MetaItem { 29 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 30 | self.key.to_tokens(tokens); 31 | if let Some((eq, expr)) = &self.value { 32 | eq.to_tokens(tokens); 33 | expr.to_tokens(tokens); 34 | } 35 | } 36 | } 37 | 38 | /// Returns if `#[ATTR_NAME(META_KEY)]` exists within any of the attributes. 39 | /// 40 | /// ```rust 41 | /// # mod attr { include!("attr.rs"); } use attr::find_attr_meta; // hack for proc_macro doctests. 42 | /// # fn main() { 43 | /// use syn::parse_quote; 44 | /// 45 | /// // `Some(..)` 46 | /// assert!(find_attr_meta(&[parse_quote!( #[serde(rename)] )], "serde", "rename").is_some()); 47 | /// assert!(find_attr_meta(&[parse_quote!( #[serde(rename = "foo")] )], "serde", "rename").is_some()); 48 | /// assert!(find_attr_meta(&[parse_quote!( #[serde(default, rename)] )], "serde", "rename").is_some()); 49 | /// assert!(find_attr_meta(&[parse_quote!( #[serde(default, rename = "bar")] )], "serde", "rename").is_some()); 50 | /// assert!(find_attr_meta(&[parse_quote!( #[serde(rename, rename = "baz", rename)] )], "serde", "rename").is_some()); 51 | /// assert!(find_attr_meta(&[parse_quote!( #[serde(rename, rename)] )], "serde", "rename").is_some()); 52 | /// // `None` 53 | /// assert!(find_attr_meta(&[parse_quote!( #[serde(default)] )], "serde", "rename").is_none()); 54 | /// assert!(find_attr_meta(&[parse_quote!( #[ignore(rename = "bing")] )], "serde", "rename").is_none()); 55 | /// assert!(find_attr_meta(&[parse_quote!( #[serde = "rename"] )], "serde", "rename").is_none()); 56 | /// # } 57 | /// ``` 58 | pub fn find_attr_meta( 59 | attrs: &[syn::Attribute], 60 | attr_name: &str, 61 | meta_key: &str, 62 | ) -> Option { 63 | visit_attr_metas(attrs, attr_name, |meta_item| { 64 | if meta_item.key.is_ident(meta_key) { 65 | Break(meta_item) 66 | } else { 67 | Continue(()) 68 | } 69 | }) 70 | } 71 | 72 | /// Visits each meta item within each `#[ATTR_NAME(...)]` attribute, calling `visitor` on each one. 73 | /// 74 | /// If `visitor` returns `Break`, the iteration stops and the value is returned. 75 | pub fn visit_attr_metas( 76 | attrs: &[syn::Attribute], 77 | attr_name: &str, 78 | mut visitor: impl FnMut(MetaItem) -> ControlFlow, 79 | ) -> Option { 80 | attrs.iter().find_map(move |attr| { 81 | if !attr.path().is_ident(attr_name) { 82 | return None; 83 | } 84 | let mut out = None; 85 | let _ = attr.parse_nested_meta(|meta_item| { 86 | let meta_item = MetaItem::parse(meta_item)?; 87 | if let Break(value) = (visitor)(meta_item) { 88 | out = Some(value); 89 | // Exit `parse_nested_meta` early. 90 | Err(Error::new(Span::call_site(), "")) 91 | } else { 92 | Ok(()) 93 | } 94 | }); 95 | out 96 | }) 97 | } 98 | 99 | /// Removes the first `#[ATTR_NAME(META_KEY)]` within the attributes and returns it. 100 | pub fn remove_attr_meta( 101 | attrs: &mut [syn::Attribute], 102 | attr_name: &str, 103 | meta_key: &str, 104 | ) -> Option { 105 | attrs.iter_mut().find_map(|attr| { 106 | if !attr.path().is_ident(attr_name) { 107 | return None; 108 | } 109 | let Meta::List(meta_list) = &mut attr.meta else { 110 | return None; 111 | }; 112 | let mut out = None; 113 | // The transformed meta items. 114 | let mut transformed = Punctuated::::new(); 115 | let result = meta_list.parse_nested_meta(|meta_item| { 116 | let meta_item = MetaItem::parse(meta_item)?; 117 | if meta_item.key.is_ident(meta_key) && out.is_none() { 118 | // Remove the item. 119 | out = Some(meta_item); 120 | } else { 121 | // Keep the original item. 122 | transformed.push(meta_item.clone()); 123 | } 124 | Ok(()) 125 | }); 126 | 127 | if result.is_err() { 128 | // Parsing error occured, leave this attribute unchanged, continue. 129 | None 130 | } else { 131 | // Replace the attribute's list tokens with the transformed ones. 132 | meta_list.tokens = transformed.into_token_stream(); 133 | out 134 | } 135 | }) 136 | } 137 | -------------------------------------------------------------------------------- /eserde_derive/src/emit.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexSet; 2 | use quote::{format_ident, quote, ToTokens}; 3 | use syn::{Data, DeriveInput, GenericParam, Generics, Lifetime}; 4 | 5 | use crate::model::{PermissiveCompanionType, ShadowType}; 6 | 7 | impl ToTokens for ShadowType { 8 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 9 | let Self(input) = self; 10 | 11 | quote! { 12 | #[derive(::eserde::_serde::Deserialize)] 13 | #[serde(crate = "eserde::_serde")] 14 | #input 15 | } 16 | .to_tokens(tokens); 17 | } 18 | } 19 | 20 | impl ToTokens for PermissiveCompanionType { 21 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 22 | let Self { ty_, impl_, .. } = self; 23 | quote! { 24 | #[derive(::eserde::_serde::Deserialize)] 25 | #[serde(crate = "eserde::_serde")] 26 | #ty_ 27 | 28 | #impl_ 29 | } 30 | .to_tokens(tokens); 31 | } 32 | } 33 | 34 | pub struct ImplDeserGenerics<'a> { 35 | deser_generics: Generics, 36 | input_generics: &'a Generics, 37 | } 38 | 39 | impl<'a> ImplDeserGenerics<'a> { 40 | pub fn new( 41 | input: &'a DeriveInput, 42 | eserde_aware_generics: &IndexSet, 43 | ) -> ImplDeserGenerics<'a> { 44 | let mut deser_generics = input.generics.clone(); 45 | deser_generics.make_where_clause(); 46 | if let Some(where_clause) = &mut deser_generics.where_clause { 47 | // Each type parameter must implement `Deserialize` for the 48 | // type to implement `Deserialize`. 49 | // 50 | // TODO: Take into account the `#[serde(bound)]` attribute https://serde.rs/container-attrs.html#bound 51 | for ty_param in input.generics.type_params() { 52 | let ident = &ty_param.ident; 53 | let predicate = if eserde_aware_generics.contains(ident) { 54 | syn::parse_quote! { #ident: ::eserde::EDeserialize<'de> } 55 | } else { 56 | syn::parse_quote! { #ident: ::eserde::_serde::Deserialize<'de> } 57 | }; 58 | where_clause.predicates.push(predicate); 59 | } 60 | 61 | // Each lifetime parameter must be outlived by `'de`, the lifetime of the `Deserialize` trait. 62 | for lifetime_param in input.generics.lifetimes() { 63 | let lifetime = &lifetime_param.lifetime; 64 | where_clause 65 | .predicates 66 | .push(syn::parse_quote! { 'de: #lifetime }); 67 | } 68 | } else { 69 | unreachable!() 70 | } 71 | 72 | // The `'de` lifetime of the `Deserialize` trait. 73 | // There is no way to add a lifetime to the `impl_generics` returned by `split_for_impl`, so we 74 | // have to create a new set of generics with the lifetime added and then split again. 75 | let param = GenericParam::Lifetime(syn::LifetimeParam::new(Lifetime::new( 76 | "'de", 77 | proc_macro2::Span::call_site(), 78 | ))); 79 | deser_generics.params.push(param); 80 | 81 | Self { 82 | deser_generics, 83 | input_generics: &input.generics, 84 | } 85 | } 86 | 87 | pub fn split_for_impl( 88 | &self, 89 | ) -> ( 90 | syn::ImplGenerics<'_>, 91 | syn::TypeGenerics, 92 | Option<&syn::WhereClause>, 93 | ) { 94 | let (impl_generics, _, where_clause) = self.deser_generics.split_for_impl(); 95 | let (_, ty_generics, _) = self.input_generics.split_for_impl(); 96 | (impl_generics, ty_generics, where_clause) 97 | } 98 | } 99 | 100 | /// Initialize the target type from the shadow type, assigning each field from the shadow type to the 101 | /// corresponding field on the target type. 102 | pub fn initialize_from_shadow( 103 | input: &Data, 104 | type_ident: &syn::Ident, 105 | shadow_binding: &syn::Ident, 106 | shadow_type_ident: &syn::Ident, 107 | ) -> proc_macro2::TokenStream { 108 | match input { 109 | Data::Struct(data) => { 110 | let fields = data.fields.members().map(|field| { 111 | quote! { 112 | #field: #shadow_binding.#field 113 | } 114 | }); 115 | quote! { 116 | #type_ident { 117 | #(#fields),* 118 | } 119 | } 120 | } 121 | Data::Enum(e) => { 122 | let variants = e.variants.iter().map(|variant| { 123 | let variant_ident = &variant.ident; 124 | match &variant.fields { 125 | syn::Fields::Named(fields) => { 126 | let fields: Vec<_> = fields.named.iter().map(|field| { 127 | let field = field.ident.as_ref().unwrap(); 128 | quote! { 129 | #field 130 | } 131 | }).collect(); 132 | quote! { 133 | #shadow_type_ident::#variant_ident { #(#fields),* } => #type_ident::#variant_ident { #(#fields),* } 134 | } 135 | } 136 | syn::Fields::Unnamed(fields) => { 137 | let fields: Vec<_> = fields.unnamed.iter().enumerate().map(|(i, _)| { 138 | let i = format_ident!("__v{i}"); 139 | quote! { 140 | #i 141 | } 142 | }).collect(); 143 | quote! { 144 | #shadow_type_ident::#variant_ident(#(#fields),*) => #type_ident::#variant_ident(#(#fields),*) 145 | } 146 | } 147 | syn::Fields::Unit => { 148 | quote! { 149 | #shadow_type_ident::#variant_ident => #type_ident::#variant_ident 150 | } 151 | } 152 | } 153 | }); 154 | quote! { 155 | match #shadow_binding { 156 | #(#variants),* 157 | } 158 | } 159 | } 160 | Data::Union(_) => unimplemented!(), 161 | } 162 | } 163 | 164 | /// Walk all fields on the companion types to report errors about missing values, if any. 165 | pub fn collect_missing_errors( 166 | input: &Data, 167 | companion_type: &syn::Ident, 168 | companion_binding: &syn::Ident, 169 | n_errors: &syn::Ident, 170 | ) -> proc_macro2::TokenStream { 171 | match input { 172 | Data::Struct(data) => { 173 | let accumulate = data.fields.members().map(|field| { 174 | let field_str = match &field { 175 | syn::Member::Named(ident) => ident.to_string(), 176 | // TODO: Improve naming for unnamed fields 177 | syn::Member::Unnamed(index) => format!("{}", index.index), 178 | }; 179 | quote! { 180 | #companion_binding.#field.push_error_if_missing(#field_str); 181 | } 182 | }); 183 | quote! { 184 | #(#accumulate)* 185 | let __n_new_errors = ::eserde::reporter::ErrorReporter::n_errors(); 186 | if __n_new_errors > #n_errors { 187 | Err(()) 188 | } else { 189 | Ok(()) 190 | } 191 | } 192 | } 193 | Data::Enum(e) => { 194 | let variants = e.variants.iter().map(|variant| { 195 | let variant_ident = &variant.ident; 196 | 197 | if matches!(variant.fields, syn::Fields::Unit) { 198 | return quote! { 199 | #companion_type::#variant_ident => Ok(()) 200 | }; 201 | } 202 | let bindings: Vec<_> = variant 203 | .fields 204 | .members() 205 | .enumerate() 206 | .map(|(i, _)| format_ident!("__v{}", i)) 207 | .collect(); 208 | let destructure = 209 | variant 210 | .fields 211 | .members() 212 | .zip(bindings.iter()) 213 | .map(|(field, v)| { 214 | quote! { 215 | #field: #v 216 | } 217 | }); 218 | let accumulate = variant 219 | .fields 220 | .members() 221 | .zip(bindings.iter()) 222 | .map(|(field, v)| { 223 | let field_str = match &field { 224 | syn::Member::Named(ident) => ident.to_string(), 225 | // TODO: Improve naming for unnamed fields 226 | syn::Member::Unnamed(index) => format!("{}", index.index), 227 | }; 228 | quote! { 229 | #v.push_error_if_missing(#field_str); 230 | } 231 | }); 232 | quote! { 233 | #companion_type::#variant_ident { #(#destructure),* } => { 234 | #(#accumulate)* 235 | let __n_new_errors = ::eserde::reporter::ErrorReporter::n_errors(); 236 | if __n_new_errors > #n_errors { 237 | Err(()) 238 | } else { 239 | Ok(()) 240 | } 241 | } 242 | } 243 | }); 244 | quote! { 245 | match #companion_binding { 246 | #(#variants),* 247 | } 248 | } 249 | } 250 | Data::Union(_) => unreachable!(), 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /eserde_derive/src/filter_attributes.rs: -------------------------------------------------------------------------------- 1 | pub(crate) trait FilterAttributes { 2 | /// Remove all attributes that don't satisfy the predicate. 3 | fn filter_attributes(&self, filter: impl Fn(&syn::Attribute) -> bool) -> Self; 4 | } 5 | 6 | impl FilterAttributes for syn::DeriveInput { 7 | fn filter_attributes(&self, filter: impl Fn(&syn::Attribute) -> bool) -> Self { 8 | Self { 9 | vis: self.vis.clone(), 10 | ident: self.ident.clone(), 11 | generics: self.generics.clone(), 12 | attrs: self.attrs.filter_attributes(&filter), 13 | data: self.data.filter_attributes(filter), 14 | } 15 | } 16 | } 17 | 18 | impl FilterAttributes for syn::Data { 19 | fn filter_attributes(&self, filter: impl Fn(&syn::Attribute) -> bool) -> Self { 20 | match self { 21 | syn::Data::Struct(data) => syn::Data::Struct(data.filter_attributes(filter)), 22 | syn::Data::Enum(data) => syn::Data::Enum(data.filter_attributes(filter)), 23 | syn::Data::Union(_) => unimplemented!(), 24 | } 25 | } 26 | } 27 | 28 | impl FilterAttributes for syn::DataStruct { 29 | fn filter_attributes(&self, filter: impl Fn(&syn::Attribute) -> bool) -> Self { 30 | Self { 31 | fields: self.fields.filter_attributes(&filter), 32 | struct_token: self.struct_token, 33 | semi_token: self.semi_token, 34 | } 35 | } 36 | } 37 | 38 | impl FilterAttributes for syn::DataEnum { 39 | fn filter_attributes(&self, filter: impl Fn(&syn::Attribute) -> bool) -> Self { 40 | let variants = self 41 | .variants 42 | .iter() 43 | .map(|variant| variant.filter_attributes(&filter)) 44 | .collect(); 45 | Self { 46 | variants, 47 | enum_token: self.enum_token, 48 | brace_token: self.brace_token, 49 | } 50 | } 51 | } 52 | 53 | impl FilterAttributes for syn::Variant { 54 | fn filter_attributes(&self, filter: impl Fn(&syn::Attribute) -> bool) -> Self { 55 | syn::Variant { 56 | attrs: self.attrs.filter_attributes(&filter), 57 | fields: self.fields.filter_attributes(&filter), 58 | ..self.clone() 59 | } 60 | } 61 | } 62 | 63 | impl FilterAttributes for syn::Fields { 64 | fn filter_attributes(&self, filter: impl Fn(&syn::Attribute) -> bool) -> Self { 65 | match &self { 66 | syn::Fields::Named(fields) => syn::Fields::Named(fields.filter_attributes(filter)), 67 | syn::Fields::Unnamed(fields) => syn::Fields::Unnamed(fields.filter_attributes(filter)), 68 | syn::Fields::Unit => syn::Fields::Unit, 69 | } 70 | } 71 | } 72 | 73 | impl FilterAttributes for syn::FieldsNamed { 74 | fn filter_attributes(&self, filter: impl Fn(&syn::Attribute) -> bool) -> Self { 75 | let named = self 76 | .named 77 | .iter() 78 | .map(|field| syn::Field { 79 | attrs: field.attrs.filter_attributes(&filter), 80 | ..field.clone() 81 | }) 82 | .collect(); 83 | Self { 84 | named, 85 | brace_token: self.brace_token, 86 | } 87 | } 88 | } 89 | 90 | impl FilterAttributes for syn::FieldsUnnamed { 91 | fn filter_attributes(&self, filter: impl Fn(&syn::Attribute) -> bool) -> Self { 92 | let unnamed = self 93 | .unnamed 94 | .iter() 95 | .map(|field| syn::Field { 96 | attrs: field.attrs.filter_attributes(&filter), 97 | ..field.clone() 98 | }) 99 | .collect(); 100 | Self { 101 | unnamed, 102 | paren_token: self.paren_token, 103 | } 104 | } 105 | } 106 | 107 | impl FilterAttributes for Vec { 108 | fn filter_attributes(&self, filter: impl Fn(&syn::Attribute) -> bool) -> Self { 109 | self.iter().filter(|a| filter(a)).cloned().collect() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /eserde_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The `eserde_derive` crate provides procedural macros for deriving the `EDeserialize` trait 2 | //! from the [`eserde`](https://crates.io/crates/eserde) crate. 3 | //! 4 | //! You most likely don't want to use `eserde_derive` directly. Instead, use the `eserde` crate's 5 | //! `Deserialize` derive macro, which will automatically use the correct version of `eserde_derive` 6 | //! under the hood. 7 | use emit::{collect_missing_errors, initialize_from_shadow, ImplDeserGenerics}; 8 | use indexmap::IndexSet; 9 | use proc_macro::TokenStream; 10 | use quote::{format_ident, quote}; 11 | use syn::{parse_macro_input, DeriveInput}; 12 | use unsupported::reject_unsupported_inputs; 13 | 14 | mod attr; 15 | mod emit; 16 | mod filter_attributes; 17 | mod model; 18 | mod unsupported; 19 | 20 | #[proc_macro_derive(Deserialize, attributes(serde, eserde))] 21 | pub fn derive_deserialize(input: TokenStream) -> TokenStream { 22 | let input = parse_macro_input!(input as DeriveInput); 23 | 24 | if let Err(e) = reject_unsupported_inputs(&input) { 25 | return e.into_compile_error().into(); 26 | } 27 | 28 | let name = &input.ident; 29 | let shadow_type = model::ShadowType::new(format_ident!("__ImplDeserializeFor{}", name), &input); 30 | let shadow_type_ident = &shadow_type.0.ident; 31 | 32 | let shadow_binding = format_ident!("__shadowed"); 33 | let initialize_from_shadow = initialize_from_shadow( 34 | &input.data, 35 | &format_ident!("Self"), 36 | &shadow_binding, 37 | shadow_type_ident, 38 | ); 39 | 40 | let companion_type = 41 | model::PermissiveCompanionType::new(format_ident!("__ImplEDeserializeFor{}", name), &input); 42 | let companion_type_ident = &companion_type.ty_.ident; 43 | let companion_binding = format_ident!("__companion"); 44 | let deserializer_generic_ident = format_ident!("__D"); 45 | let n_errors = format_ident!("__n_errors"); 46 | let collect_missing_errors = collect_missing_errors( 47 | &input.data, 48 | companion_type_ident, 49 | &companion_binding, 50 | &n_errors, 51 | ); 52 | 53 | let deser_generics = ImplDeserGenerics::new(&input, &IndexSet::new()); 54 | let (impl_generics, ty_generics, where_clause) = deser_generics.split_for_impl(); 55 | 56 | let human_deser_generics = 57 | ImplDeserGenerics::new(&input, &companion_type.eserde_aware_generics); 58 | let (human_impl_generics, human_ty_generics, human_where_clause) = 59 | human_deser_generics.split_for_impl(); 60 | 61 | let expanded = quote! { 62 | const _: () = { 63 | #companion_type 64 | 65 | #shadow_type 66 | 67 | #[automatically_derived] 68 | impl #human_impl_generics ::eserde::EDeserialize<'de> for #name #human_ty_generics 69 | #human_where_clause 70 | { 71 | fn deserialize_for_errors<#deserializer_generic_ident>(__deserializer: #deserializer_generic_ident) -> Result<(), ()> 72 | where 73 | #deserializer_generic_ident: ::eserde::_serde::Deserializer<'de>, 74 | { 75 | let #n_errors = ::eserde::reporter::ErrorReporter::n_errors(); 76 | let #companion_binding = <#companion_type_ident #ty_generics as ::eserde::_serde::Deserialize>::deserialize(__deserializer) 77 | .map_err(::eserde::reporter::ErrorReporter::report)?; 78 | #collect_missing_errors 79 | } 80 | } 81 | 82 | #[automatically_derived] 83 | impl #impl_generics ::eserde::_serde::Deserialize<'de> for #name #ty_generics 84 | #where_clause 85 | { 86 | fn deserialize<#deserializer_generic_ident>(__deserializer: #deserializer_generic_ident) -> Result 87 | where 88 | #deserializer_generic_ident: ::eserde::_serde::Deserializer<'de>, 89 | { 90 | let #shadow_binding = #shadow_type_ident::deserialize(__deserializer)?; 91 | Ok(#initialize_from_shadow) 92 | } 93 | } 94 | }; 95 | }; 96 | 97 | TokenStream::from(expanded) 98 | } 99 | -------------------------------------------------------------------------------- /eserde_derive/src/model.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use crate::{ 4 | attr::{find_attr_meta, remove_attr_meta}, 5 | filter_attributes::FilterAttributes, 6 | }; 7 | use indexmap::IndexSet; 8 | use syn::{spanned::Spanned, DeriveInput}; 9 | 10 | /// A type with exactly the same set of fields/variants as the original type, but with a different name. 11 | /// This type is used to derive `Deserialize`, thus obtaining from `serde` the same deserialize implementation 12 | /// we would get for the original type had we annotated it with `#[derive(Deserialize)]` directly. 13 | pub struct ShadowType(pub DeriveInput); 14 | 15 | fn keep_serde_attributes(attr: &syn::Attribute) -> bool { 16 | attr.meta.path().is_ident("serde") 17 | } 18 | 19 | impl ShadowType { 20 | pub fn new(ident: syn::Ident, input: &syn::DeriveInput) -> Self { 21 | let shadow = DeriveInput { 22 | vis: syn::Visibility::Inherited, 23 | ident, 24 | // We don't want to keep _all_ attributes for the shadow type, only the `serde` ones 25 | // (e.g. `#[serde(default)]`), so we filter out the others. 26 | ..input.filter_attributes(|attr| attr.meta.path().is_ident("serde")) 27 | }; 28 | Self(shadow) 29 | } 30 | } 31 | 32 | /// A companion type that, unlike the original, uses `MaybeInvalidOrMissing` for all fields, where 33 | /// `T` is the original field type. 34 | /// This type should never fail to deserialize, thus allowing us to collect all errors in one go. 35 | pub struct PermissiveCompanionType { 36 | pub ty_: DeriveInput, 37 | /// Generic type parameters that must be constrained with `::eserde::EDeserialize` instead of `::serde::Deserialize`. 38 | pub eserde_aware_generics: IndexSet, 39 | /// Optional impl block; contains methods for `#[serde(deserialize_with)]` attributes. 40 | pub impl_: Option, 41 | } 42 | 43 | impl PermissiveCompanionType { 44 | pub fn new(ident: syn::Ident, input: &syn::DeriveInput) -> Self { 45 | let mut companion = DeriveInput { 46 | vis: syn::Visibility::Inherited, 47 | ident, 48 | generics: input.generics.clone(), 49 | ..input.filter_attributes(|attr| { 50 | attr.meta.path().is_ident("serde") || attr.meta.path().is_ident("eserde") 51 | }) 52 | }; 53 | 54 | // We keep track of the generic parameters that are used in the fields of the companion type 55 | // that have not been marked with `#[serde(compat)]`. We'll use this information to generate 56 | // the correct `#[serde(bound)]` attributes for them. 57 | let generic_params: HashSet = companion 58 | .generics 59 | .type_params() 60 | .map(|param| param.ident.clone()) 61 | .collect(); 62 | let mut eserde_aware_generics = IndexSet::new(); 63 | 64 | let mut impl_items: Vec = Vec::new(); 65 | 66 | let mut modify_field_types = |fields: &mut syn::Fields| { 67 | for (i, field) in fields.iter_mut().enumerate() { 68 | let span = field.span(); 69 | 70 | // Process all `eserde` attributes, then remove them since 71 | // they are not valid `serde` attributes. 72 | let is_eserde_compatible = 73 | find_attr_meta(&field.attrs, "eserde", "compat").is_none(); 74 | field.attrs.retain(keep_serde_attributes); 75 | 76 | if is_eserde_compatible { 77 | collect_generic_type_params( 78 | &field.ty, 79 | &mut eserde_aware_generics, 80 | &generic_params, 81 | ); 82 | } 83 | 84 | // If `&str` or `&[u8]` are used, we need to add a `#[serde(bound)]` attribute 85 | // on the wrapped field to make sure `serde` applies the right lifetime constraints. 86 | if let syn::Type::Reference(ref_) = &field.ty { 87 | let mut add_borrow_attr = false; 88 | if let syn::Type::Path(path) = &*ref_.elem { 89 | if path.path.is_ident("str") { 90 | add_borrow_attr = true; 91 | } 92 | } 93 | 94 | if let syn::Type::Slice(slice) = &*ref_.elem { 95 | if let syn::Type::Path(path) = &*slice.elem { 96 | if path.path.is_ident("u8") { 97 | add_borrow_attr = true; 98 | } 99 | } 100 | } 101 | if add_borrow_attr && find_attr_meta(&field.attrs, "serde", "borrow").is_none() 102 | { 103 | field 104 | .attrs 105 | .push(syn::parse_quote_spanned!(span=> #[serde(borrow)])); 106 | } 107 | } 108 | 109 | // Remove any `#[serde(default = "..")]` on the field, and re-add it without any custom value. 110 | let has_default = remove_attr_meta(&mut field.attrs, "serde", "default").is_some(); 111 | field 112 | .attrs 113 | .push(syn::parse_quote_spanned!(span=> #[serde(default)])); 114 | 115 | let field_ty = &field.ty; 116 | let wrapper_ty = if has_default { 117 | syn::parse_quote_spanned!(field_ty.span()=> ::eserde::_macro_impl::MaybeInvalid::<#field_ty>) 118 | } else { 119 | syn::parse_quote_spanned!(field_ty.span()=> ::eserde::_macro_impl::MaybeInvalidOrMissing::<#field_ty>) 120 | }; 121 | 122 | if is_eserde_compatible { 123 | // Add or replace `#[serde(deserialize_with = "..")]` for our wrapper. 124 | 125 | // Handle user `#[serde(deserialize_with = "..")]` or `#[serde(with = "..')]` attributes. 126 | let dewith_path = 127 | // Remove `#[serde(deserialize_with = "..")]` and get the string value. 128 | remove_attr_meta(&mut field.attrs, "serde", "deserialize_with") 129 | .and_then(|meta_item| meta_str_value(&meta_item)) 130 | // Or else remove `#[serde(with = "..")]` and get the string value with `"::deserialize"` appended. 131 | .or_else(|| remove_attr_meta(&mut field.attrs, "serde", "with") 132 | .and_then(|meta_item| meta_str_value(&meta_item)) 133 | .map(|s| format!("{}::deserialize", s)) 134 | ) 135 | // Parse the string as a path. 136 | .and_then(|s| syn::parse_str::(&s).ok()); 137 | 138 | let attr = if let Some(dewith_path) = dewith_path { 139 | // User specified a custom `deserialize_with` function. 140 | // We need to wrap it in our own function. 141 | 142 | let fn_name = format!( 143 | "__eserde_deserialize_with_{}", 144 | field 145 | .ident 146 | .as_ref() 147 | .map(|ident| ident.to_string()) 148 | .unwrap_or_else(|| i.to_string()), 149 | ); 150 | let fn_ident = syn::Ident::new(&fn_name, field.span()); 151 | 152 | // Add the method to `deserialize_withs`. 153 | impl_items.push(syn::parse_quote_spanned! {field.span()=> 154 | fn #fn_ident<'de, D>(deserializer: D) -> ::core::result::Result<#wrapper_ty, D::Error> 155 | where 156 | D: ::eserde::_serde::Deserializer<'de>, 157 | { 158 | let result: ::core::result::Result<#field_ty, D::Error> = (#dewith_path)(deserializer); 159 | let value = match result { 160 | Ok(_) => #wrapper_ty::Valid(::core::marker::PhantomData), 161 | Err(e) => { 162 | ::eserde::reporter::ErrorReporter::report(e); 163 | #wrapper_ty::Invalid 164 | } 165 | }; 166 | Ok(value) 167 | } 168 | }); 169 | 170 | // `"self::fn_name"` 171 | let new_path = syn::LitStr::new( 172 | &format!("{}::{}", companion.ident, fn_name), 173 | field.span(), 174 | ); 175 | syn::parse_quote!(#[serde(deserialize_with = #new_path)]) 176 | } else if has_default { 177 | syn::parse_quote_spanned!(span=> #[serde(deserialize_with = "::eserde::_macro_impl::maybe_invalid")]) 178 | } else { 179 | syn::parse_quote_spanned!(span=> #[serde(deserialize_with = "::eserde::_macro_impl::maybe_invalid_or_missing")]) 180 | }; 181 | field.attrs.push(attr); 182 | } 183 | 184 | // Done last for ownership. 185 | field.ty = wrapper_ty; 186 | } 187 | }; 188 | 189 | match &mut companion.data { 190 | syn::Data::Struct(data_struct) => { 191 | (modify_field_types)(&mut data_struct.fields); 192 | } 193 | syn::Data::Enum(data_enum) => { 194 | data_enum 195 | .variants 196 | .iter_mut() 197 | .for_each(|variant| (modify_field_types)(&mut variant.fields)); 198 | } 199 | syn::Data::Union(_) => unreachable!(), 200 | }; 201 | 202 | let bounds: Vec = companion 203 | .generics 204 | .type_params() 205 | // `serde` will infer that the type parameters of the companion type must implement 206 | // the `Default` trait, on top of the `Deserialize` trait, since we marked fields 207 | // that use those type parameters with `#[serde(default)]`. 208 | // That's unnecessary, so we override the bounds here using `#[serde(bound(deserialize = "..."))]`. 209 | .map(|param| { 210 | if eserde_aware_generics.contains(¶m.ident) { 211 | format!("{}: ::eserde::EDeserialize<'de>", param.ident) 212 | } else { 213 | format!("{}: ::eserde::_serde::Deserialize<'de>", param.ident) 214 | } 215 | }) 216 | .collect::>(); 217 | if !bounds.is_empty() { 218 | let bound = bounds.join(", "); 219 | // TODO: when we start supporting `serde(bound = "...")`, we'll have to 220 | // concatenate the new bound with the existing ones otherwise `serde` 221 | // will complain about duplicate attributes. 222 | companion 223 | .attrs 224 | .push(syn::parse_quote!(#[serde(bound(deserialize = #bound))])); 225 | } 226 | 227 | // Impl block, if needed. 228 | let impl_ = if impl_items.is_empty() { 229 | None 230 | } else { 231 | let name = &companion.ident; 232 | let (impl_generics, ty_generics, where_clause) = companion.generics.split_for_impl(); 233 | Some(syn::parse_quote! { 234 | impl #impl_generics #name #ty_generics #where_clause { 235 | #(#impl_items)* 236 | } 237 | }) 238 | }; 239 | 240 | Self { 241 | ty_: companion, 242 | eserde_aware_generics, 243 | impl_, 244 | } 245 | } 246 | } 247 | 248 | fn collect_generic_type_params( 249 | ty_: &syn::Type, 250 | set: &mut IndexSet, 251 | generic_params: &HashSet, 252 | ) { 253 | match ty_ { 254 | syn::Type::Path(path) => { 255 | // Generic type parameters are represented as single-segment paths. 256 | if let Some(ident) = path.path.get_ident() { 257 | if generic_params.contains(ident) { 258 | set.insert(ident.clone()); 259 | } 260 | } else { 261 | for seg in &path.path.segments { 262 | if let syn::PathArguments::AngleBracketed(args) = &seg.arguments { 263 | for arg in &args.args { 264 | if let syn::GenericArgument::Type(ty) = arg { 265 | collect_generic_type_params(ty, set, generic_params); 266 | } 267 | } 268 | } 269 | } 270 | } 271 | } 272 | syn::Type::Reference(ref_) => { 273 | collect_generic_type_params(&ref_.elem, set, generic_params); 274 | } 275 | syn::Type::Slice(slice) => { 276 | collect_generic_type_params(&slice.elem, set, generic_params); 277 | } 278 | syn::Type::Array(type_array) => { 279 | collect_generic_type_params(&type_array.elem, set, generic_params); 280 | } 281 | syn::Type::TraitObject(_) => {} 282 | syn::Type::Tuple(type_tuple) => { 283 | for elem in &type_tuple.elems { 284 | collect_generic_type_params(elem, set, generic_params); 285 | } 286 | } 287 | t => { 288 | unimplemented!("{:?}", t); 289 | } 290 | } 291 | } 292 | 293 | /// If the `MetaItem` has a string literal value, return it as `Some(String)`, otherwise return `None`. 294 | fn meta_str_value(meta: &crate::attr::MetaItem) -> Option { 295 | let (_eq, expr) = meta.value.as_ref()?; 296 | if let syn::Expr::Lit(syn::ExprLit { 297 | attrs: _, 298 | lit: syn::Lit::Str(lit_str), 299 | }) = expr 300 | { 301 | Some(lit_str.value()) 302 | } else { 303 | None 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /eserde_derive/src/unsupported.rs: -------------------------------------------------------------------------------- 1 | use syn::DeriveInput; 2 | 3 | use crate::{attr::find_attr_meta, filter_attributes::FilterAttributes}; 4 | 5 | /// Return a compiler error if the input contains data types or 6 | /// `serde` attributes that are not supported by our custom derive. 7 | pub fn reject_unsupported_inputs(input: &DeriveInput) -> Result<(), syn::Error> { 8 | let mut errors = Vec::new(); 9 | 10 | let input = input.filter_attributes(|a| a.meta.path().is_ident("serde")); 11 | reject_container_attributes(&mut errors, &input.attrs); 12 | 13 | match &input.data { 14 | syn::Data::Struct(data_struct) => { 15 | data_struct.fields.iter().for_each(|field| { 16 | reject_field_attributes(&mut errors, field); 17 | }); 18 | } 19 | syn::Data::Enum(data_enum) => { 20 | data_enum.variants.iter().for_each(|variant| { 21 | reject_variant_attributes(&mut errors, variant); 22 | 23 | variant.fields.iter().for_each(|field| { 24 | reject_field_attributes(&mut errors, field); 25 | }); 26 | }); 27 | } 28 | syn::Data::Union(_) => { 29 | errors.push(syn::Error::new_spanned(&input, "Unions are not supported")); 30 | } 31 | } 32 | 33 | if let Some(error) = errors.into_iter().reduce(|mut a, b| { 34 | a.combine(b); 35 | a 36 | }) { 37 | Err(error) 38 | } else { 39 | Ok(()) 40 | } 41 | } 42 | 43 | /// Attributes from that we either 44 | /// can't support or haven't implemented yet. 45 | fn reject_container_attributes(errors: &mut Vec, attrs: &[syn::Attribute]) { 46 | // We can't support `#[serde(untagged)]` because our permissive deserialization 47 | // strategy conflicts with the "try-until-it-succeeds" mechanism used by 48 | // `untagged` deserialization to find the correct variant. 49 | if let Some(meta_item) = find_attr_meta(attrs, "serde", "untagged") { 50 | errors.push(syn::Error::new_spanned( 51 | meta_item, 52 | "`eserde::Deserialize` can't be derived for enums that use the untagged representation. \ 53 | Use a plain `#[derive(serde::Deserialize)]` instead.", 54 | )); 55 | } 56 | 57 | for (path, example, additional) in [ 58 | ( 59 | "default", 60 | "`#[serde(default)]`", 61 | " It is only supported on fields.", 62 | ), 63 | ( 64 | "remote", 65 | "`#[serde(remote = \"..\")]`", 66 | " It can only be derived for local types.", 67 | ), 68 | ("try_from", "`#[serde(try_from = \"..\")]`", ""), 69 | ("from", "`#[serde(from = \"..\")]`", ""), 70 | ("bound", "`#[serde(bound = \"..\")]`", ""), 71 | ("variant_identifier", "`#[serde(variant_identifier)]`", ""), 72 | ("field_identifier", "`#[serde(field_identifier)]`", ""), 73 | ] { 74 | if let Some(meta_item) = find_attr_meta(attrs, "serde", path) { 75 | errors.push(syn::Error::new_spanned( 76 | meta_item, 77 | format!("`eserde::Deserialize` doesn't yet support the {example} attribute.{additional}", 78 | ))); 79 | } 80 | } 81 | } 82 | 83 | /// Attributes from that we either 84 | /// can't support or haven't implemented yet. 85 | fn reject_field_attributes(errors: &mut Vec, field: &syn::Field) { 86 | for (path, example) in [ 87 | ("skip_deserializing", "`#[serde(skip_deserializing)]`"), 88 | ("bound", "`#[serde(bound = \"..\")]`"), 89 | ] { 90 | if let Some(meta_item) = find_attr_meta(&field.attrs, "serde", path) { 91 | errors.push(syn::Error::new_spanned( 92 | meta_item, 93 | format!( 94 | "`eserde::Deserialize` doesn't yet support the {example} attribute on fields." 95 | ), 96 | )); 97 | } 98 | } 99 | } 100 | 101 | /// Attributes from that we either 102 | /// can't support or haven't implemented yet. 103 | fn reject_variant_attributes(errors: &mut Vec, variant: &syn::Variant) { 104 | for (path, example) in [ 105 | ("skip_deserializing", "`#[serde(skip_deserializing)]`"), 106 | ("deserialize_with", "`#[serde(deserialize_with = \"..\")]`"), 107 | ("with", "`#[serde(with = \"..\")]`"), 108 | ("bound", "`#[serde(bound = \"..\")]`"), 109 | ("untagged", "`#[serde(untagged)]`"), 110 | ] { 111 | if let Some(meta_item) = find_attr_meta(&variant.attrs, "serde", path) { 112 | errors.push(syn::Error::new_spanned( 113 | meta_item, 114 | format!( 115 | "`eserde::Deserialize` doesn't yet support the {example} attribute on enum variants." 116 | ), 117 | )); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | coverage 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "eserde-fuzz" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [package.metadata] 8 | cargo-fuzz = true 9 | 10 | [dependencies] 11 | arbitrary = { workspace = true, features = ["derive"] } 12 | libfuzzer-sys = { workspace = true } 13 | eserde = { workspace = true, features = ["json"] } 14 | eserde_test_helper = { workspace = true } 15 | insta = { workspace = true } 16 | itertools = { workspace = true } 17 | serde_path_to_error = { workspace = true } 18 | serde_json = { workspace = true } 19 | serde = { workspace = true, features = ["derive"] } 20 | 21 | 22 | [[bin]] 23 | name = "contract" 24 | path = "fuzz_targets/contract.rs" 25 | test = false 26 | doc = false 27 | bench = false 28 | 29 | [[bin]] 30 | name = "contract_json" 31 | path = "fuzz_targets/contract_json.rs" 32 | test = false 33 | doc = false 34 | bench = false 35 | 36 | [[bin]] 37 | name = "enum_repr" 38 | path = "fuzz_targets/enum_repr.rs" 39 | test = false 40 | doc = false 41 | bench = false 42 | 43 | [[bin]] 44 | name = "enum_repr_json" 45 | path = "fuzz_targets/enum_repr_json.rs" 46 | test = false 47 | doc = false 48 | bench = false 49 | 50 | [[bin]] 51 | name = "enums" 52 | path = "fuzz_targets/enums.rs" 53 | test = false 54 | doc = false 55 | bench = false 56 | 57 | [[bin]] 58 | name = "enums_json" 59 | path = "fuzz_targets/enums_json.rs" 60 | test = false 61 | doc = false 62 | bench = false 63 | 64 | [[bin]] 65 | name = "enums_deny_unknown_fields" 66 | path = "fuzz_targets/enums_deny_unknown_fields.rs" 67 | test = false 68 | doc = false 69 | bench = false 70 | 71 | [[bin]] 72 | name = "enums_deny_unknown_fields_json" 73 | path = "fuzz_targets/enums_deny_unknown_fields_json.rs" 74 | test = false 75 | doc = false 76 | bench = false 77 | 78 | [[bin]] 79 | name = "enums_flattened" 80 | path = "fuzz_targets/enums_flattened.rs" 81 | test = false 82 | doc = false 83 | bench = false 84 | 85 | [[bin]] 86 | name = "enums_flattened_json" 87 | path = "fuzz_targets/enums_flattened_json.rs" 88 | test = false 89 | doc = false 90 | bench = false 91 | 92 | [[bin]] 93 | name = "extra" 94 | path = "fuzz_targets/extra.rs" 95 | test = false 96 | doc = false 97 | bench = false 98 | 99 | [[bin]] 100 | name = "extra_json" 101 | path = "fuzz_targets/extra_json.rs" 102 | test = false 103 | doc = false 104 | bench = false 105 | 106 | 107 | [[bin]] 108 | name = "flatten" 109 | path = "fuzz_targets/flatten.rs" 110 | test = false 111 | doc = false 112 | bench = false 113 | 114 | [[bin]] 115 | name = "flatten_json" 116 | path = "fuzz_targets/flatten_json.rs" 117 | test = false 118 | doc = false 119 | bench = false 120 | 121 | [[bin]] 122 | name = "structs" 123 | path = "fuzz_targets/structs.rs" 124 | test = false 125 | doc = false 126 | bench = false 127 | 128 | [[bin]] 129 | name = "structs_json" 130 | path = "fuzz_targets/structs_json.rs" 131 | test = false 132 | doc = false 133 | bench = false 134 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/contract.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::contract::*; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: &str| { 9 | fuzz_many!( 10 | s, 11 | StructDenyUnknownFields, 12 | StructAllowUnknownFields, 13 | TupleStruct, 14 | ExternalEnum, 15 | InternalEnum, 16 | AdjacentEnum, 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/contract_json.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::{contract::*, json::JsonValue}; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: JsonValue| { 9 | let s = serde_json::to_string(&s).unwrap(); 10 | fuzz_many!( 11 | &s, 12 | StructDenyUnknownFields, 13 | StructAllowUnknownFields, 14 | TupleStruct, 15 | ExternalEnum, 16 | InternalEnum, 17 | AdjacentEnum, 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/enum_repr.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::enums::*; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: &str| { 9 | fuzz_many!( 10 | s, 11 | Struct, 12 | External, 13 | Internal, 14 | Adjacent, 15 | UntaggedWrapper, 16 | Renamed 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/enum_repr_json.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::{enum_repr::*, json::JsonValue}; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: JsonValue| { 9 | let s = serde_json::to_string(&s).unwrap(); 10 | fuzz_many!(&s, Enum); 11 | }); 12 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/enums.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::enum_repr::*; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: &str| { 9 | fuzz_many!(s, Enum); 10 | }); 11 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/enums_deny_unknown_fields.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::enums_deny_unknown_fields::*; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: &str| { 9 | fuzz_many!( 10 | s, 11 | Struct, 12 | StructDenyUnknownFields, 13 | External, 14 | Internal, 15 | Adjacent 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/enums_deny_unknown_fields_json.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::{enums_deny_unknown_fields::*, json::JsonValue}; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: JsonValue| { 9 | let s = serde_json::to_string(&s).unwrap(); 10 | fuzz_many!( 11 | &s, 12 | Struct, 13 | StructDenyUnknownFields, 14 | External, 15 | Internal, 16 | Adjacent 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/enums_flattened.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::enums_flattened::*; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: &str| { 9 | fuzz_many!(s, Container, ContainerDenyUnknownFields); 10 | }); 11 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/enums_flattened_json.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::{enums_flattened::*, json::JsonValue}; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: JsonValue| { 9 | let s = serde_json::to_string(&s).unwrap(); 10 | fuzz_many!(&s, Container, ContainerDenyUnknownFields); 11 | }); 12 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/enums_json.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::{enums::*, json::JsonValue}; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: JsonValue| { 9 | let s = serde_json::to_string(&s).unwrap(); 10 | fuzz_many!( 11 | &s, 12 | Struct, 13 | External, 14 | Internal, 15 | Adjacent, 16 | UntaggedWrapper, 17 | Renamed 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/extra.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::extra::*; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: &str| { 9 | fuzz_many!(s, NamedStruct, 10 | GenericStruct, 11 | TupleStructOneField, 12 | TupleStructOneField, 13 | TupleStructMultipleFields, 14 | CLikeEnumOneVariant, 15 | EnumWithBothNamedAndTupleVariants, 16 | ); 17 | 18 | let _ = eserde::json::from_str::(s); 19 | }); 20 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/extra_json.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::{extra::*, json::JsonValue}; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: JsonValue| { 9 | let s = serde_json::to_string(&s).unwrap(); 10 | fuzz_many!( 11 | &s, 12 | GenericStruct, 13 | TupleStructOneField, 14 | TupleStructOneField, 15 | TupleStructMultipleFields, 16 | CLikeEnumOneVariant, 17 | EnumWithBothNamedAndTupleVariants, 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/flatten.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::flatten::*; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: &str| { 9 | fuzz_many!(&s, Deep1, FlattenValue, FlattenMap,); 10 | }); 11 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/flatten_json.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::{flatten::*, json::JsonValue}; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: JsonValue| { 9 | let s = serde_json::to_string(&s).unwrap(); 10 | fuzz_many!(&s, Deep1, FlattenValue, FlattenMap,); 11 | }); 12 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/structs.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::structs::*; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: &str| { 9 | fuzz_many!( 10 | s, 11 | UnitStruct, 12 | NormalStruct, 13 | NewType, 14 | TupleStruct, 15 | RenamedFields, 16 | DenyUnknownFields 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/structs_json.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![allow(dead_code)] 3 | 4 | use eserde_fuzz::fuzz_many; 5 | use eserde_test_helper::{json::JsonValue, structs::*}; 6 | use libfuzzer_sys::fuzz_target; 7 | 8 | fuzz_target!(|s: JsonValue| { 9 | let s = serde_json::to_string(&s).unwrap(); 10 | fuzz_many!( 11 | &s, 12 | UnitStruct, 13 | NormalStruct, 14 | NewType, 15 | TupleStruct, 16 | RenamedFields, 17 | DenyUnknownFields 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /fuzz/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! fuzz_many { 3 | ($s:expr, $($t:ty),+$(,)?) => { 4 | $({ 5 | let _ = eserde_test_helper::test_helper::TestHelper::<$t>::new_serialized($s).from_json(); 6 | })+ 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /test_helper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "eserde_test_helper" 3 | edition.workspace = true 4 | repository.workspace = true 5 | license.workspace = true 6 | version.workspace = true 7 | publish = false 8 | 9 | [dependencies] 10 | arbitrary = { workspace = true, features = ["derive"] } 11 | eserde = { workspace = true, features = ["json"] } 12 | fake = { version = "4", features = ["derive", "chrono", "serde_json"] } 13 | insta = { workspace = true } 14 | serde = { workspace = true } 15 | serde_json = { workspace = true } 16 | serde_repr = { version = "0.1.19", default-features = false } 17 | rand = { version = "0.9", default-features = false } 18 | -------------------------------------------------------------------------------- /test_helper/src/contract.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 4 | #[serde(rename_all = "SCREAMING-KEBAB-CASE", deny_unknown_fields)] 5 | pub struct StructDenyUnknownFields { 6 | #[allow(dead_code)] 7 | #[serde(skip_serializing)] 8 | write_only: bool, 9 | #[serde(default)] 10 | default: bool, 11 | #[serde(skip_serializing_if = "core::ops::Not::not")] 12 | skip_serializing_if: bool, 13 | #[serde(rename = "renamed")] 14 | renamed: bool, 15 | option: Option, 16 | } 17 | 18 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 19 | pub struct StructAllowUnknownFields { 20 | #[serde(flatten)] 21 | inner: StructDenyUnknownFields, 22 | } 23 | 24 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 25 | pub struct TupleStruct( 26 | String, 27 | #[allow(dead_code)] 28 | #[serde(skip)] 29 | bool, 30 | String, 31 | String, 32 | ); 33 | 34 | #[allow(dead_code)] 35 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 36 | #[serde(rename_all = "SCREAMING-KEBAB-CASE", rename_all_fields = "PascalCase")] 37 | pub enum ExternalEnum { 38 | #[serde(skip_serializing)] 39 | WriteOnlyStruct { i: isize }, 40 | #[serde(rename = "renamed_unit")] 41 | RenamedUnit, 42 | #[serde(rename = "renamed_struct")] 43 | RenamedStruct { b: bool }, 44 | } 45 | 46 | #[allow(dead_code)] 47 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 48 | #[serde( 49 | tag = "tag", 50 | rename_all = "SCREAMING-KEBAB-CASE", 51 | rename_all_fields = "PascalCase" 52 | )] 53 | pub enum InternalEnum { 54 | #[serde(skip_serializing)] 55 | WriteOnlyStruct { i: isize }, 56 | #[serde(rename = "renamed_unit")] 57 | RenamedUnit, 58 | #[serde(rename = "renamed_struct")] 59 | RenamedStruct { b: bool }, 60 | } 61 | 62 | #[allow(dead_code)] 63 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 64 | #[serde( 65 | tag = "tag", 66 | content = "content", 67 | rename_all = "SCREAMING-KEBAB-CASE", 68 | rename_all_fields = "PascalCase" 69 | )] 70 | pub enum AdjacentEnum { 71 | #[serde(skip_serializing)] 72 | WriteOnlyStruct { i: isize }, 73 | #[serde(rename = "renamed_unit")] 74 | RenamedUnit, 75 | #[serde(rename = "renamed_struct")] 76 | RenamedStruct { b: bool }, 77 | } 78 | -------------------------------------------------------------------------------- /test_helper/src/enum_repr.rs: -------------------------------------------------------------------------------- 1 | use serde_repr::{Deserialize_repr, Serialize_repr}; 2 | 3 | use crate::prelude::*; 4 | 5 | #[derive(Deserialize_repr, Serialize_repr, Debug, Dummy, Arbitrary)] 6 | #[repr(u8)] 7 | #[serde(rename = "EnumWithReprAttr")] 8 | /// Description from comment 9 | pub enum EnumWithReprAttr { 10 | Zero, 11 | One, 12 | Five = 5, 13 | Six, 14 | Three = 3, 15 | } 16 | 17 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 18 | pub struct Enum(#[eserde(compat)] EnumWithReprAttr); 19 | -------------------------------------------------------------------------------- /test_helper/src/enums.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use crate::prelude::*; 4 | 5 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 6 | pub struct UnitStruct; 7 | 8 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary, Default)] 9 | pub struct Struct { 10 | foo: i32, 11 | bar: bool, 12 | } 13 | 14 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 15 | #[serde(rename_all = "camelCase")] 16 | pub enum External { 17 | UnitOne, 18 | StringMap(BTreeMap), 19 | UnitStructNewType(UnitStruct), 20 | StructNewType(Struct), 21 | Struct { foo: i32, bar: bool }, 22 | Tuple(i32, bool), 23 | UnitTwo, 24 | } 25 | 26 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 27 | #[serde(tag = "tag")] 28 | pub enum Internal { 29 | UnitOne, 30 | StringMap(BTreeMap), 31 | UnitStructNewType(UnitStruct), 32 | StructNewType(Struct), 33 | Struct { foo: i32, bar: bool }, 34 | UnitTwo, 35 | } 36 | 37 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 38 | #[serde(tag = "tag", content = "content")] 39 | pub enum Adjacent { 40 | UnitOne, 41 | StringMap(BTreeMap), 42 | UnitStructNewType(UnitStruct), 43 | StructNewType(Struct), 44 | Struct { foo: i32, bar: bool }, 45 | Tuple(i32, bool), 46 | UnitTwo, 47 | } 48 | 49 | #[derive(serde::Deserialize, Serialize, Dummy, Debug, Arbitrary)] 50 | #[serde(untagged)] 51 | pub enum Untagged { 52 | UnitOne, 53 | StringMap(BTreeMap), 54 | UnitStructNewType(UnitStruct), 55 | StructNewType(Struct), 56 | Struct { foo: i32, bar: bool }, 57 | Tuple(i32, bool), 58 | UnitTwo, 59 | } 60 | 61 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 62 | pub struct UntaggedWrapper(#[eserde(compat)] Untagged); 63 | 64 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 65 | #[serde(rename_all_fields = "UPPERCASE", rename_all = "snake_case")] 66 | pub enum Renamed { 67 | StructVariant { 68 | field: String, 69 | }, 70 | #[serde(rename = "custom name variant")] 71 | RenamedStructVariant { 72 | #[serde(rename = "custom name field")] 73 | field: String, 74 | }, 75 | } 76 | -------------------------------------------------------------------------------- /test_helper/src/enums_deny_unknown_fields.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::collections::BTreeMap; 3 | 4 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 5 | pub struct Struct { 6 | foo: i32, 7 | bar: bool, 8 | } 9 | 10 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 11 | #[serde(deny_unknown_fields)] 12 | pub struct StructDenyUnknownFields { 13 | baz: i32, 14 | foobar: bool, 15 | } 16 | 17 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 18 | #[serde(deny_unknown_fields)] 19 | pub enum External { 20 | Unit, 21 | StringMap(BTreeMap), 22 | StructNewType(Struct), 23 | StructDenyUnknownFieldsNewType(StructDenyUnknownFields), 24 | Struct { foo: i32, bar: bool }, 25 | } 26 | 27 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 28 | #[serde(tag = "tag", deny_unknown_fields)] 29 | pub enum Internal { 30 | Unit, 31 | StringMap(BTreeMap), 32 | StructNewType(Struct), 33 | StructDenyUnknownFieldsNewType(StructDenyUnknownFields), 34 | Struct { foo: i32, bar: bool }, 35 | } 36 | 37 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 38 | #[serde(tag = "tag", content = "content", deny_unknown_fields)] 39 | pub enum Adjacent { 40 | Unit, 41 | StringMap(BTreeMap), 42 | StructNewType(Struct), 43 | StructDenyUnknownFieldsNewType(StructDenyUnknownFields), 44 | Struct { foo: i32, bar: bool }, 45 | } 46 | -------------------------------------------------------------------------------- /test_helper/src/enums_flattened.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 4 | pub enum Enum1 { 5 | B(bool), 6 | S(String), 7 | } 8 | 9 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 10 | pub enum Enum2 { 11 | U(u32), 12 | F(f64), 13 | } 14 | 15 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 16 | pub enum Enum3 { 17 | B2(bool), 18 | S2(String), 19 | } 20 | 21 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 22 | pub enum Enum4 { 23 | U2(u32), 24 | F2(f64), 25 | } 26 | 27 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 28 | pub enum Enum5 { 29 | B3(bool), 30 | S3(String), 31 | } 32 | 33 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 34 | pub struct Container { 35 | f: f32, 36 | #[serde(flatten)] 37 | e1: Enum1, 38 | #[serde(flatten)] 39 | e2: Enum2, 40 | #[serde(flatten)] 41 | e3: Enum3, 42 | #[serde(flatten)] 43 | e4: Enum4, 44 | #[serde(flatten)] 45 | e5: Enum5, 46 | } 47 | 48 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 49 | #[serde(deny_unknown_fields)] 50 | pub struct ContainerDenyUnknownFields { 51 | f: f32, 52 | #[serde(flatten)] 53 | e1: Enum1, 54 | #[serde(flatten)] 55 | e2: Enum2, 56 | #[serde(flatten)] 57 | e3: Enum3, 58 | #[serde(flatten)] 59 | e4: Enum4, 60 | #[serde(flatten)] 61 | e5: Enum5, 62 | } 63 | -------------------------------------------------------------------------------- /test_helper/src/extra.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use std::borrow::Cow; 3 | 4 | #[derive(Debug, Serialize, Deserialize, Arbitrary)] 5 | pub struct NamedStruct { 6 | #[serde(default)] 7 | a: Option, 8 | b: TupleStructOneField, 9 | c: Vec, 10 | } 11 | 12 | #[derive(Debug, Serialize, Deserialize, Arbitrary)] 13 | pub struct GenericStruct { 14 | // #[eserde(compat)] 15 | a: T, 16 | #[eserde(compat)] 17 | b: S, 18 | } 19 | 20 | #[derive(Debug, Serialize, Deserialize, Arbitrary)] 21 | pub struct LifetimeGenericStruct<'a, 'b, 'c, 'd, 'e: 'a> { 22 | #[serde(borrow)] 23 | a: Cow<'a, str>, 24 | // `&str` and `&[u8]` are special-cased by `serde` 25 | // and treated as if `#[serde(borrow)]` was applied. 26 | b: &'b str, 27 | c: &'c [u8], 28 | d: Cow<'d, str>, 29 | // Check that we don't add `borrow` twice, angering `serde` 30 | #[serde(borrow)] 31 | e: &'e str, 32 | } 33 | 34 | #[derive(Debug, Serialize, Deserialize, Arbitrary)] 35 | pub struct TupleStructOneField(#[serde(default)] Option); 36 | 37 | #[derive(Debug, Serialize, Deserialize, Arbitrary)] 38 | pub struct TupleStructMultipleFields(Option, u32, #[serde(default)] u64); 39 | 40 | #[derive(Debug, Serialize, Deserialize, Arbitrary)] 41 | pub enum CLikeEnumOneVariant { 42 | A, 43 | } 44 | 45 | #[derive(Debug, Serialize, Deserialize, Arbitrary)] 46 | pub enum CLikeEnumMultipleVariants { 47 | A, 48 | B, 49 | } 50 | 51 | #[derive(Debug, Serialize, Deserialize, Arbitrary)] 52 | pub enum EnumWithBothNamedAndTupleVariants { 53 | Named { a: u32 }, 54 | NamedMultiple { a: u32, b: u64 }, 55 | Tuple(u32), 56 | TupleMultiple(u32, u64), 57 | Unit, 58 | } 59 | -------------------------------------------------------------------------------- /test_helper/src/flatten.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use crate::prelude::*; 4 | 5 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 6 | pub struct Flat { 7 | f: f32, 8 | b: bool, 9 | #[serde(default, skip_serializing_if = "str::is_empty")] 10 | s: String, 11 | v: Vec, 12 | } 13 | 14 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 15 | pub struct Deep1 { 16 | f: f32, 17 | #[serde(flatten)] 18 | deep2: Deep2, 19 | v: Vec, 20 | } 21 | 22 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 23 | pub struct Deep2 { 24 | b: bool, 25 | #[serde(flatten, skip_serializing_if = "Option::is_none")] 26 | deep3: Option, 27 | } 28 | 29 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 30 | pub struct Deep3 { 31 | s: String, 32 | } 33 | 34 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 35 | pub struct FlattenValue { 36 | flag: bool, 37 | #[serde(flatten)] 38 | value: JsonValue, 39 | } 40 | 41 | #[derive(Deserialize, Serialize, Dummy, Debug, Arbitrary)] 42 | pub struct FlattenMap { 43 | flag: bool, 44 | #[serde(flatten)] 45 | value: BTreeMap, 46 | } 47 | -------------------------------------------------------------------------------- /test_helper/src/json.rs: -------------------------------------------------------------------------------- 1 | use arbitrary::Arbitrary; 2 | use fake::{Fake, Faker}; 3 | 4 | /// Wrapper around [`serde_json::Value`] that implements the traits needed 5 | /// for use in tests and fuzz targets. 6 | #[derive(crate::prelude::Deserialize, crate::prelude::Serialize, std::fmt::Debug, fake::Dummy)] 7 | pub struct JsonValue(#[eserde(compat)] serde_json::Value); 8 | 9 | impl<'a> arbitrary::Arbitrary<'a> for JsonValue { 10 | fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { 11 | let value = match u8::arbitrary(u)? % 5 { 12 | 0 => serde_json::Value::Null, 13 | 1 => serde_json::Value::Bool(Arbitrary::arbitrary(u)?), 14 | 2 => serde_json::Value::Number(JsonNumber::arbitrary(u)?.0), 15 | 3 => serde_json::Value::String(Arbitrary::arbitrary(u)?), 16 | 4 => { 17 | let items = u.arbitrary_iter::()?.try_fold( 18 | Vec::new(), 19 | |mut items, v| { 20 | items.push(v?.into()); 21 | Ok(items) 22 | }, 23 | )?; 24 | serde_json::Value::Array(items) 25 | } 26 | 5.. => unreachable!("We %'d by 5 just now"), 27 | }; 28 | 29 | Ok(Self(value)) 30 | } 31 | } 32 | 33 | impl JsonValue { 34 | /// Convenience method that calls [`Faker::fake`]. 35 | /// Saves us from depending on [`fake`] in other crates or re-exporting it. 36 | pub fn fake() -> Self { 37 | Faker.fake() 38 | } 39 | } 40 | 41 | #[derive(Arbitrary)] 42 | /// JSON value that doesn't hold an array or map, 43 | /// so as to not blow up the stack when generating arbitrary values. 44 | /// Based on `>::dummy_with_rng` 45 | enum NonRecursiveValue { 46 | Null, 47 | Bool(bool), 48 | Number(JsonNumber), 49 | String(String), 50 | } 51 | 52 | impl From for serde_json::Value { 53 | fn from(value: NonRecursiveValue) -> Self { 54 | match value { 55 | NonRecursiveValue::Null => Self::Null, 56 | NonRecursiveValue::Bool(b) => Self::Bool(b), 57 | NonRecursiveValue::Number(json_number) => Self::Number(json_number.0), 58 | NonRecursiveValue::String(s) => Self::String(s), 59 | } 60 | } 61 | } 62 | 63 | pub struct JsonNumber(pub serde_json::Number); 64 | 65 | impl<'a> Arbitrary<'a> for JsonNumber { 66 | fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { 67 | let number = match bool::arbitrary(u)? { 68 | true => serde_json::Number::from_f64(Arbitrary::arbitrary(u)?), 69 | false => serde_json::Number::from_i128(Arbitrary::arbitrary(u)?), 70 | }; 71 | Ok(Self(number.ok_or(arbitrary::Error::IncorrectFormat)?)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test_helper/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::disallowed_names)] 2 | 3 | pub mod contract; 4 | pub mod enum_repr; 5 | pub mod enums; 6 | pub mod enums_deny_unknown_fields; 7 | pub mod enums_flattened; 8 | pub mod extra; 9 | pub mod flatten; 10 | pub mod json; 11 | pub mod structs; 12 | 13 | pub mod test_helper; 14 | 15 | mod prelude { 16 | pub(crate) use crate::json::JsonValue; 17 | pub(crate) use arbitrary::Arbitrary; 18 | pub(crate) use eserde::Deserialize; 19 | pub(crate) use fake::Dummy; 20 | pub(crate) use serde::Serialize; 21 | } 22 | 23 | // #[macro_export] 24 | // macro_rules! test { 25 | // (serialized; $type:ty, $serialized:expr$(; $suffix:expr)?) => { 26 | // $crate::test_helper::TestHelper::<$type>::new_serialized($serialized) 27 | // }; 28 | // } 29 | -------------------------------------------------------------------------------- /test_helper/src/structs.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | 3 | #[derive(Deserialize, Serialize, Default, Arbitrary, Dummy, Debug)] 4 | pub struct UnitStruct; 5 | 6 | #[derive(Deserialize, Serialize, Default, Arbitrary, Dummy, Debug)] 7 | pub struct NormalStruct { 8 | foo: String, 9 | bar: bool, 10 | } 11 | 12 | #[derive(Deserialize, Serialize, Default, Arbitrary, Dummy, Debug)] 13 | pub struct NewType(String); 14 | 15 | #[derive(Deserialize, Serialize, Default, Arbitrary, Dummy, Debug)] 16 | pub struct TupleStruct(String, bool); 17 | 18 | #[derive(Deserialize, Serialize, Default, Arbitrary, Dummy, Debug)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct RenamedFields { 21 | camel_case: i32, 22 | #[serde(rename = "new_name")] 23 | old_name: i32, 24 | } 25 | 26 | #[derive(Deserialize, Serialize, Default, Arbitrary, Dummy, Debug)] 27 | #[serde(deny_unknown_fields)] 28 | pub struct DenyUnknownFields { 29 | foo: String, 30 | bar: bool, 31 | } 32 | -------------------------------------------------------------------------------- /test_helper/src/test_helper.rs: -------------------------------------------------------------------------------- 1 | use eserde::EDeserialize; 2 | 3 | use std::marker::PhantomData; 4 | 5 | #[macro_export] 6 | macro_rules! assert_from_json_inline { 7 | ($helper:expr, @$expected:expr) => { 8 | let result = $helper.from_json(); 9 | insta::assert_debug_snapshot!(result, @$expected); 10 | }; 11 | } 12 | 13 | pub struct TestHelper { 14 | value_serialized: String, 15 | _marker: PhantomData, 16 | } 17 | 18 | impl<'de, T> TestHelper 19 | where 20 | T: EDeserialize<'de>, 21 | { 22 | /// Create a new test helper, that holds the passed string for use in assertions. 23 | /// Use [`crate::test`] to invoke this function to have it generate a unique, 24 | /// stable name and figure out the directory the test lives in. 25 | pub fn new_serialized(value_serialized: impl ToString) -> Self { 26 | Self { 27 | value_serialized: value_serialized.to_string(), 28 | _marker: PhantomData, 29 | } 30 | } 31 | 32 | /// Try to deserialize the held data using [`eserde::json::from_str`], 33 | /// returning the result. 34 | pub fn from_json(&'de self) -> Result { 35 | eserde::json::from_str(&self.value_serialized) 36 | } 37 | } 38 | --------------------------------------------------------------------------------