├── .clippy.toml ├── tests ├── integration.rs ├── regression.rs ├── ui │ ├── de-unit-struct.rs │ ├── de-union.stderr │ ├── de-tuple-struct.rs │ ├── de-union.rs │ ├── de-enum-tuple.rs │ ├── de-enum-data.stderr │ ├── de-enum-tuple.stderr │ ├── de-tuple-struct.stderr │ ├── de-enum-untagged-with-unnamed-data.rs │ ├── de-enum-untagged-with-data.rs │ ├── de-double-from-attribute.stderr │ ├── de-container-attr-from-and-try-from.stderr │ ├── de-field-attr-from-and-try-from.stderr │ ├── de-container-attr-try-from-and-from.stderr │ ├── de-field-attr-try-from-and-from.stderr │ ├── de-container-attr-from-and-try-from-on-single-line.rs │ ├── de-container-attr-from-and-try-from.rs │ ├── de-container-attr-try-from-and-from.rs │ ├── de-double-from-attribute-one-line.rs │ ├── de-field-attr-from-and-try-from-on-single-line.rs │ ├── de-field-attr-from-and-try-from.rs │ ├── de-field-attr-try-from-and-from.rs │ ├── de-double-from-attribute.rs │ ├── de-enum-untagged-with-unnamed-data.stderr │ ├── de-enum-generic.stderr │ ├── de-unit-struct.stderr │ ├── de-container-attr-from-and-try-from-on-single-line.stderr │ ├── de-field-attr-from-and-try-from-on-single-line.stderr │ ├── de-double-from-attribute-one-line.stderr │ └── de-enum-untagged-with-data.stderr ├── compiletest.rs ├── attributes │ ├── mod.rs │ ├── where_predicate.rs │ ├── error.rs │ ├── map.rs │ ├── missing_field_error.rs │ ├── validate.rs │ ├── deny_unknown_fields.rs │ ├── default.rs │ ├── from.rs │ ├── rename_all.rs │ ├── tag.rs │ ├── skip.rs │ └── try_from.rs ├── regression │ └── issue24.rs ├── char-error-message.rs ├── supported_value_types.rs └── number-range-error-messages.rs ├── .gitignore ├── assets ├── deserr.ico ├── deserr.png └── deserr_squared.png ├── book ├── book.toml └── src │ ├── error │ ├── mod.md │ ├── custom.md │ └── available.md │ ├── SUMMARY.md │ ├── faq.md │ ├── attributes │ ├── variant.md │ ├── mod.md │ ├── field.md │ └── container.md │ ├── deserialize.md │ └── overview.md ├── src ├── axum │ ├── mod.rs │ └── serde_json.rs ├── errors │ ├── mod.rs │ └── helpers.rs ├── actix_web │ ├── mod.rs │ ├── query_parameters.rs │ └── serde_json.rs ├── serde_cs.rs ├── serde_json.rs ├── lib.rs └── value.rs ├── examples ├── axum │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── actix_web_server │ ├── Cargo.toml │ └── src │ │ └── main.rs └── implements_deserr_manually.rs ├── derive ├── Cargo.toml └── src │ ├── derive_struct.rs │ ├── lib.rs │ ├── derive_user_provided_function.rs │ ├── derive_named_fields.rs │ └── derive_enum.rs ├── .github └── workflows │ ├── publish-binaries.yml │ ├── ci.yml │ └── deploy.yml ├── LICENSE-MIT ├── Cargo.toml ├── README.md ├── benches └── bench.rs └── LICENSE-APACHE /.clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.55.0" 2 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | mod attributes; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /assets/deserr.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/deserr/HEAD/assets/deserr.ico -------------------------------------------------------------------------------- /assets/deserr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/deserr/HEAD/assets/deserr.png -------------------------------------------------------------------------------- /tests/regression.rs: -------------------------------------------------------------------------------- 1 | mod regression { 2 | automod::dir!("tests/regression"); 3 | } 4 | -------------------------------------------------------------------------------- /assets/deserr_squared.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/deserr/HEAD/assets/deserr_squared.png -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Tamo"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "deserr" 7 | -------------------------------------------------------------------------------- /tests/ui/de-unit-struct.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | #[deserr(error = deserr::Error)] 5 | struct UnitStruct; 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /tests/ui/de-union.stderr: -------------------------------------------------------------------------------- 1 | error: Unions aren't supported by the Deserr derive macro 2 | --> tests/ui/de-union.rs:5:1 3 | | 4 | 5 | union Union { 5 | | ^^^^^ 6 | -------------------------------------------------------------------------------- /src/axum/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde-json")] 2 | mod serde_json; 3 | 4 | #[cfg(feature = "serde-json")] 5 | pub use self::serde_json::{AxumJson, AxumJsonRejection}; 6 | -------------------------------------------------------------------------------- /tests/ui/de-tuple-struct.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | #[deserr(error = deserr::Error)] 5 | struct TupleStruct(i32, i32); 6 | 7 | fn main() {} 8 | -------------------------------------------------------------------------------- /tests/ui/de-union.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | #[deserr(error = deserr::Error)] 5 | union Union { 6 | x: i32, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /tests/ui/de-enum-tuple.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | #[deserr(error = deserr::Error, tag = "t")] 5 | enum Enum { 6 | Variant(i32), 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /tests/compiletest.rs: -------------------------------------------------------------------------------- 1 | #[rustversion::attr(not(nightly), ignore)] 2 | #[cfg_attr(miri, ignore)] 3 | #[test] 4 | fn ui() { 5 | let t = trybuild::TestCases::new(); 6 | t.compile_fail("tests/ui/*.rs"); 7 | } 8 | -------------------------------------------------------------------------------- /tests/ui/de-enum-data.stderr: -------------------------------------------------------------------------------- 1 | error: Enum variants with unnamed associated data aren't supported by the Deserr derive macro. 2 | --> tests/ui/de-enum-data.rs:6:12 3 | | 4 | 6 | Variant(i32), 5 | | ^^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/de-enum-tuple.stderr: -------------------------------------------------------------------------------- 1 | error: Enum variants with unnamed associated data aren't supported by the Deserr derive macro. 2 | --> tests/ui/de-enum-tuple.rs:6:12 3 | | 4 | 6 | Variant(i32), 5 | | ^^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/de-tuple-struct.stderr: -------------------------------------------------------------------------------- 1 | error: Tuple structs aren't supported by the Deserr derive macro 2 | --> tests/ui/de-tuple-struct.rs:5:19 3 | | 4 | 5 | struct TupleStruct(i32, i32); 5 | | ^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/attributes/mod.rs: -------------------------------------------------------------------------------- 1 | mod deny_unknown_fields; 2 | mod error; 3 | mod from; 4 | mod map; 5 | mod missing_field_error; 6 | mod rename_all; 7 | mod skip; 8 | mod tag; 9 | mod try_from; 10 | mod validate; 11 | mod where_predicate; 12 | -------------------------------------------------------------------------------- /src/errors/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module holds some pre-made error types to eases your usage of deserr 2 | 3 | pub mod helpers; 4 | pub mod json; 5 | pub mod query_params; 6 | 7 | pub use json::JsonError; 8 | pub use query_params::QueryParamError; 9 | -------------------------------------------------------------------------------- /tests/ui/de-enum-untagged-with-unnamed-data.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | #[deserr(error = deserr::Error)] 5 | enum Enum { 6 | EmptyVariant, 7 | VariantWithSomething(u16), 8 | } 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /tests/ui/de-enum-untagged-with-data.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | #[deserr(error = deserr::Error)] 5 | enum Enum { 6 | EmptyVariant, 7 | VariantWithSomething { data: usize }, 8 | } 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /tests/ui/de-double-from-attribute.stderr: -------------------------------------------------------------------------------- 1 | error: The `try_from` field attribute is defined twice. 2 | --> tests/ui/de-double-from-attribute.rs:6:14 3 | | 4 | 6 | #[deserr(try_from(String) = usize::FromStr -> usize)] 5 | | ^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/de-container-attr-from-and-try-from.stderr: -------------------------------------------------------------------------------- 1 | error: The `try_from` and `from` attributes can't be used together. 2 | --> tests/ui/de-container-attr-from-and-try-from.rs:4:10 3 | | 4 | 4 | #[deserr(from(String) = usize::FromStr)] 5 | | ^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/de-field-attr-from-and-try-from.stderr: -------------------------------------------------------------------------------- 1 | error: The `try_from` and `from` attributes can't be used together. 2 | --> tests/ui/de-field-attr-from-and-try-from.rs:5:14 3 | | 4 | 5 | #[deserr(from(String) = usize::FromStr)] 5 | | ^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/de-container-attr-try-from-and-from.stderr: -------------------------------------------------------------------------------- 1 | error: The `from` and `try_from` attributes can't be used together. 2 | --> tests/ui/de-container-attr-try-from-and-from.rs:4:10 3 | | 4 | 4 | #[deserr(try_from(String) = String::parse -> usize)] 5 | | ^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/de-field-attr-try-from-and-from.stderr: -------------------------------------------------------------------------------- 1 | error: The `from` and `try_from` attributes can't be used together. 2 | --> tests/ui/de-field-attr-try-from-and-from.rs:5:14 3 | | 4 | 5 | #[deserr(try_from(String) = String::parse -> usize)] 5 | | ^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/de-container-attr-from-and-try-from-on-single-line.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | #[deserr(from(String) = usize::FromStr, try_from(String) = String::parse -> usize)] 5 | struct UnitStruct { 6 | hello: usize, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /tests/ui/de-container-attr-from-and-try-from.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | #[deserr(from(String) = usize::FromStr)] 5 | #[deserr(try_from(String) = String::parse -> usize)] 6 | struct UnitStruct { 7 | hello: usize, 8 | } 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /tests/ui/de-container-attr-try-from-and-from.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | #[deserr(try_from(String) = String::parse -> usize)] 5 | #[deserr(from(String) = usize::FromStr)] 6 | struct UnitStruct { 7 | hello: usize, 8 | } 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /tests/ui/de-double-from-attribute-one-line.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | struct UnitStruct { 5 | #[deserr(try_from(String) = String::parse -> usize, try_from(String) = usize::FromStr -> usize)] 6 | hello: usize, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /tests/ui/de-field-attr-from-and-try-from-on-single-line.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | struct UnitStruct { 5 | #[deserr(from(String) = usize::FromStr, try_from(String) = String::parse -> usize)] 6 | hello: usize, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /tests/ui/de-field-attr-from-and-try-from.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | struct UnitStruct { 5 | #[deserr(from(String) = usize::FromStr)] 6 | #[deserr(try_from(String) = String::parse -> usize)] 7 | hello: usize, 8 | } 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /tests/ui/de-field-attr-try-from-and-from.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | struct UnitStruct { 5 | #[deserr(try_from(String) = String::parse -> usize)] 6 | #[deserr(from(String) = usize::FromStr)] 7 | hello: usize, 8 | } 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /tests/ui/de-double-from-attribute.rs: -------------------------------------------------------------------------------- 1 | use deserr::Deserr; 2 | 3 | #[derive(Deserr)] 4 | struct UnitStruct { 5 | #[deserr(try_from(String) = String::parse -> usize)] 6 | #[deserr(try_from(String) = usize::FromStr -> usize)] 7 | hello: usize, 8 | } 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /tests/ui/de-enum-untagged-with-unnamed-data.stderr: -------------------------------------------------------------------------------- 1 | error: Enum variants with unnamed associated data aren't supported by the Deserr derive macro. 2 | --> tests/ui/de-enum-untagged-with-unnamed-data.rs:7:25 3 | | 4 | 7 | VariantWithSomething(u16), 5 | | ^^^^^ 6 | -------------------------------------------------------------------------------- /book/src/error/mod.md: -------------------------------------------------------------------------------- 1 | # Handling errors 2 | 3 | The main point of deserr is to improve the error message of your user facing API by using one of our already defined error type or by creating your own. 4 | 5 | - [Available error type](available.md) 6 | - [Defining your own error](custom.md) 7 | 8 | -------------------------------------------------------------------------------- /tests/ui/de-enum-generic.stderr: -------------------------------------------------------------------------------- 1 | error: Enums with generics are not supported 2 | --> tests/ui/de-enum-generic.rs:3:10 3 | | 4 | 3 | #[derive(Deserr)] 5 | | ^^^^^^ 6 | | 7 | = note: this error originates in the derive macro `Deserr` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | -------------------------------------------------------------------------------- /tests/ui/de-unit-struct.stderr: -------------------------------------------------------------------------------- 1 | error: Unit structs aren't supported by the Deserr derive macro 2 | --> tests/ui/de-unit-struct.rs:3:10 3 | | 4 | 3 | #[derive(Deserr)] 5 | | ^^^^^^ 6 | | 7 | = note: this error originates in the derive macro `Deserr` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | -------------------------------------------------------------------------------- /tests/ui/de-container-attr-from-and-try-from-on-single-line.stderr: -------------------------------------------------------------------------------- 1 | error: The `try_from` and `from` attributes can't be used together. 2 | --> tests/ui/de-container-attr-from-and-try-from-on-single-line.rs:4:10 3 | | 4 | 4 | #[deserr(from(String) = usize::FromStr, try_from(String) = String::parse -> usize)] 5 | | ^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/de-field-attr-from-and-try-from-on-single-line.stderr: -------------------------------------------------------------------------------- 1 | error: The `try_from` and `from` attributes can't be used together. 2 | --> tests/ui/de-field-attr-from-and-try-from-on-single-line.rs:5:14 3 | | 4 | 5 | #[deserr(from(String) = usize::FromStr, try_from(String) = String::parse -> usize)] 5 | | ^^^^ 6 | -------------------------------------------------------------------------------- /src/actix_web/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde-json")] 2 | mod query_parameters; 3 | #[cfg(feature = "serde-json")] 4 | mod serde_json; 5 | 6 | #[cfg(feature = "serde-json")] 7 | pub use self::query_parameters::AwebQueryParameter; 8 | #[cfg(feature = "serde-json")] 9 | pub use self::serde_json::{AwebJson, AwebJsonExtractFut}; 10 | -------------------------------------------------------------------------------- /tests/ui/de-double-from-attribute-one-line.stderr: -------------------------------------------------------------------------------- 1 | error: The `try_from` field attribute is defined twice. 2 | --> tests/ui/de-double-from-attribute-one-line.rs:5:57 3 | | 4 | 5 | #[deserr(try_from(String) = String::parse -> usize, try_from(String) = usize::FromStr -> usize)] 5 | | ^^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/de-enum-untagged-with-data.stderr: -------------------------------------------------------------------------------- 1 | error: Externally tagged enums are not supported yet by deserr. Add #[deserr(tag = "some_tag_key")] 2 | --> tests/ui/de-enum-untagged-with-data.rs:3:10 3 | | 4 | 3 | #[derive(Deserr)] 5 | | ^^^^^^ 6 | | 7 | = note: this error originates in the derive macro `Deserr` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | -------------------------------------------------------------------------------- /examples/axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | deserr = { path = "../../", features = ["axum"] } 9 | axum = { version = "0.8.4", features = ["json", "macros"] } 10 | tokio = { version = "1.47", features = ["full"] } 11 | serde = { version = "1.0.223", features = ["derive"] } 12 | tracing = "0.1" 13 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 14 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Overview](overview.md) 4 | - [Attributes](attributes/mod.md) 5 | - [Container attributes](attributes/container.md) 6 | - [Variant attributes](attributes/variant.md) 7 | - [Field attributes](attributes/field.md) 8 | - [Implementing deserialize](deserialize.md) 9 | - [Handling errors](error/mod.md) 10 | - [Already available error type](error/available.md) 11 | - [Defining your own error](error/custom.md) 12 | - [FAQ](faq.md) 13 | -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deserr-internal" 3 | version = "0.6.4" 4 | authors = ["Lo ", "Tamo "] 5 | license = "MIT OR Apache-2.0" 6 | description = "Derive macros for Deserr. Use the re-exports from the deserr crate instead." 7 | repository = "https://github.com/meilisearch/deserr" 8 | edition = "2021" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | proc-macro2 = "1.0" 15 | quote = "1.0.40" 16 | syn = { version = "2.0", features=["extra-traits", "parsing"]} 17 | convert_case = "0.6.0" 18 | 19 | [package.metadata.docs.rs] 20 | targets = ["x86_64-unknown-linux-gnu"] 21 | -------------------------------------------------------------------------------- /.github/workflows/publish-binaries.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [published] 4 | 5 | name: Publish binaries to release 6 | 7 | jobs: 8 | publish-crates-io: 9 | name: Publish on crates.io 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | toolchain: stable 16 | override: true 17 | - name: Login 18 | run: cargo login ${{ secrets.CRATES_TOKEN }} 19 | - name: Publish the internal derive crate 20 | run: |- 21 | cd derive 22 | cargo publish 23 | - name: Publish deserr to crates.io 24 | run: cargo publish 25 | -------------------------------------------------------------------------------- /examples/actix_web_server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix_web_server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | actix-http = { version = "3.11.1", default-features = false, features = ["compress-brotli", "compress-gzip", "rustls"] } 11 | actix-web = { version = "4.11.0", default-features = false, features = ["macros", "compress-brotli", "compress-gzip", "cookies", "rustls"] } 12 | anyhow = "1.0.99" 13 | deserr = { path = "../../", features = ["actix-web"] } 14 | env_logger = "0.11.8" 15 | futures = "0.3.31" 16 | futures-util = "0.3.31" 17 | log = "0.4.28" 18 | serde = { version = "1.0.223", features = ["derive"] } 19 | serde_json = "1.0.145" 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: [cron: "40 1 * * *"] 7 | 8 | env: 9 | RUSTFLAGS: '-Dwarnings' 10 | 11 | jobs: 12 | test: 13 | name: Rust ${{matrix.rust}} 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | rust: [nightly, stable] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: dtolnay/rust-toolchain@master 22 | with: 23 | toolchain: ${{matrix.rust}} 24 | - run: cargo test 25 | 26 | clippy: 27 | name: Clippy 28 | runs-on: ubuntu-latest 29 | if: github.event_name != 'pull_request' 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: dtolnay/rust-toolchain@clippy 33 | - run: cargo clippy --tests --benches -- -Dclippy::all 34 | -------------------------------------------------------------------------------- /tests/attributes/where_predicate.rs: -------------------------------------------------------------------------------- 1 | use deserr::{deserialize, errors::JsonError, Deserr}; 2 | use insta::assert_debug_snapshot; 3 | use serde_json::json; 4 | 5 | #[test] 6 | fn where_attribute() { 7 | #[allow(unused)] 8 | #[derive(Debug, Deserr)] 9 | #[deserr(where_predicate = T: Deserr<__Deserr_E>)] 10 | struct Struct { 11 | doggo: String, 12 | catto: T, 13 | } 14 | 15 | let data = 16 | deserialize::, _, JsonError>(json!({ "doggo": "bork", "catto": "jorts" })) 17 | .unwrap(); 18 | assert_debug_snapshot!(data, @r###" 19 | Struct { 20 | doggo: "bork", 21 | catto: "jorts", 22 | } 23 | "###); 24 | 25 | let data = 26 | deserialize::, _, JsonError>(json!({ "doggo": "bork", "catto": 3 })).unwrap(); 27 | assert_debug_snapshot!(data, @r###" 28 | Struct { 29 | doggo: "bork", 30 | catto: 3, 31 | } 32 | "###); 33 | } 34 | -------------------------------------------------------------------------------- /src/errors/helpers.rs: -------------------------------------------------------------------------------- 1 | //! Provide generic function that could be useful when creating your own 2 | //! error type. 3 | 4 | use strsim::damerau_levenshtein; 5 | 6 | /// Compute a did you mean message from a received string and a list of 7 | /// accepted strings. 8 | pub fn did_you_mean(received: &str, accepted: &[&str]) -> String { 9 | let typo_allowed = match received.len() { 10 | // no typos are allowed, we can early return 11 | 0..=3 => return String::new(), 12 | 4..=7 => 1, 13 | 8..=12 => 2, 14 | 13..=17 => 3, 15 | 18..=24 => 4, 16 | _ => 5, 17 | }; 18 | match accepted 19 | .iter() 20 | .map(|accepted| (accepted, damerau_levenshtein(received, accepted))) 21 | .filter(|(_, distance)| distance <= &typo_allowed) 22 | .min_by(|(_, d1), (_, d2)| d1.cmp(d2)) 23 | { 24 | None => String::new(), 25 | Some((accepted, _)) => format!("did you mean `{}`? ", accepted), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/attributes/error.rs: -------------------------------------------------------------------------------- 1 | use deserr::{deserialize, errors::JsonError, Deserr}; 2 | use insta::{assert_debug_snapshot, assert_snapshot}; 3 | use serde_json::json; 4 | 5 | #[test] 6 | fn error_attribute() { 7 | #[allow(unused)] 8 | #[derive(Debug, Deserr)] 9 | #[deserr(error = JsonError)] 10 | struct Struct { 11 | doggo: String, 12 | #[deserr(error = JsonError)] 13 | catto: String, 14 | } 15 | 16 | // now deserr know the error type to use 17 | let data = deserialize::(json!({ "doggo": "bork", "catto": "jorts" })).unwrap(); 18 | assert_debug_snapshot!(data, @r###" 19 | Struct { 20 | doggo: "bork", 21 | catto: "jorts", 22 | } 23 | "###); 24 | 25 | let data = deserialize::(json!({ "catto": "jorts" })).unwrap_err(); 26 | assert_snapshot!(data, @"Missing field `doggo`"); 27 | 28 | let data = deserialize::(json!({ "doggo": "bork" })).unwrap_err(); 29 | assert_snapshot!(data, @"Missing field `catto`"); 30 | } 31 | -------------------------------------------------------------------------------- /tests/regression/issue24.rs: -------------------------------------------------------------------------------- 1 | use deserr::{DeserializeError, Deserr, IntoValue, MergeWithError}; 2 | use std::ops::ControlFlow; 3 | 4 | #[derive(Debug)] 5 | pub struct MyError; 6 | impl DeserializeError for MyError { 7 | fn error( 8 | _self_: Option, 9 | _error: deserr::ErrorKind, 10 | _location: deserr::ValuePointerRef, 11 | ) -> ControlFlow { 12 | todo!() 13 | } 14 | } 15 | 16 | impl MergeWithError for MyError { 17 | fn merge( 18 | _self_: Option, 19 | _other: MyError, 20 | _merge_location: deserr::ValuePointerRef, 21 | ) -> ControlFlow { 22 | todo!() 23 | } 24 | } 25 | 26 | #[derive(Deserr)] 27 | #[deserr(error = MyError)] 28 | #[allow(unused)] 29 | pub struct Point { 30 | pub x: u32, 31 | pub y: u32, 32 | } 33 | 34 | #[test] 35 | fn main() { 36 | let result = serde_json::from_str::(r#"{"x": 1, "y": 2, "z": 3}"#).unwrap(); 37 | let _: Point = deserr::deserialize(result).unwrap(); 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /book/src/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## But why? 4 | At Meilisearch, we wanted to customize the error code we return when we fail 5 | the deserialization of a specific field. 6 | Some error messages were also not clear at all and impossible to edit. 7 | 8 | ## What about the maintenance? 9 | At Meilisearch we're already using deserr in production; thus, it's well maintained. 10 | 11 | ## Where can I see more examples of usage of this crate? 12 | Currently, you can read our examples in the `examples` directory of this repository. 13 | You can also look at our integration test; each attribute has a simple-to-read test. 14 | 15 | And obviously, you can read the code of Meilisearch where deserr is used on all our 16 | routes. 17 | 18 | ## My question is not listed 19 | Please, if you think there is a bug in this lib or would like a new feature, 20 | open an issue or a discussion. 21 | If you would like to chat more directly with us, you can join us on discord 22 | at https://discord.com/invite/meilisearch and ping @irevoire. 23 | 24 | ## The logo 25 | The logo was graciously offered and crafted by @irevoire 's sister after a lot of back and forth. 26 | Many thanks to her. 27 | -------------------------------------------------------------------------------- /tests/char-error-message.rs: -------------------------------------------------------------------------------- 1 | use deserr::errors::JsonError; 2 | use serde_json::json; 3 | 4 | #[allow(unused)] 5 | #[derive(Debug, deserr::Deserr)] 6 | #[deserr(deny_unknown_fields)] 7 | struct Test { 8 | #[deserr(default)] 9 | c: char, 10 | } 11 | 12 | #[test] 13 | fn deserialize_char() { 14 | let ret = deserr::deserialize::(json!({ "c": "j" })).unwrap(); 15 | insta::assert_debug_snapshot!(ret, @r###" 16 | Test { 17 | c: 'j', 18 | } 19 | "###); 20 | 21 | let ret = deserr::deserialize::(json!({ "c": "jorts" })).unwrap_err(); 22 | insta::assert_snapshot!(ret, @"Invalid value at `.c`: expected a string of one character, but found the following string of 5 characters: `jorts`"); 23 | 24 | let ret = deserr::deserialize::(json!({ "c": "" })).unwrap_err(); 25 | insta::assert_snapshot!(ret, @"Invalid value at `.c`: expected a string of one character, but found an empty string"); 26 | 27 | let ret = deserr::deserialize::(json!({ "c": null })).unwrap_err(); 28 | insta::assert_snapshot!(ret, @"Invalid value type at `.c`: expected a string, but found null"); 29 | } 30 | -------------------------------------------------------------------------------- /src/serde_cs.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use serde_cs::vec::CS; 4 | 5 | use crate::{ 6 | take_cf_content, DeserializeError, Deserr, ErrorKind, IntoValue, Value, ValueKind, 7 | ValuePointerRef, 8 | }; 9 | 10 | impl Deserr for CS 11 | where 12 | R: FromStr, 13 | FE: std::error::Error, 14 | E: DeserializeError, 15 | { 16 | fn deserialize_from_value( 17 | value: Value, 18 | location: ValuePointerRef, 19 | ) -> Result { 20 | match value { 21 | Value::String(s) => match CS::from_str(&s) { 22 | Ok(ret) => Ok(ret), 23 | Err(e) => Err(take_cf_content(E::error::( 24 | None, 25 | ErrorKind::Unexpected { msg: e.to_string() }, 26 | location, 27 | ))), 28 | }, 29 | value => Err(take_cf_content(E::error::( 30 | None, 31 | ErrorKind::IncorrectValueKind { 32 | actual: value, 33 | accepted: &[ValueKind::String], 34 | }, 35 | location, 36 | ))), 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write # To push a branch 12 | pull-requests: write # To create a PR from that branch 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - name: Install latest mdbook 18 | run: | 19 | tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') 20 | url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" 21 | mkdir mdbook 22 | curl -sSL $url | tar -xz --directory=./mdbook 23 | echo `pwd`/mdbook >> $GITHUB_PATH 24 | - name: Deploy GitHub Pages 25 | run: | 26 | # This assumes your book is in the root of your repository. 27 | # Just add a `cd` here if you need to change to another directory. 28 | cd book 29 | mdbook build 30 | git worktree add gh-pages 31 | git config user.name "Deploy from CI" 32 | git config user.email "" 33 | cd gh-pages 34 | # Delete the ref to avoid keeping history. 35 | git update-ref -d refs/heads/gh-pages 36 | rm -rf * 37 | mv ../book/* . 38 | git add . 39 | git commit -m "Deploy $GITHUB_SHA to gh-pages" 40 | git push --force --set-upstream origin gh-pages 41 | -------------------------------------------------------------------------------- /tests/attributes/map.rs: -------------------------------------------------------------------------------- 1 | use deserr::{deserialize, errors::JsonError, Deserr}; 2 | use insta::assert_debug_snapshot; 3 | use serde_json::json; 4 | 5 | #[test] 6 | fn map() { 7 | #[allow(unused)] 8 | #[derive(Debug, Deserr)] 9 | struct Struct { 10 | #[deserr(map = square)] 11 | doggo: usize, 12 | } 13 | 14 | fn square(n: usize) -> usize { 15 | n * n 16 | } 17 | 18 | let data = deserialize::(json!({ "doggo": 1 })).unwrap(); 19 | 20 | assert_debug_snapshot!(data, @r###" 21 | Struct { 22 | doggo: 1, 23 | } 24 | "###); 25 | 26 | let data = deserialize::(json!({})).unwrap_err(); 27 | 28 | assert_debug_snapshot!(data, @r###" 29 | JsonError( 30 | "Missing field `doggo`", 31 | ) 32 | "###); 33 | } 34 | 35 | #[test] 36 | fn map_and_default() { 37 | #[allow(unused)] 38 | #[derive(Debug, Deserr)] 39 | struct Struct { 40 | #[deserr(default = 2, map = square)] 41 | doggo: usize, 42 | } 43 | 44 | fn square(n: usize) -> usize { 45 | n * n 46 | } 47 | 48 | let data = deserialize::(json!({ "doggo": 1 })).unwrap(); 49 | 50 | assert_debug_snapshot!(data, @r###" 51 | Struct { 52 | doggo: 1, 53 | } 54 | "###); 55 | 56 | let data = deserialize::(json!({})).unwrap(); 57 | 58 | assert_debug_snapshot!(data, @r###" 59 | Struct { 60 | doggo: 4, 61 | } 62 | "###); 63 | } 64 | -------------------------------------------------------------------------------- /tests/attributes/missing_field_error.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use deserr::{ 4 | deserialize, errors::JsonError, take_cf_content, DeserializeError, Deserr, ErrorKind, 5 | ValuePointerRef, 6 | }; 7 | use insta::{assert_debug_snapshot, assert_snapshot}; 8 | use serde_json::json; 9 | 10 | #[test] 11 | fn missing_field_error() { 12 | #[allow(unused)] 13 | #[derive(Debug, Deserr)] 14 | struct Struct { 15 | doggo: String, 16 | #[deserr(missing_field_error = custom_function)] 17 | catto: String, 18 | } 19 | 20 | fn custom_function(_field_name: &str, location: ValuePointerRef) -> E { 21 | take_cf_content(E::error::( 22 | None, 23 | ErrorKind::Unexpected { 24 | msg: String::from("I really need the query field, please give it to me uwu"), 25 | }, 26 | location, 27 | )) 28 | } 29 | 30 | let data = 31 | deserialize::(json!({ "doggo": "bork", "catto": "jorts" })).unwrap(); 32 | assert_debug_snapshot!(data, @r###" 33 | Struct { 34 | doggo: "bork", 35 | catto: "jorts", 36 | } 37 | "###); 38 | 39 | let data = deserialize::(json!({ "catto": "jorts" })).unwrap_err(); 40 | assert_snapshot!(data, @"Missing field `doggo`"); 41 | 42 | let data = deserialize::(json!({ "doggo": "bork" })).unwrap_err(); 43 | assert_snapshot!(data, @"Invalid value: I really need the query field, please give it to me uwu"); 44 | } 45 | -------------------------------------------------------------------------------- /book/src/attributes/variant.md: -------------------------------------------------------------------------------- 1 | # Variant attributes 2 | 3 | ### `#[deserr(rename = "...")]` 4 | 5 | Deserialize this enum variant with the given name instead of its Rust name. 6 | 7 | ```rust 8 | use deserr::{Deserr, deserialize, errors::JsonError}; 9 | use serde_json::json; 10 | 11 | #[derive(Deserr, Debug, PartialEq, Eq)] 12 | enum Dog { 13 | #[deserr(rename = "the kef")] 14 | Kefir, 15 | Echo, 16 | Intel 17 | } 18 | 19 | let data = deserialize::( 20 | json!("the kef"), 21 | ) 22 | .unwrap(); 23 | assert_eq!(data, Dog::Kefir); 24 | ``` 25 | 26 | [Also available as a field attribute.](field.md#deserrrename) 27 | 28 | ### `#[deserr(rename_all = ...)]` 29 | 30 | Rename all the variants according to the given case convention. 31 | The possible values are: `lowercase`, `camelCase`. 32 | 33 | If you need more values please open an issue, it's easy to implement and was simply not implemented because it isn't required for Meilisearch at the moment. 34 | 35 |
36 | 37 | Unlike `serde`, you don't need to put the double-quotes (`"`) around the name of the case, e.g.: `#[deserr(rename_all = camelCase)]`. 38 | 39 |
40 | 41 | ```rust 42 | use deserr::{Deserr, deserialize, errors::JsonError}; 43 | use serde_json::json; 44 | 45 | #[derive(Deserr, Debug, PartialEq, Eq)] 46 | #[deserr(rename_all = lowercase)] 47 | enum Pets { 48 | KefirTheSnob, 49 | EchoTheFilthyGoblin, 50 | IntelTheWise, 51 | } 52 | 53 | let data = deserialize::( 54 | json!("echothefilthygoblin"), 55 | ) 56 | .unwrap(); 57 | assert_eq!(data, Pets::EchoTheFilthyGoblin); 58 | ``` 59 | -------------------------------------------------------------------------------- /tests/attributes/validate.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use deserr::{ 4 | deserialize, errors::JsonError, DeserializeError, Deserr, ErrorKind, ValuePointerRef, 5 | }; 6 | use insta::{assert_debug_snapshot, assert_snapshot}; 7 | use serde_json::json; 8 | 9 | #[test] 10 | fn validate() { 11 | #[allow(unused)] 12 | #[derive(Debug, Deserr)] 13 | #[deserr(validate = validate_range -> __Deserr_E)] 14 | struct Range { 15 | start: usize, 16 | end: usize, 17 | } 18 | 19 | fn validate_range( 20 | range: Range, 21 | location: ValuePointerRef, 22 | ) -> Result { 23 | if range.end < range.start { 24 | Err(deserr::take_cf_content(E::error::( 25 | None, 26 | ErrorKind::Unexpected { 27 | msg: format!( 28 | "`end` (`{}`) should be greater than `start` (`{}`)", 29 | range.end, range.start 30 | ), 31 | }, 32 | location, 33 | ))) 34 | } else { 35 | Ok(range) 36 | } 37 | } 38 | 39 | let data = deserialize::(json!({ "start": 2, "end": 6 })).unwrap(); 40 | 41 | assert_debug_snapshot!(data, @r###" 42 | Range { 43 | start: 2, 44 | end: 6, 45 | } 46 | "###); 47 | 48 | let data = deserialize::(json!({ "start": 6, "end": 2 })).unwrap_err(); 49 | 50 | assert_snapshot!(data, @"Invalid value: `end` (`2`) should be greater than `start` (`6`)"); 51 | } 52 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deserr" 3 | version = "0.6.4" 4 | authors = ["Lo ", "Tamo "] 5 | license = "MIT OR Apache-2.0" 6 | description = "Deserialization library with focus on error handling" 7 | repository = "https://github.com/meilisearch/deserr" 8 | readme = "README.md" 9 | exclude = ["benches"] 10 | keywords = ["deserialization", "error"] 11 | edition = "2021" 12 | 13 | [dependencies] 14 | serde_json = { version = "1.0", optional = true } 15 | serde-cs = { version = "0.2.4", optional = true } 16 | actix-web = { version = "4.11.0", default-features = false, optional = true } 17 | axum = { version = "0.8.4", features = ["json"], optional = true } 18 | http = { version = "1.3.1", optional = true } 19 | futures = { version = "0.3.31", optional = true } 20 | deserr-internal = { version = "=0.6.4", path = "derive" } 21 | strsim = "0.11.1" 22 | actix-http = { version = "3.11.1", optional = true } 23 | actix-utils = { version = "3.0.1", optional = true } 24 | 25 | [features] 26 | default = ["serde-json", "serde-cs"] 27 | serde-json = ["serde_json"] 28 | serde-cs = ["dep:serde-cs"] 29 | actix-web = ["dep:actix-web", "futures", "actix-http", "actix-utils"] 30 | axum = ["dep:axum", "http"] 31 | 32 | [dev-dependencies] 33 | automod = "1.0" 34 | insta = { version = "1.43.2", features = ["json"] } 35 | rustversion = "1.0" 36 | serde = { version = "1.0", features = ["derive"] } 37 | serde_derive = "1.0" 38 | trybuild = { version = "1.0.111", features = ["diff"] } 39 | 40 | [workspace] 41 | members = ["derive", "examples/*"] 42 | 43 | [package.metadata.docs.rs] 44 | targets = ["x86_64-unknown-linux-gnu"] 45 | -------------------------------------------------------------------------------- /derive/src/derive_struct.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | use crate::parse_type::{CommonDerivedTypeInfo, NamedFieldsInfo}; 5 | 6 | /// Return a token stream that implements `Deserr` for the given derived struct with named fields 7 | pub fn generate_derive_struct_impl( 8 | info: CommonDerivedTypeInfo, 9 | fields: NamedFieldsInfo, 10 | ) -> TokenStream { 11 | let CommonDerivedTypeInfo { 12 | impl_trait_tokens, 13 | err_ty, 14 | validate, 15 | } = info; 16 | 17 | let fields_impl = crate::generate_named_fields_impl(&fields, &err_ty, quote! { Self }); 18 | 19 | quote! { 20 | #impl_trait_tokens { 21 | fn deserialize_from_value(deserr_value__: ::deserr::Value, deserr_location__: ::deserr::ValuePointerRef) -> ::std::result::Result { 22 | let deserr_final__ = match deserr_value__ { 23 | // The value must always be a map 24 | ::deserr::Value::Map(deserr_map__) => { 25 | let mut deserr_error__ = None; 26 | #fields_impl 27 | } 28 | // this is the case where the value is not a map 29 | v => { 30 | ::std::result::Result::Err( 31 | ::deserr::take_cf_content(<#err_ty as ::deserr::DeserializeError>::error::( 32 | None, 33 | ::deserr::ErrorKind::IncorrectValueKind { 34 | actual: v, 35 | accepted: &[::deserr::ValueKind::Map], 36 | }, 37 | deserr_location__ 38 | )) 39 | ) 40 | } 41 | }?; 42 | #validate 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /book/src/error/custom.md: -------------------------------------------------------------------------------- 1 | # Defining your own error 2 | 3 | Defining your own error type comes down to implementing the [`DeserrError`](https://docs.rs/deserr/latest/deserr/trait.DeserializeError.html) trait that looks like that: 4 | ```rust 5 | pub trait DeserializeError: Sized + deserr::MergeWithError { 6 | fn error( 7 | self_: Option, 8 | error: deserr::ErrorKind<'_, V>, 9 | location: deserr::ValuePointerRef<'_>, 10 | ) -> std::ops::ControlFlow; 11 | } 12 | ``` 13 | 14 | The method's job is to build your custom error type from an error kind and a location. 15 | deserr will call this method everytime it encounter an error while deserializing the specified payload and your job will be 16 | to craft your own error type from the parameters, and let deserr know if it should continue to explore the payload looking 17 | for more errors or stop immediately. 18 | - `_self` contains the previous version of your error if you told deserr to accumulate errors. 19 | - `error` the error encountered by deserr whil deserializing the value. 20 | - `location` the location of the error 21 | - [`ControlFlow`](https://doc.rust-lang.org/stable/std/ops/enum.ControlFlow.html) is your way to tell deserr to continue accumulating errors or to stop. 22 | 23 | And you may have noticed that your type must also implements the [`MergeWithError`](https://docs.rs/deserr/latest/deserr/trait.MergeWithError.html) trait. 24 | This trait describe error type that can be merged together to return only one final type. 25 | It also gives you the opportunity to tell deserr to stop deserializing the structure. 26 | 27 | ```rust 28 | pub trait MergeWithError: Sized { 29 | fn merge( 30 | self_: Option, 31 | other: T, 32 | merge_location: deserr::ValuePointerRef<'_>, 33 | ) -> std::ops::ControlFlow; 34 | } 35 | ``` 36 | 37 | This trait also gives you the opportunity to merge **an other** error type with your error type. 38 | -------------------------------------------------------------------------------- /src/actix_web/query_parameters.rs: -------------------------------------------------------------------------------- 1 | //! A module to parse query parameter as String with deserr 2 | 3 | use std::marker::PhantomData; 4 | use std::{fmt, ops}; 5 | 6 | use crate::{DeserializeError, Deserr}; 7 | use actix_http::Payload; 8 | use actix_utils::future::{err, ok, Ready}; 9 | use actix_web::web::Query; 10 | use actix_web::{FromRequest, HttpRequest, ResponseError}; 11 | 12 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 13 | pub struct AwebQueryParameter(pub T, PhantomData<*const E>); 14 | 15 | impl AwebQueryParameter { 16 | /// Unwrap into inner `T` value. 17 | pub fn into_inner(self) -> T { 18 | self.0 19 | } 20 | } 21 | 22 | impl AwebQueryParameter 23 | where 24 | T: Deserr, 25 | E: DeserializeError + ResponseError + 'static, 26 | { 27 | pub fn from_query(query_str: &str) -> Result { 28 | let value = Query::::from_query(query_str)?; 29 | 30 | match deserr::deserialize::<_, _, E>(value.0) { 31 | Ok(data) => Ok(AwebQueryParameter(data, PhantomData)), 32 | Err(e) => Err(e)?, 33 | } 34 | } 35 | } 36 | 37 | impl ops::Deref for AwebQueryParameter { 38 | type Target = T; 39 | 40 | fn deref(&self) -> &T { 41 | &self.0 42 | } 43 | } 44 | 45 | impl ops::DerefMut for AwebQueryParameter { 46 | fn deref_mut(&mut self) -> &mut T { 47 | &mut self.0 48 | } 49 | } 50 | 51 | impl fmt::Display for AwebQueryParameter { 52 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 53 | self.0.fmt(f) 54 | } 55 | } 56 | 57 | impl FromRequest for AwebQueryParameter 58 | where 59 | T: Deserr, 60 | E: DeserializeError + ResponseError + 'static, 61 | { 62 | type Error = actix_web::Error; 63 | type Future = Ready>; 64 | 65 | #[inline] 66 | fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { 67 | AwebQueryParameter::from_query(req.query_string()) 68 | .map(ok) 69 | .unwrap_or_else(err) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/attributes/deny_unknown_fields.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use deserr::{ 4 | deserialize, errors::JsonError, take_cf_content, DeserializeError, Deserr, ErrorKind, 5 | ValuePointerRef, 6 | }; 7 | use insta::{assert_debug_snapshot, assert_snapshot}; 8 | use serde_json::json; 9 | 10 | #[test] 11 | fn default_deny_unknown_fields() { 12 | #[allow(unused)] 13 | #[derive(Debug, Deserr)] 14 | #[deserr(deny_unknown_fields)] 15 | struct Struct { 16 | word: String, 17 | } 18 | 19 | let data = deserialize::(json!({ "word": "doggo" })).unwrap(); 20 | 21 | assert_debug_snapshot!(data, @r###" 22 | Struct { 23 | word: "doggo", 24 | } 25 | "###); 26 | 27 | let data = deserialize::(json!({ "word": "doggo", "turbo": "doggo" })) 28 | .unwrap_err(); 29 | 30 | assert_snapshot!(data, @"Unknown field `turbo`: expected one of `word`"); 31 | } 32 | 33 | #[test] 34 | fn custom_deny_unknown_fields() { 35 | #[allow(unused)] 36 | #[derive(Debug, Deserr)] 37 | #[deserr(deny_unknown_fields = custom_function)] 38 | struct Struct { 39 | word: String, 40 | } 41 | 42 | fn custom_function( 43 | field: &str, 44 | accepted: &[&str], 45 | location: ValuePointerRef, 46 | ) -> E { 47 | match field { 48 | "doggo" => take_cf_content(E::error::( 49 | None, 50 | ErrorKind::Unexpected { 51 | msg: "The word is doggo, not the opposite".to_string(), 52 | }, 53 | location, 54 | )), 55 | _ => take_cf_content(E::error::( 56 | None, 57 | deserr::ErrorKind::UnknownKey { 58 | key: field, 59 | accepted, 60 | }, 61 | location, 62 | )), 63 | } 64 | } 65 | 66 | let data = deserialize::(json!({ "word": "doggo" })).unwrap(); 67 | 68 | assert_debug_snapshot!(data, @r###" 69 | Struct { 70 | word: "doggo", 71 | } 72 | "###); 73 | 74 | let data = deserialize::(json!({ "word": "doggo", "turbo": "doggo" })) 75 | .unwrap_err(); 76 | 77 | assert_snapshot!(data, @"Unknown field `turbo`: expected one of `word`"); 78 | 79 | let data = deserialize::(json!({ "word": "doggo", "doggo": "word" })) 80 | .unwrap_err(); 81 | 82 | assert_snapshot!(data, @"Invalid value: The word is doggo, not the opposite"); 83 | } 84 | -------------------------------------------------------------------------------- /book/src/deserialize.md: -------------------------------------------------------------------------------- 1 | # Implementing deserialize manually 2 | 3 | The [`Deserr`](https://docs.rs/deserr/latest/deserr/trait.Deserr.html) trait looks like this: 4 | 5 | ```rust 6 | pub trait Deserr: Sized { 7 | fn deserialize_from_value( 8 | value: deserr::Value, 9 | location: deserr::ValuePointerRef<'_>, 10 | ) -> Result; 11 | } 12 | ``` 13 | 14 | The method's job is to deserialize a value to the concrete type you're implementing this trait on. 15 | It's useful when the derive macro is not powerful enough for you. 16 | Let's go through all of its paratemers: 17 | - `E: deserr::DeserializeError`: The error type that can be returned while deserializing your type. It can be anything that implements the [`DeserializeError`](https://docs.rs/deserr/latest/deserr/trait.DeserializeError.html) trait. 18 | - `value` parameter: The value you must deserialize, it's similar to a `serde_json::Value`. 19 | - `location` parameter: A linked list representing the path being explored. Always make sure to update the location correctly otherwise the error messages will be really hard to debug. 20 | 21 | For example you'll often need to implement the type yourself while working with enums since deserr 22 | only supports unit enums. 23 | 24 | One of the most common type you might need while working with json is a type that represents if a value 25 | is `Set` (specified by the user), `NotSet` (the field is not present) or `Reset` (the field is set to `null`). 26 | Instead of working with an `Option>` we may want to introduce the following enum and implement `Deserr` on it: 27 | ```rust 28 | use deserr::{DeserializeError, Deserr, IntoValue, Value, ValuePointerRef}; 29 | 30 | #[derive(Debug, Clone, PartialEq, Eq, Copy)] 31 | pub enum Setting { 32 | Set(T), 33 | Reset, 34 | NotSet, 35 | } 36 | 37 | // If the value is missing we're going to rely on its default implementation of `NotSet`. 38 | impl Default for Setting { 39 | fn default() -> Self { 40 | Self::NotSet 41 | } 42 | } 43 | 44 | impl Deserr for Setting 45 | where 46 | T: Deserr, 47 | // We didn't put any constraint on the error type, that means it's up to the caller to decide the type of errors to return 48 | E: DeserializeError, 49 | { 50 | fn deserialize_from_value( 51 | value: Value, 52 | location: ValuePointerRef<'_>, 53 | ) -> Result { 54 | match value { 55 | deserr::Value::Null => Ok(Setting::Reset), 56 | // If the value contains something, we let the inner type deserialize it 57 | _ => T::deserialize_from_value(value, location).map(Setting::Set), 58 | } 59 | } 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | mod attribute_parser; 4 | mod derive_enum; 5 | mod derive_named_fields; 6 | mod derive_struct; 7 | mod derive_user_provided_function; 8 | mod parse_type; 9 | 10 | use attribute_parser::TagType; 11 | use derive_named_fields::generate_named_fields_impl; 12 | use parse_type::{DerivedTypeInfo, TraitImplementationInfo, VariantData}; 13 | use proc_macro::TokenStream; 14 | use proc_macro2::Span; 15 | use syn::{parse_macro_input, DeriveInput}; 16 | 17 | #[proc_macro_derive(Deserr, attributes(deserr, serde))] 18 | pub fn derive_deserialize(input: TokenStream) -> TokenStream { 19 | let input = parse_macro_input!(input as DeriveInput); 20 | 21 | match DerivedTypeInfo::parse(input) { 22 | Ok(derived_type_info) => match derived_type_info.data { 23 | TraitImplementationInfo::Struct(fields) => { 24 | derive_struct::generate_derive_struct_impl(derived_type_info.common, fields).into() 25 | } 26 | TraitImplementationInfo::Enum { tag, variants } => match tag { 27 | TagType::Internal(tag_key) => derive_enum::generate_derive_tagged_enum_impl( 28 | derived_type_info.common, 29 | tag_key, 30 | variants, 31 | ) 32 | .into(), 33 | TagType::External 34 | if variants 35 | .iter() 36 | .all(|variant| matches!(variant.data, VariantData::Unit)) => 37 | { 38 | derive_enum::generate_derive_untagged_enum_impl( 39 | derived_type_info.common, 40 | variants, 41 | ) 42 | .into() 43 | } 44 | TagType::External => 45 | syn::Error::new( 46 | Span::call_site(), 47 | r#"Externally tagged enums are not supported yet by deserr. Add #[deserr(tag = "some_tag_key")]"#, 48 | ).to_compile_error().into() 49 | }, 50 | TraitImplementationInfo::UnfallibleUserProvidedFunction { from_attr } => { 51 | derive_user_provided_function::generate_derive_from_user_function( 52 | derived_type_info.common, 53 | from_attr, 54 | ) 55 | .into() 56 | } 57 | TraitImplementationInfo::FallibleUserProvidedFunction { try_from_attr } => { 58 | derive_user_provided_function::generate_derive_try_from_user_function( 59 | derived_type_info.common, 60 | try_from_attr, 61 | ) 62 | .into() 63 | } 64 | }, 65 | Err(e) => e.to_compile_error().into(), 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/axum/serde_json.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::errors::JsonError; 4 | use crate::{DeserializeError, Deserr}; 5 | use axum::extract::rejection::JsonRejection; 6 | use axum::extract::FromRequest; 7 | use axum::response::IntoResponse; 8 | use axum::Json; 9 | use http::StatusCode; 10 | 11 | /// Extractor for typed data from Json request payloads 12 | /// deserialised by deserr. 13 | /// 14 | /// ## Extractor 15 | /// To extract typed data from a request body, the inner type `T` must implement the 16 | /// [`deserr::Deserr`] trait. The inner type `E` must implement the 17 | /// [`DeserializeError`] trait. 18 | #[derive(Debug)] 19 | pub struct AxumJson(pub T, PhantomData); 20 | 21 | impl AxumJson { 22 | pub fn new(data: T) -> Self { 23 | AxumJson(data, PhantomData) 24 | } 25 | 26 | pub fn into_inner(self) -> T { 27 | self.0 28 | } 29 | } 30 | 31 | #[derive(Debug)] 32 | pub enum AxumJsonRejection { 33 | DeserrError(E), 34 | JsonRejection(JsonRejection), 35 | } 36 | 37 | impl std::fmt::Display for AxumJsonRejection { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | match self { 40 | AxumJsonRejection::DeserrError(e) => e.fmt(f), 41 | AxumJsonRejection::JsonRejection(e) => e.fmt(f), 42 | } 43 | } 44 | } 45 | 46 | impl FromRequest for AxumJson 47 | where 48 | E: DeserializeError + IntoResponse + 'static, 49 | T: Deserr, 50 | S: Send + Sync, 51 | { 52 | type Rejection = AxumJsonRejection; 53 | 54 | async fn from_request(req: axum::extract::Request, state: &S) -> Result { 55 | let Json(value) = Json::::from_request(req, state).await?; 56 | let data = deserr::deserialize::<_, _, _>(value)?; 57 | Ok(AxumJson(data, PhantomData)) 58 | } 59 | } 60 | 61 | impl From for AxumJsonRejection { 62 | fn from(value: E) -> Self { 63 | AxumJsonRejection::DeserrError(value) 64 | } 65 | } 66 | 67 | impl From for AxumJsonRejection { 68 | fn from(value: JsonRejection) -> Self { 69 | AxumJsonRejection::JsonRejection(value) 70 | } 71 | } 72 | 73 | impl IntoResponse for AxumJsonRejection { 74 | fn into_response(self) -> axum::response::Response { 75 | match self { 76 | AxumJsonRejection::DeserrError(e) => e.into_response(), 77 | AxumJsonRejection::JsonRejection(e) => e.into_response(), 78 | } 79 | } 80 | } 81 | 82 | impl IntoResponse for JsonError { 83 | fn into_response(self) -> axum::response::Response { 84 | (StatusCode::BAD_REQUEST, self.to_string()).into_response() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/attributes/default.rs: -------------------------------------------------------------------------------- 1 | use deserr::{deserialize, Deserr, JsonError}; 2 | use insta::{assert_debug_snapshot, assert_snapshot}; 3 | use serde_json::json; 4 | 5 | #[test] 6 | fn option_dont_use_default_by_default() { 7 | #[allow(unused)] 8 | #[derive(Debug, Deserr)] 9 | struct Struct { 10 | doggo: Option, 11 | } 12 | 13 | let data = deserialize::(json!({ "doggo": null })).unwrap(); 14 | 15 | assert_debug_snapshot!(data, @r###" 16 | Struct { 17 | doggo: None, 18 | } 19 | "###); 20 | 21 | let data = deserialize::(json!({ "doggo": "bork" })).unwrap(); 22 | 23 | assert_debug_snapshot!(data, @r###" 24 | Struct { 25 | doggo: Some( 26 | "bork", 27 | ), 28 | } 29 | "###); 30 | 31 | let data = deserialize::(json!({})).unwrap_err(); 32 | 33 | assert_snapshot!(data, @"Json deserialize error: missing field `doggo` at ``"); 34 | } 35 | 36 | #[test] 37 | fn default_without_parameter() { 38 | #[allow(unused)] 39 | #[derive(Debug, Deserr)] 40 | struct Struct { 41 | #[deserr(default)] 42 | doggo: Option, 43 | } 44 | 45 | let data = deserialize::(json!({})).unwrap(); 46 | 47 | assert_debug_snapshot!(data, @r###" 48 | Struct { 49 | doggo: None, 50 | } 51 | "###); 52 | 53 | let data = deserialize::(json!({ "doggo": null })).unwrap(); 54 | 55 | assert_debug_snapshot!(data, @r###" 56 | Struct { 57 | doggo: None, 58 | } 59 | "###); 60 | 61 | let data = deserialize::(json!({ "doggo": "bork" })).unwrap(); 62 | 63 | assert_debug_snapshot!(data, @r###" 64 | Struct { 65 | doggo: Some( 66 | "bork", 67 | ), 68 | } 69 | "###); 70 | } 71 | 72 | #[test] 73 | fn default_with_a_parameter() { 74 | #[allow(unused)] 75 | #[derive(Debug, Deserr)] 76 | struct Struct { 77 | #[deserr(default = Some(String::from("BORK")))] 78 | doggo: Option, 79 | } 80 | 81 | let data = deserialize::(json!({})).unwrap(); 82 | 83 | assert_debug_snapshot!(data, @r###" 84 | Struct { 85 | doggo: Some( 86 | "BORK", 87 | ), 88 | } 89 | "###); 90 | 91 | let data = deserialize::(json!({ "doggo": null })).unwrap(); 92 | 93 | assert_debug_snapshot!(data, @r###" 94 | Struct { 95 | doggo: None, 96 | } 97 | "###); 98 | 99 | let data = deserialize::(json!({ "doggo": "bork" })).unwrap(); 100 | 101 | assert_debug_snapshot!(data, @r###" 102 | Struct { 103 | doggo: Some( 104 | "bork", 105 | ), 106 | } 107 | "###); 108 | } 109 | -------------------------------------------------------------------------------- /src/actix_web/serde_json.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::future::Future; 3 | use std::marker::PhantomData; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | 7 | use actix_web::dev::Payload; 8 | use actix_web::web::Json; 9 | use actix_web::{FromRequest, HttpRequest, ResponseError}; 10 | use deserr::{DeserializeError, Deserr}; 11 | use futures::ready; 12 | 13 | use crate::errors::JsonError; 14 | 15 | /// Extractor for typed data from Json request payloads 16 | /// deserialised by deserr. 17 | /// 18 | /// # Extractor 19 | /// To extract typed data from a request body, the inner type `T` must implement the 20 | /// [`deserr::Deserr`] trait. The inner type `E` must implement the 21 | /// [`DeserializeError`] + `ResponseError` traits. 22 | #[derive(Debug)] 23 | pub struct AwebJson(pub T, PhantomData<*const E>); 24 | 25 | impl AwebJson { 26 | pub fn new(data: T) -> Self { 27 | AwebJson(data, PhantomData) 28 | } 29 | 30 | pub fn into_inner(self) -> T { 31 | self.0 32 | } 33 | } 34 | 35 | impl FromRequest for AwebJson 36 | where 37 | E: DeserializeError + ResponseError + 'static, 38 | T: Deserr, 39 | { 40 | type Error = actix_web::Error; 41 | type Future = AwebJsonExtractFut; 42 | 43 | #[inline] 44 | fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { 45 | AwebJsonExtractFut { 46 | fut: Json::::from_request(req, payload), 47 | _phantom: PhantomData, 48 | } 49 | } 50 | } 51 | 52 | pub struct AwebJsonExtractFut { 53 | fut: as FromRequest>::Future, 54 | _phantom: PhantomData<*const (T, E)>, 55 | } 56 | 57 | impl Future for AwebJsonExtractFut 58 | where 59 | T: Deserr, 60 | E: DeserializeError + ResponseError + 'static, 61 | { 62 | type Output = Result, actix_web::Error>; 63 | 64 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 65 | let AwebJsonExtractFut { fut, .. } = self.get_mut(); 66 | let fut = Pin::new(fut); 67 | 68 | let res = ready!(fut.poll(cx)); 69 | 70 | let res = match res { 71 | Err(err) => Err(err), 72 | Ok(data) => match deserr::deserialize::<_, _, E>(data.into_inner()) { 73 | Ok(data) => Ok(AwebJson::new(data)), 74 | Err(e) => Err(e)?, 75 | }, 76 | }; 77 | 78 | Poll::Ready(res) 79 | } 80 | } 81 | 82 | impl actix_web::ResponseError for JsonError { 83 | fn status_code(&self) -> actix_web::http::StatusCode { 84 | actix_web::http::StatusCode::BAD_REQUEST 85 | } 86 | 87 | fn error_response(&self) -> actix_web::HttpResponse { 88 | actix_web::HttpResponseBuilder::new(self.status_code()) 89 | .content_type("text/plain") 90 | .body(self.to_string()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/attributes/from.rs: -------------------------------------------------------------------------------- 1 | use deserr::{deserialize, errors::JsonError, Deserr}; 2 | use insta::assert_debug_snapshot; 3 | use serde_json::json; 4 | 5 | #[test] 6 | fn from_container_attribute() { 7 | #[allow(unused)] 8 | #[derive(Debug, Deserr)] 9 | #[deserr(from(String) = From::from)] 10 | enum AsciiString { 11 | Valid(String), 12 | Invalid(String), 13 | } 14 | 15 | impl From for AsciiString { 16 | fn from(s: String) -> Self { 17 | if !s.is_ascii() { 18 | Self::Invalid(s) 19 | } else { 20 | Self::Valid(s) 21 | } 22 | } 23 | } 24 | 25 | let data = deserialize::(json!("doggo")).unwrap(); 26 | 27 | assert_debug_snapshot!(data, @r###" 28 | Valid( 29 | "doggo", 30 | ) 31 | "###); 32 | 33 | let data = deserialize::(json!("🥺")).unwrap(); 34 | 35 | assert_debug_snapshot!(data, @r###" 36 | Invalid( 37 | "🥺", 38 | ) 39 | "###); 40 | 41 | #[allow(unused)] 42 | #[derive(Debug, Deserr)] 43 | struct Struct { 44 | #[deserr(needs_predicate)] 45 | doggo: AsciiString, 46 | } 47 | 48 | let data = deserialize::(json!({ "doggo": "BORK" })).unwrap(); 49 | 50 | assert_debug_snapshot!(data, @r###" 51 | Struct { 52 | doggo: Valid( 53 | "BORK", 54 | ), 55 | } 56 | "###); 57 | 58 | let data = deserialize::(json!({ "doggo": "👉 👈"})).unwrap(); 59 | 60 | assert_debug_snapshot!(data, @r###" 61 | Struct { 62 | doggo: Invalid( 63 | "👉 👈", 64 | ), 65 | } 66 | "###); 67 | } 68 | 69 | #[test] 70 | fn from_field_attribute() { 71 | #[allow(unused)] 72 | #[derive(Debug)] 73 | enum AsciiString { 74 | Valid(String), 75 | Invalid(String), 76 | } 77 | 78 | impl From for AsciiString { 79 | fn from(s: String) -> Self { 80 | if !s.is_ascii() { 81 | Self::Invalid(s) 82 | } else { 83 | Self::Valid(s) 84 | } 85 | } 86 | } 87 | 88 | #[allow(unused)] 89 | #[derive(Debug, Deserr)] 90 | struct Struct { 91 | #[deserr(from(String) = From::from)] 92 | doggo: AsciiString, 93 | } 94 | 95 | let data = deserialize::(json!({ "doggo": "BORK" })).unwrap(); 96 | 97 | assert_debug_snapshot!(data, @r###" 98 | Struct { 99 | doggo: Valid( 100 | "BORK", 101 | ), 102 | } 103 | "###); 104 | 105 | let data = deserialize::(json!({ "doggo": "👉 👈"})).unwrap(); 106 | 107 | assert_debug_snapshot!(data, @r###" 108 | Struct { 109 | doggo: Invalid( 110 | "👉 👈", 111 | ), 112 | } 113 | "###); 114 | } 115 | -------------------------------------------------------------------------------- /tests/attributes/rename_all.rs: -------------------------------------------------------------------------------- 1 | use deserr::{deserialize, errors::JsonError, Deserr}; 2 | use insta::{assert_debug_snapshot, assert_snapshot}; 3 | use serde_json::json; 4 | 5 | #[test] 6 | fn rename_all_camel_case() { 7 | #[allow(unused)] 8 | #[derive(Debug, Deserr)] 9 | #[deserr(rename_all = camelCase)] 10 | struct Struct { 11 | word: String, 12 | multiple_words: String, 13 | #[deserr(rename = "renamed_field")] 14 | renamed_field: String, 15 | } 16 | 17 | let data = deserialize::( 18 | json!({ "word": "doggo", "multipleWords": "good doggo", "renamed_field": "bork" }), 19 | ) 20 | .unwrap(); 21 | 22 | assert_debug_snapshot!(data, @r###" 23 | Struct { 24 | word: "doggo", 25 | multiple_words: "good doggo", 26 | renamed_field: "bork", 27 | } 28 | "###); 29 | 30 | let data = deserialize::( 31 | json!({ "Word": "doggo", "multipleWords": "good doggo", "renamed_field": "bork" }), 32 | ) 33 | .unwrap_err(); 34 | 35 | assert_snapshot!(data, @"Missing field `word`"); 36 | 37 | let data = deserialize::( 38 | json!({ "word": "doggo", "multiple_words": "good doggo", "renamed_field": "bork" }), 39 | ) 40 | .unwrap_err(); 41 | 42 | assert_snapshot!(data, @"Missing field `multipleWords`"); 43 | 44 | let data = deserialize::( 45 | json!({ "word": "doggo", "multipleWords": "good doggo", "renamedField": "bork" }), 46 | ) 47 | .unwrap_err(); 48 | 49 | assert_snapshot!(data, @"Missing field `renamed_field`"); 50 | } 51 | 52 | #[allow(non_snake_case)] 53 | #[test] 54 | fn rename_all_lowercase() { 55 | #[allow(unused)] 56 | #[derive(Debug, Deserr)] 57 | #[deserr(rename_all = lowercase)] 58 | struct Struct { 59 | word: String, 60 | SCREAMING_WORD: String, 61 | #[deserr(rename = "BORK")] 62 | smol: String, 63 | } 64 | 65 | let data = deserialize::( 66 | json!({ "word": "doggo", "screaming_word": "good doggo", "BORK": "bork" }), 67 | ) 68 | .unwrap(); 69 | 70 | assert_debug_snapshot!(data, @r###" 71 | Struct { 72 | word: "doggo", 73 | SCREAMING_WORD: "good doggo", 74 | smol: "bork", 75 | } 76 | "###); 77 | 78 | let data = deserialize::( 79 | json!({ "Word": "doggo", "SCREAMING_WORD": "good doggo", "BORK": "bork" }), 80 | ) 81 | .unwrap_err(); 82 | 83 | assert_snapshot!(data, @"Missing field `word`"); 84 | 85 | let data = deserialize::( 86 | json!({ "word": "doggo", "screamingWord": "good doggo", "BORK": "bork" }), 87 | ) 88 | .unwrap_err(); 89 | 90 | assert_snapshot!(data, @"Missing field `screaming_word`"); 91 | 92 | let data = deserialize::( 93 | json!({ "word": "doggo", "screaming_word": "good doggo", "smol": "bork" }), 94 | ) 95 | .unwrap_err(); 96 | 97 | assert_snapshot!(data, @"Missing field `BORK`"); 98 | } 99 | -------------------------------------------------------------------------------- /derive/src/derive_user_provided_function.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | attribute_parser::{AttributeFrom, AttributeTryFrom, FunctionReturningError}, 3 | parse_type::CommonDerivedTypeInfo, 4 | }; 5 | use proc_macro2::TokenStream; 6 | use quote::quote; 7 | 8 | /// Return a token stream that implements `Deserr` by calling the user-provided function 9 | pub fn generate_derive_try_from_user_function( 10 | info: CommonDerivedTypeInfo, 11 | from_attr: AttributeTryFrom, 12 | ) -> TokenStream { 13 | let CommonDerivedTypeInfo { 14 | impl_trait_tokens, 15 | err_ty, 16 | validate, 17 | } = info; 18 | 19 | let AttributeTryFrom { 20 | is_ref, 21 | try_from_ty: from_ty, 22 | function: 23 | FunctionReturningError { 24 | function, 25 | error_ty: function_error_ty, 26 | }, 27 | .. 28 | } = from_attr; 29 | 30 | let function_call = if is_ref { 31 | quote! { #function (&deserr_from__) } 32 | } else { 33 | quote! { #function (deserr_from__) } 34 | }; 35 | 36 | quote! { 37 | #impl_trait_tokens { 38 | fn deserialize_from_value(deserr_value__: ::deserr::Value, deserr_location__: ::deserr::ValuePointerRef) -> ::std::result::Result { 39 | // first create the intermediate from_ty 40 | let deserr_from__ = <#from_ty as ::deserr::Deserr<#err_ty>>::deserialize_from_value(deserr_value__, deserr_location__)?; 41 | // then apply the function to it 42 | let deserr_final__ = #function_call.map_err(|e| { 43 | // then map the error to the final error type 44 | ::deserr::take_cf_content( 45 | <#err_ty as ::deserr::MergeWithError<#function_error_ty>>::merge(None, e, deserr_location__) 46 | ) 47 | })?; 48 | #validate 49 | } 50 | } 51 | } 52 | } 53 | 54 | /// Return a token stream that implements `Deserr` by calling the user-provided function 55 | pub fn generate_derive_from_user_function( 56 | info: CommonDerivedTypeInfo, 57 | from_attr: AttributeFrom, 58 | ) -> TokenStream { 59 | let CommonDerivedTypeInfo { 60 | impl_trait_tokens, 61 | err_ty, 62 | validate, 63 | } = info; 64 | 65 | let AttributeFrom { 66 | is_ref, 67 | from_ty, 68 | function, 69 | .. 70 | } = from_attr; 71 | 72 | let function_call = if is_ref { 73 | quote! { #function (&deserr_from__) } 74 | } else { 75 | quote! { #function (deserr_from__) } 76 | }; 77 | 78 | quote! { 79 | #impl_trait_tokens { 80 | fn deserialize_from_value(deserr_value__: ::deserr::Value, deserr_location__: ::deserr::ValuePointerRef) -> ::std::result::Result { 81 | // first create the intermediate from_ty 82 | let deserr_from__ = <#from_ty as ::deserr::Deserr<#err_ty>>::deserialize_from_value(deserr_value__, deserr_location__)?; 83 | // then apply the function to it 84 | let deserr_final__ = #function_call; 85 | #validate 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

deserr

3 | 4 | [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE-MIT) 5 | [![License](https://img.shields.io/badge/license-apache-green)](LICENSE-APACHE) 6 | [![Crates.io](https://img.shields.io/crates/v/deserr)](https://crates.io/crates/deserr) 7 | [![Docs](https://docs.rs/deserr/badge.svg)](https://docs.rs/deserr) 8 | [![dependency status](https://deps.rs/repo/github/meilisearch/deserr/status.svg)](https://deps.rs/repo/github/meilisearch/deserr) 9 | 10 | Deserr is a crate for deserializing data, with the ability to return 11 | custom, type-specific errors upon failure. It was also designed with 12 | user-facing APIs in mind and thus provides better defaults than serde for 13 | this use case. 14 | 15 | Unlike serde, deserr does not parse the data in its serialization format itself 16 | but offloads the work to other crates. Instead, it deserializes 17 | the already-parsed serialized data into the final type. For example: 18 | 19 | ```rust,ignore 20 | // bytes of the serialized value 21 | let s: &str = ".." ; 22 | // parse serialized data using another crate, such as `serde_json` 23 | let json: serde_json::Value = serde_json::from_str(s).unwrap(); 24 | // finally deserialize with deserr 25 | let data = T::deserialize_from_value(json.into_value()).unwrap(); 26 | // `T` must implement `Deserr`. 27 | ``` 28 | 29 | You may be looking for: 30 | - [The `docs.rs` documentation](https://docs.rs/deserr/latest/deserr/) 31 | - [The reference book](https://meilisearch.github.io/deserr/overview.html) 32 | 33 | ### FAQ 34 | 35 | #### But why? 36 | At Meilisearch, we wanted to customize the error code we return when we fail 37 | the deserialization of a specific field. 38 | Some error messages were also not clear at all and impossible to edit. 39 | 40 | #### What about the maintenance? 41 | At Meilisearch we're already using deserr in production; thus, it's well maintained. 42 | 43 | #### Where can I see more examples of usage of this crate? 44 | Currently, you can read our examples in the `examples` directory of this repository. 45 | You can also look at our integration test; each attribute has a simple-to-read test. 46 | 47 | And obviously, you can read the code of Meilisearch where deserr is used on all our 48 | routes. 49 | 50 | #### My question is not listed 51 | Please, if you think there is a bug in this lib or would like a new feature, 52 | open an issue or a discussion. 53 | If you would like to chat more directly with us, you can join us on discord 54 | at https://discord.com/invite/meilisearch 55 | 56 | #### The logo 57 | The logo was graciously offered and crafted by @irevoire 's sister after a lot of back and forth. 58 | Many thanks to her. 59 | 60 | #### License 61 | 62 | 63 | Licensed under either of Apache License, Version 64 | 2.0 or MIT license at your option. 65 | 66 | 67 |
68 | 69 | 70 | Unless you explicitly state otherwise, any contribution intentionally submitted 71 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 72 | be dual licensed as above, without any additional terms or conditions. 73 | 74 | -------------------------------------------------------------------------------- /examples/actix_web_server/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use actix_web::{web, App, HttpResponse, HttpServer}; 4 | use deserr::{ 5 | actix_web::AwebJson, errors::JsonError, take_cf_content, DeserializeError, Deserr, ErrorKind, 6 | ValuePointerRef, 7 | }; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Debug, Serialize, Deserialize, Deserr)] 11 | #[serde(deny_unknown_fields)] 12 | #[deserr(deny_unknown_fields)] 13 | struct Query { 14 | name: String, 15 | 16 | // deserr don't do anything strange with `Option`, if you don't 17 | // want to make the `Option` mandatory specify it. 18 | #[deserr(default)] 19 | number: Option, 20 | 21 | // you can put expression in the default values 22 | #[serde(default = "default_range")] 23 | #[deserr(default = Range { min: 2, max: 4 })] 24 | range: Range, 25 | 26 | // serde support a wide variety of enums, but deserr only support 27 | // tagged enums, or unit enum as value. 28 | #[serde(rename = "return")] 29 | #[deserr(rename = "return")] 30 | returns: Return, 31 | } 32 | 33 | fn default_range() -> Range { 34 | Range { min: 2, max: 4 } 35 | } 36 | 37 | #[derive(Debug, Serialize, Deserialize, Deserr)] 38 | #[serde(deny_unknown_fields)] 39 | #[deserr(deny_unknown_fields, validate = validate_range -> __Deserr_E)] 40 | struct Range { 41 | min: u8, 42 | max: u8, 43 | } 44 | 45 | // Here we could specify the error type we're going to return or stay entirely generic so the 46 | // final caller can decide which implementation of error handler will generate the error message. 47 | fn validate_range( 48 | range: Range, 49 | location: ValuePointerRef, 50 | ) -> Result { 51 | if range.min > range.max { 52 | Err(take_cf_content(E::error::( 53 | None, 54 | ErrorKind::Unexpected { 55 | msg: format!( 56 | "`max` (`{}`) should be greater than `min` (`{}`)", 57 | range.max, range.min 58 | ), 59 | }, 60 | location, 61 | ))) 62 | } else { 63 | Ok(range) 64 | } 65 | } 66 | 67 | #[derive(Debug, Serialize, Deserialize, Deserr)] 68 | #[serde(rename_all = "camelCase")] 69 | #[deserr(rename_all = camelCase)] 70 | enum Return { 71 | Name, 72 | Number, 73 | } 74 | 75 | /// This handler uses the official `actix_web` `serde_json` extractor 76 | async fn serde(item: web::Json) -> HttpResponse { 77 | if item.range.min > item.range.max { 78 | HttpResponse::BadRequest().body(format!( 79 | "`max` (`{}`) should be greater than `min` (`{}`)", 80 | item.range.max, item.range.min 81 | )) 82 | } else { 83 | HttpResponse::Ok().json(item.0) 84 | } 85 | } 86 | 87 | /// This handler uses the official `actix_web` `serde_json` extractor 88 | async fn deserr(item: AwebJson) -> HttpResponse { 89 | HttpResponse::Ok().json(item.0) 90 | } 91 | 92 | #[actix_web::main] 93 | async fn main() -> std::io::Result<()> { 94 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 95 | 96 | log::info!("starting HTTP server at http://localhost:8080"); 97 | 98 | HttpServer::new(|| { 99 | App::new() 100 | .service(web::resource("/serde").route(web::post().to(serde))) 101 | .service(web::resource("/deserr").route(web::post().to(deserr))) 102 | }) 103 | .bind(("127.0.0.1", 8080))? 104 | .run() 105 | .await 106 | } 107 | -------------------------------------------------------------------------------- /tests/attributes/tag.rs: -------------------------------------------------------------------------------- 1 | use deserr::{deserialize, errors::JsonError, Deserr}; 2 | use insta::{assert_debug_snapshot, assert_snapshot}; 3 | use serde_json::json; 4 | 5 | #[test] 6 | fn tagged_enum() { 7 | #[allow(unused)] 8 | #[derive(Debug, Deserr)] 9 | struct Struct { 10 | either: Either, 11 | } 12 | 13 | #[allow(unused)] 14 | #[derive(Debug, Deserr)] 15 | #[deserr(tag = "type")] 16 | enum Either { 17 | Left { doggo: String }, 18 | Right { doggo: bool, catto: String }, 19 | } 20 | 21 | let data = deserialize::( 22 | json!({ "either": { "type": "Left", "doggo": "bork" } }), 23 | ) 24 | .unwrap(); 25 | 26 | assert_debug_snapshot!(data, @r###" 27 | Struct { 28 | either: Left { 29 | doggo: "bork", 30 | }, 31 | } 32 | "###); 33 | 34 | let data = deserialize::( 35 | json!({ "either": { "type": "Right", "doggo": false, "catto": "jorts" } }), 36 | ) 37 | .unwrap(); 38 | 39 | assert_debug_snapshot!(data, @r###" 40 | Struct { 41 | either: Right { 42 | doggo: false, 43 | catto: "jorts", 44 | }, 45 | } 46 | "###); 47 | 48 | let data = 49 | deserialize::(json!({ "either": { "doggo": "bork" } })).unwrap_err(); 50 | 51 | assert_snapshot!(data, @"Missing field `type` inside `.either`"); 52 | 53 | let data = deserialize::( 54 | json!({ "either": { "doggo": false, "catto": "jorts" } }), 55 | ) 56 | .unwrap_err(); 57 | 58 | assert_snapshot!(data, @"Missing field `type` inside `.either`"); 59 | } 60 | #[test] 61 | fn tagged_enum_plus_rename() { 62 | #[allow(unused)] 63 | #[derive(Debug, Deserr)] 64 | struct Struct { 65 | either: Either, 66 | } 67 | 68 | #[allow(unused)] 69 | #[derive(Debug, Deserr)] 70 | #[deserr(tag = "type", rename_all = lowercase)] 71 | enum Either { 72 | Left { 73 | doggo: String, 74 | }, 75 | #[deserr(rename = "RIGHT")] 76 | Right { 77 | doggo: bool, 78 | catto: String, 79 | }, 80 | } 81 | 82 | let data = deserialize::( 83 | json!({ "either": { "type": "left", "doggo": "bork" } }), 84 | ) 85 | .unwrap(); 86 | 87 | assert_debug_snapshot!(data, @r###" 88 | Struct { 89 | either: Left { 90 | doggo: "bork", 91 | }, 92 | } 93 | "###); 94 | 95 | let data = deserialize::( 96 | json!({ "either": { "type": "RIGHT", "doggo": false, "catto": "jorts" } }), 97 | ) 98 | .unwrap(); 99 | 100 | assert_debug_snapshot!(data, @r###" 101 | Struct { 102 | either: Right { 103 | doggo: false, 104 | catto: "jorts", 105 | }, 106 | } 107 | "###); 108 | 109 | let data = deserialize::( 110 | json!({ "either": { "type": "Left", "doggo": "bork" } }), 111 | ) 112 | .unwrap_err(); 113 | 114 | assert_debug_snapshot!(data, @r###" 115 | JsonError( 116 | "Invalid value at `.either`: Incorrect tag value", 117 | ) 118 | "###); 119 | 120 | let data = deserialize::( 121 | json!({ "either": { "type": "Right", "doggo": false, "catto": "jorts" } }), 122 | ) 123 | .unwrap_err(); 124 | 125 | assert_debug_snapshot!(data, @r###" 126 | JsonError( 127 | "Invalid value at `.either`: Incorrect tag value", 128 | ) 129 | "###); 130 | } 131 | -------------------------------------------------------------------------------- /derive/src/derive_named_fields.rs: -------------------------------------------------------------------------------- 1 | use crate::parse_type::NamedFieldsInfo; 2 | use proc_macro2::TokenStream; 3 | use quote::quote; 4 | 5 | pub fn generate_named_fields_impl( 6 | fields: &NamedFieldsInfo, 7 | err_ty: &syn::Type, 8 | create: TokenStream, 9 | ) -> TokenStream { 10 | let NamedFieldsInfo { 11 | field_names, 12 | field_tys, 13 | field_defaults, 14 | field_errs, 15 | field_from_fns, 16 | field_from_errors: _, 17 | field_maps, 18 | missing_field_errors, 19 | key_names, 20 | unknown_key, 21 | needs_predicate: _, 22 | } = fields; 23 | quote! { 24 | // Start by declaring all the fields as mutable optionals 25 | // Their initial value is given by the precomputed `#field_defaults`, 26 | // see [NamedFieldsInfo] and [NamedFieldsInfo::parse]. 27 | // 28 | // The initial value of #field_names is `None` if the field has no initial value and 29 | // thus must be given by the map, and `Some` otherwise. 30 | #( 31 | let mut #field_names : ::deserr::FieldState<_> = #field_defaults ; 32 | )* 33 | // We traverse the entire map instead of looking for specific keys, because we want 34 | // to handle the case where a key is unknown and the attribute `deny_unknown_fields` was used. 35 | for (deserr_key__, deserr_value__) in ::deserr::Map::into_iter(deserr_map__) { 36 | match deserr_key__.as_str() { 37 | // For each known key, look at the corresponding value and try to deserialize it 38 | 39 | #( 40 | #key_names => { 41 | #field_names = match 42 | <#field_tys as ::deserr::Deserr<#field_errs>>::deserialize_from_value( 43 | ::deserr::IntoValue::into_value(deserr_value__), 44 | deserr_location__.push_key(deserr_key__.as_str()) 45 | ) { 46 | ::std::result::Result::Ok(x) => { 47 | #field_from_fns 48 | }, 49 | ::std::result::Result::Err(e) => { 50 | deserr_error__ = match <#err_ty as ::deserr::MergeWithError<_>>::merge( 51 | deserr_error__, 52 | e, 53 | deserr_location__.push_key(deserr_key__.as_str()) 54 | ) { 55 | ::std::ops::ControlFlow::Continue(e) => ::std::option::Option::Some(e), 56 | ::std::ops::ControlFlow::Break(e) => return ::std::result::Result::Err(e), 57 | }; 58 | ::deserr::FieldState::Err 59 | } 60 | }; 61 | } 62 | )* 63 | // For an unknown key, use the precomputed #unknown_key token stream 64 | deserr_key__ => { 65 | #unknown_key 66 | } 67 | } 68 | } 69 | // Now we check whether any field was missing 70 | #( 71 | if #field_names .is_missing() { 72 | #missing_field_errors 73 | } 74 | )* 75 | 76 | if let Some(deserr_error__) = deserr_error__ { 77 | ::std::result::Result::Err(deserr_error__) 78 | } else { 79 | // If the deserialization was successful, then all #field_names are `Some(..)` 80 | // Otherwise, an error was thrown earlier 81 | ::std::result::Result::Ok(#create { 82 | #( 83 | #field_names : #field_names.map(#field_maps).unwrap(), 84 | )* 85 | }) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/attributes/skip.rs: -------------------------------------------------------------------------------- 1 | use deserr::{deserialize, errors::JsonError, Deserr}; 2 | use insta::assert_debug_snapshot; 3 | use serde_json::json; 4 | 5 | #[test] 6 | fn skip() { 7 | #[allow(unused)] 8 | #[derive(Debug, Deserr)] 9 | struct Struct { 10 | #[deserr(skip)] 11 | doggo: Option, 12 | } 13 | 14 | let data = deserialize::(json!({})).unwrap(); 15 | 16 | assert_debug_snapshot!(data, @r###" 17 | Struct { 18 | doggo: None, 19 | } 20 | "###); 21 | 22 | let data = deserialize::(json!({ "doggo": null })).unwrap(); 23 | 24 | assert_debug_snapshot!(data, @r###" 25 | Struct { 26 | doggo: None, 27 | } 28 | "###); 29 | 30 | let data = deserialize::(json!({ "doggo": "bork" })).unwrap(); 31 | 32 | assert_debug_snapshot!(data, @r###" 33 | Struct { 34 | doggo: None, 35 | } 36 | "###); 37 | } 38 | 39 | #[test] 40 | fn skip_and_deny_unknown_fields() { 41 | #[allow(unused)] 42 | #[derive(Debug, Deserr)] 43 | #[deserr(deny_unknown_fields)] 44 | struct Struct { 45 | #[deserr(skip)] 46 | doggo: Option, 47 | } 48 | 49 | let data = deserialize::(json!({})).unwrap(); 50 | 51 | assert_debug_snapshot!(data, @r###" 52 | Struct { 53 | doggo: None, 54 | } 55 | "###); 56 | 57 | let data = deserialize::(json!({ "doggo": null })).unwrap_err(); 58 | 59 | assert_debug_snapshot!(data, @r###" 60 | JsonError( 61 | "Unknown field `doggo`: expected one of ", 62 | ) 63 | "###); 64 | 65 | let data = deserialize::(json!({ "doggo": "bork" })).unwrap_err(); 66 | 67 | assert_debug_snapshot!(data, @r###" 68 | JsonError( 69 | "Unknown field `doggo`: expected one of ", 70 | ) 71 | "###); 72 | } 73 | 74 | #[test] 75 | fn skip_and_default() { 76 | #[allow(unused)] 77 | #[derive(Debug, Deserr)] 78 | struct Struct { 79 | #[deserr(skip, default = Some(String::from("bork")))] 80 | doggo: Option, 81 | } 82 | 83 | let data = deserialize::(json!({})).unwrap(); 84 | 85 | assert_debug_snapshot!(data, @r###" 86 | Struct { 87 | doggo: Some( 88 | "bork", 89 | ), 90 | } 91 | "###); 92 | 93 | let data = deserialize::(json!({ "doggo": null })).unwrap(); 94 | 95 | assert_debug_snapshot!(data, @r###" 96 | Struct { 97 | doggo: Some( 98 | "bork", 99 | ), 100 | } 101 | "###); 102 | 103 | let data = deserialize::(json!({ "doggo": "bork" })).unwrap(); 104 | 105 | assert_debug_snapshot!(data, @r###" 106 | Struct { 107 | doggo: Some( 108 | "bork", 109 | ), 110 | } 111 | "###); 112 | } 113 | 114 | #[test] 115 | fn skip_and_default_and_deny_unknown_fields() { 116 | #[allow(unused)] 117 | #[derive(Debug, Deserr)] 118 | #[deserr(deny_unknown_fields)] 119 | struct Struct { 120 | #[deserr(skip, default = Some(String::from("bork")))] 121 | doggo: Option, 122 | } 123 | 124 | let data = deserialize::(json!({})).unwrap(); 125 | 126 | assert_debug_snapshot!(data, @r###" 127 | Struct { 128 | doggo: Some( 129 | "bork", 130 | ), 131 | } 132 | "###); 133 | 134 | let data = deserialize::(json!({ "doggo": null })).unwrap_err(); 135 | 136 | assert_debug_snapshot!(data, @r###" 137 | JsonError( 138 | "Unknown field `doggo`: expected one of ", 139 | ) 140 | "###); 141 | 142 | let data = deserialize::(json!({ "doggo": "bork" })).unwrap_err(); 143 | 144 | assert_debug_snapshot!(data, @r###" 145 | JsonError( 146 | "Unknown field `doggo`: expected one of ", 147 | ) 148 | "###); 149 | } 150 | -------------------------------------------------------------------------------- /examples/axum/src/main.rs: -------------------------------------------------------------------------------- 1 | use axum::http::StatusCode; 2 | use axum::response::IntoResponse; 3 | use axum::routing::post; 4 | use axum::Json; 5 | use axum::Router; 6 | use deserr::axum::AxumJson; 7 | use deserr::errors::JsonError; 8 | use deserr::take_cf_content; 9 | use deserr::DeserializeError; 10 | use deserr::Deserr; 11 | use deserr::ErrorKind; 12 | use deserr::ValuePointerRef; 13 | use serde::Deserialize; 14 | use serde::Serialize; 15 | use std::convert::Infallible; 16 | use std::net::Ipv4Addr; 17 | use tokio::net::TcpListener; 18 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 19 | 20 | #[derive(Debug, Serialize, Deserialize, Deserr)] 21 | #[serde(deny_unknown_fields)] 22 | #[deserr(deny_unknown_fields)] 23 | struct Query { 24 | name: String, 25 | 26 | // deserr don't do anything strange with `Option`, if you don't 27 | // want to make the `Option` mandatory specify it. 28 | #[deserr(default)] 29 | number: Option, 30 | 31 | // you can put expression in the default values 32 | #[serde(default = "default_range")] 33 | #[deserr(default = Range { min: 2, max: 4 })] 34 | range: Range, 35 | 36 | // serde support a wide variety of enums, but deserr only support 37 | // tagged enums, or unit enum as value. 38 | #[serde(rename = "return")] 39 | #[deserr(rename = "return")] 40 | returns: Return, 41 | } 42 | 43 | fn default_range() -> Range { 44 | Range { min: 2, max: 4 } 45 | } 46 | 47 | #[derive(Debug, Serialize, Deserialize, Deserr)] 48 | #[serde(deny_unknown_fields)] 49 | #[deserr(deny_unknown_fields, validate = validate_range -> __Deserr_E)] 50 | struct Range { 51 | min: u8, 52 | max: u8, 53 | } 54 | 55 | // Here we could specify the error type we're going to return or stay entirely generic so the 56 | // final caller can decide which implementation of error handler will generate the error message. 57 | fn validate_range( 58 | range: Range, 59 | location: ValuePointerRef, 60 | ) -> Result { 61 | if range.min > range.max { 62 | Err(take_cf_content(E::error::( 63 | None, 64 | ErrorKind::Unexpected { 65 | msg: format!( 66 | "`max` (`{}`) should be greater than `min` (`{}`)", 67 | range.max, range.min 68 | ), 69 | }, 70 | location, 71 | ))) 72 | } else { 73 | Ok(range) 74 | } 75 | } 76 | 77 | #[derive(Debug, Serialize, Deserialize, Deserr)] 78 | #[serde(rename_all = "camelCase")] 79 | #[deserr(rename_all = camelCase)] 80 | enum Return { 81 | Name, 82 | Number, 83 | } 84 | 85 | /// This handler uses the official `axum::Json` extractor 86 | async fn serde(Json(item): Json) -> Result, impl IntoResponse> { 87 | if item.range.min > item.range.max { 88 | Err(( 89 | StatusCode::BAD_REQUEST, 90 | format!( 91 | "`max` (`{}`) should be greater than `min` (`{}`)", 92 | item.range.max, item.range.min 93 | ), 94 | ) 95 | .into_response()) 96 | } else { 97 | Ok(Json(item)) 98 | } 99 | } 100 | 101 | /// This handler uses the official `AxumJson` deserr 102 | async fn deserr(item: AxumJson) -> Json { 103 | Json(item.0) 104 | } 105 | 106 | #[tokio::main] 107 | async fn main() { 108 | tracing_subscriber::registry() 109 | .with( 110 | tracing_subscriber::EnvFilter::try_from_default_env() 111 | .unwrap_or_else(|_| "example_axum=debug".into()), 112 | ) 113 | .with(tracing_subscriber::fmt::layer()) 114 | .init(); 115 | 116 | let app = Router::new() 117 | .route("/serde", post(serde)) 118 | .route("/deserr", post(deserr)); 119 | 120 | let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 8001)) 121 | .await 122 | .unwrap(); 123 | tracing::debug!("listening on {}", listener.local_addr().unwrap()); 124 | axum::serve(listener, app.into_make_service()) 125 | .await 126 | .unwrap(); 127 | } 128 | -------------------------------------------------------------------------------- /tests/attributes/try_from.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::Infallible, 3 | fmt::{self, Display}, 4 | ops::ControlFlow, 5 | str::FromStr, 6 | }; 7 | 8 | use deserr::{ 9 | deserialize, errors::JsonError, take_cf_content, DeserializeError, Deserr, ErrorKind, 10 | MergeWithError, ValuePointerRef, 11 | }; 12 | use insta::{assert_debug_snapshot, assert_snapshot}; 13 | use serde_json::json; 14 | 15 | // For the next tests we're going to deserialize a string that can't contains any non-ascii char 16 | // Since we need a custom error type to accumulate onto in both function it's declared here. 17 | 18 | struct AsciiStringError(char); 19 | 20 | impl Display for AsciiStringError { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | write!( 23 | f, 24 | "Encountered invalid character: `{}`, only ascii characters are accepted", 25 | self.0 26 | ) 27 | } 28 | } 29 | 30 | impl MergeWithError for JsonError { 31 | fn merge( 32 | _self_: Option, 33 | other: AsciiStringError, 34 | merge_location: ValuePointerRef, 35 | ) -> ControlFlow { 36 | ControlFlow::Break(take_cf_content(JsonError::error::( 37 | None, 38 | ErrorKind::Unexpected { 39 | msg: other.to_string(), 40 | }, 41 | merge_location, 42 | ))) 43 | } 44 | } 45 | 46 | #[test] 47 | fn from_container_attribute() { 48 | #[allow(unused)] 49 | #[derive(Debug, Deserr)] 50 | #[deserr(try_from(&String) = FromStr::from_str -> AsciiStringError)] 51 | struct AsciiString(String); 52 | 53 | impl FromStr for AsciiString { 54 | type Err = AsciiStringError; 55 | 56 | fn from_str(s: &str) -> Result { 57 | if let Some(c) = s.chars().find(|c| !c.is_ascii()) { 58 | Err(AsciiStringError(c)) 59 | } else { 60 | Ok(Self(s.to_string())) 61 | } 62 | } 63 | } 64 | 65 | let data = deserialize::(json!("doggo")).unwrap(); 66 | 67 | assert_debug_snapshot!(data, @r###" 68 | AsciiString( 69 | "doggo", 70 | ) 71 | "###); 72 | 73 | let data = deserialize::(json!("🥺")).unwrap_err(); 74 | 75 | assert_snapshot!(data, @"Invalid value: Encountered invalid character: `🥺`, only ascii characters are accepted"); 76 | 77 | #[allow(unused)] 78 | #[derive(Debug, Deserr)] 79 | struct Struct { 80 | #[deserr(needs_predicate)] 81 | doggo: AsciiString, 82 | } 83 | 84 | let data = deserialize::(json!({ "doggo": "BORK" })).unwrap(); 85 | 86 | assert_debug_snapshot!(data, @r###" 87 | Struct { 88 | doggo: AsciiString( 89 | "BORK", 90 | ), 91 | } 92 | "###); 93 | 94 | let data = deserialize::(json!({ "doggo": "👉 👈"})).unwrap_err(); 95 | 96 | assert_snapshot!(data, @"Invalid value at `.doggo`: Encountered invalid character: `👉`, only ascii characters are accepted"); 97 | } 98 | 99 | #[test] 100 | fn from_field_attribute() { 101 | #[allow(unused)] 102 | #[derive(Debug)] 103 | struct AsciiString(String); 104 | 105 | impl FromStr for AsciiString { 106 | type Err = AsciiStringError; 107 | 108 | fn from_str(s: &str) -> Result { 109 | if let Some(c) = s.chars().find(|c| !c.is_ascii()) { 110 | Err(AsciiStringError(c)) 111 | } else { 112 | Ok(Self(s.to_string())) 113 | } 114 | } 115 | } 116 | 117 | #[allow(unused)] 118 | #[derive(Debug, Deserr)] 119 | struct Struct { 120 | #[deserr(try_from(&String) = FromStr::from_str -> AsciiStringError)] 121 | doggo: AsciiString, 122 | } 123 | 124 | let data = deserialize::(json!({ "doggo": "BORK" })).unwrap(); 125 | 126 | assert_debug_snapshot!(data, @r###" 127 | Struct { 128 | doggo: AsciiString( 129 | "BORK", 130 | ), 131 | } 132 | "###); 133 | 134 | let data = deserialize::(json!({ "doggo": "👉 👈"})).unwrap_err(); 135 | 136 | assert_snapshot!(data, @"Invalid value at `.doggo`: Encountered invalid character: `👉`, only ascii characters are accepted"); 137 | } 138 | -------------------------------------------------------------------------------- /examples/implements_deserr_manually.rs: -------------------------------------------------------------------------------- 1 | use deserr::errors::{JsonError, QueryParamError}; 2 | use deserr::{ 3 | deserialize, take_cf_content, DeserializeError, Deserr, ErrorKind, Sequence, Value, ValueKind, 4 | }; 5 | use serde_json::json; 6 | 7 | #[allow(dead_code)] 8 | #[derive(Debug, Deserr)] 9 | #[deserr(deny_unknown_fields)] 10 | struct Query { 11 | name: String, 12 | filter: Filter, 13 | } 14 | 15 | /// A filter is a recursive structure, but it's always composed of arrays or direct value. 16 | /// Thus a valid filter can be on of the following: 17 | /// - `"jorts"` 18 | /// - `["jorts", "jean"]` 19 | /// - `["jorts", ["bilbo", "bob"], "jean"]` 20 | #[allow(dead_code)] 21 | #[derive(Debug)] 22 | enum Filter { 23 | Array(Vec), 24 | Direct(String), 25 | } 26 | 27 | impl Deserr for Filter { 28 | fn deserialize_from_value( 29 | value: deserr::Value, 30 | location: deserr::ValuePointerRef, 31 | ) -> Result { 32 | match value { 33 | Value::String(s) => Ok(Filter::Direct(s)), 34 | Value::Sequence(seq) => Ok(Filter::Array( 35 | seq.into_iter() 36 | .enumerate() 37 | .map(|(index, value)| { 38 | Self::deserialize_from_value(value.into_value(), location.push_index(index)) 39 | }) 40 | .collect::, E>>()?, 41 | )), 42 | value => { 43 | // we're not interested in the `ControlFlow`, we just want to use the error message defined 44 | // by the error type. 45 | Err(take_cf_content(E::error( 46 | None, 47 | ErrorKind::IncorrectValueKind { 48 | actual: value, 49 | accepted: &[ValueKind::String, ValueKind::Sequence], 50 | }, 51 | location, 52 | ))) 53 | } 54 | } 55 | } 56 | } 57 | 58 | fn main() { 59 | let filter = deserialize::(json!("jorts")).unwrap(); 60 | insta::assert_debug_snapshot!(filter, @r###" 61 | Direct( 62 | "jorts", 63 | ) 64 | "###); 65 | 66 | // As you can see, we're effectively able to deserialize all kind of valid filters. 67 | let filter = deserialize::(json!([ 68 | "jorts", 69 | "the", 70 | ["most", ["famous", "catto"], "in", "the"], 71 | "world" 72 | ])) 73 | .unwrap(); 74 | insta::assert_debug_snapshot!(filter, @r###" 75 | Array( 76 | [ 77 | Direct( 78 | "jorts", 79 | ), 80 | Direct( 81 | "the", 82 | ), 83 | Array( 84 | [ 85 | Direct( 86 | "most", 87 | ), 88 | Array( 89 | [ 90 | Direct( 91 | "famous", 92 | ), 93 | Direct( 94 | "catto", 95 | ), 96 | ], 97 | ), 98 | Direct( 99 | "in", 100 | ), 101 | Direct( 102 | "the", 103 | ), 104 | ], 105 | ), 106 | Direct( 107 | "world", 108 | ), 109 | ], 110 | ) 111 | "###); 112 | 113 | // And when an error arise, we get the nice error from the json error handler. 114 | let error = deserialize::(json!(["jorts", "is", ["a", 10]])).unwrap_err(); 115 | insta::assert_snapshot!(error, @"Invalid value type at `[2][1]`: expected a string or an array, but found a positive integer: `10`"); 116 | 117 | // But since we're generic over the error type we can as well switch to query parameter error! 118 | let error = 119 | deserialize::(json!(["jorts", "is", "a", 10])).unwrap_err(); 120 | insta::assert_snapshot!(error, @"Invalid value type for parameter `[3]`: expected a string, but found an integer: `10`"); 121 | 122 | // And as expected, using this `Filter` type from another struct that got its `Deserr` implementation from the derive macro just works. 123 | let filter = 124 | deserialize::(json!({ "name": "jorts", "filter": "catto" })).unwrap(); 125 | insta::assert_debug_snapshot!(filter, @r###" 126 | Query { 127 | name: "jorts", 128 | filter: Direct( 129 | "catto", 130 | ), 131 | } 132 | "###); 133 | } 134 | -------------------------------------------------------------------------------- /book/src/error/available.md: -------------------------------------------------------------------------------- 1 | # Already available error type 2 | 3 | Deserr comes with two predefined error type for json and query parameters. 4 | 5 | ### Json 6 | 7 | Json support is made through the [`JsonError`](https://docs.rs/deserr/latest/deserr/errors/json/struct.JsonError.html) type. 8 | 9 | #### Changes to the error messages 10 | 11 | Here's a non-exhaustive list of some of the changes that are made to the error message compared to `serde_json`: 12 | - Instead of providing the bytes indice of the error it provides the path of the error using dot: `error.on.field[3]`. 13 | - Use the word `array` instead of `Sequence` 14 | - Use the word `object` instead of `Map` 15 | - Never talk about rust type like `u8` and instead use words like number/integer or the bounds of the number directly. 16 | - When using the `deny_unknown_parameter` container attribute deserr will: 17 | - List all the available fields of the object. 18 | - Find and propose the field with the closest name of what was typed with a "did you mean" message. 19 | 20 | #### Examples 21 | 22 | ```rust 23 | use deserr::{Deserr, errors::JsonError}; 24 | use serde_json::json; 25 | #[derive(Deserr, Debug)] 26 | #[deserr(deny_unknown_fields, rename_all = camelCase)] 27 | struct Search { 28 | q: Values, 29 | filter: u8, 30 | } 31 | #[derive(Deserr, Debug)] 32 | #[deserr(rename_all = camelCase)] 33 | enum Values { 34 | Q, 35 | Filter, 36 | } 37 | 38 | // The field name is wrong but is close enough of `filter` 39 | let value = json!({ "filler": "doggo" }); 40 | let err = deserr::deserialize::(value).unwrap_err(); 41 | assert_eq!(err.to_string(), "Unknown field `filler`: did you mean `filter`? expected one of `q`, `filter`"); 42 | 43 | // The field name isn't close to anything 44 | let value = json!({ "a": "doggo" }); 45 | let err = deserr::deserialize::(value).unwrap_err(); 46 | assert_eq!(err.to_string(), "Unknown field `a`: expected one of `q`, `filter`"); 47 | 48 | // Did you mean also works with enum value 49 | let value = json!({ "q": "filler" }); 50 | let err = deserr::deserialize::(value).unwrap_err(); 51 | assert_eq!(err.to_string(), "Unknown value `filler` at `.q`: did you mean `filter`? expected one of `q`, `filter`"); 52 | 53 | let value = json!({ "filter": [2] }); 54 | let err = deserr::deserialize::(value).unwrap_err(); 55 | assert_eq!(err.to_string(), "Invalid value type at `.filter`: expected a positive integer, but found an array: `[2]`"); 56 | ``` 57 | 58 | ### Query Parameter 59 | 60 | Query parameter support is made through the [`QueryParamError`](https://docs.rs/deserr/latest/deserr/errors/query_params/struct.QueryParamError.html) type. 61 | 62 | #### Changes to the error messages 63 | 64 | Here's a non-exhaustive list of some of the changes that are made to the error message compared to `serde_qs`: 65 | - Instead of providing the bytes indice of the error it provides the path of the error using dot: `error.on.parameter[3]`. 66 | - Use the word `multiple values` instead of `Sequence` 67 | - Use the word `multiple parameters` instead of `Map` 68 | - Never talk about rust type like `u8` and instead use words like number/integer or the bounds of the number directly. 69 | - When using the `deny_unknown_parameter` container attribute deserr will: 70 | - List all the available parameters of the object. 71 | - Find and propose the parameter with the closest name of what was typed with a "did you mean" message. 72 | 73 | #### Examples 74 | 75 | 76 | ```rust 77 | use deserr::{Deserr, errors::QueryParamError}; 78 | use serde_json::json; 79 | #[derive(Deserr, Debug)] 80 | #[deserr(deny_unknown_fields, rename_all = camelCase)] 81 | struct Search { 82 | q: Values, 83 | filter: u8, 84 | } 85 | #[derive(Deserr, Debug)] 86 | #[deserr(rename_all = camelCase)] 87 | enum Values { 88 | Q, 89 | Filter, 90 | } 91 | 92 | // The field name is wrong but is close enough of `filter` 93 | let value = json!({ "filler": "doggo" }); 94 | let err = deserr::deserialize::(value).unwrap_err(); 95 | assert_eq!(err.to_string(), "Unknown parameter `filler`: did you mean `filter`? expected one of `q`, `filter`"); 96 | 97 | // The parameter name isn't close to anything 98 | let value = json!({ "a": "doggo" }); 99 | let err = deserr::deserialize::(value).unwrap_err(); 100 | assert_eq!(err.to_string(), "Unknown parameter `a`: expected one of `q`, `filter`"); 101 | 102 | // Did you mean also works with enum value 103 | let value = json!({ "q": "filler" }); 104 | let err = deserr::deserialize::(value).unwrap_err(); 105 | assert_eq!(err.to_string(), "Unknown value `filler` for parameter `q`: did you mean `filter`? expected one of `q`, `filter`"); 106 | 107 | let value = json!({ "filter": [2] }); 108 | let err = deserr::deserialize::(value).unwrap_err(); 109 | // The query parameters are always expecting string in the values 110 | assert_eq!(err.to_string(), "Invalid value type for parameter `filter`: expected a string, but found multiple values"); 111 | ``` 112 | 113 | ### Want another format 114 | 115 | Feel free to open an issue or a PR 116 | -------------------------------------------------------------------------------- /tests/supported_value_types.rs: -------------------------------------------------------------------------------- 1 | use deserr::{deserialize, errors::JsonError, Deserr}; 2 | use insta::assert_debug_snapshot; 3 | use serde_json::json; 4 | 5 | #[test] 6 | fn numbers() { 7 | #[allow(dead_code)] 8 | #[derive(Debug, Deserr)] 9 | struct Struct { 10 | u8: u8, 11 | u16: u16, 12 | u32: u32, 13 | u64: u64, 14 | u128: u128, 15 | 16 | i8: i8, 17 | i16: i16, 18 | i32: i32, 19 | i64: i64, 20 | i128: i128, 21 | 22 | f32: f32, 23 | f64: f64, 24 | } 25 | 26 | let data = deserialize::(json!({ 27 | "u8": 1, 28 | "u16": 1, 29 | "u32": 1, 30 | "u64": 1, 31 | "u128": 1, 32 | 33 | "i8": 1, 34 | "i16": 1, 35 | "i32": 1, 36 | "i64": 1, 37 | "i128": 1, 38 | 39 | "f32": 1, 40 | "f64": 1, 41 | })) 42 | .unwrap(); 43 | 44 | assert_debug_snapshot!(data, @r###" 45 | Struct { 46 | u8: 1, 47 | u16: 1, 48 | u32: 1, 49 | u64: 1, 50 | u128: 1, 51 | i8: 1, 52 | i16: 1, 53 | i32: 1, 54 | i64: 1, 55 | i128: 1, 56 | f32: 1.0, 57 | f64: 1.0, 58 | } 59 | "###); 60 | } 61 | 62 | #[test] 63 | fn strings() { 64 | #[allow(dead_code)] 65 | #[derive(Debug, Deserr)] 66 | struct Struct { 67 | c: char, 68 | s: String, 69 | } 70 | let data = deserialize::(json!({ 71 | "c": "c", 72 | "s": "catto", 73 | })) 74 | .unwrap(); 75 | 76 | assert_debug_snapshot!(data, @r###" 77 | Struct { 78 | c: 'c', 79 | s: "catto", 80 | } 81 | "###); 82 | } 83 | 84 | #[test] 85 | fn boolean() { 86 | #[allow(dead_code)] 87 | #[derive(Debug, Deserr)] 88 | struct Struct { 89 | b: bool, 90 | } 91 | let data = deserialize::(json!({ 92 | "b": true, 93 | })) 94 | .unwrap(); 95 | 96 | assert_debug_snapshot!(data, @r###" 97 | Struct { 98 | b: true, 99 | } 100 | "###); 101 | } 102 | 103 | #[test] 104 | fn tuple() { 105 | #[allow(dead_code)] 106 | #[derive(Debug, Deserr)] 107 | struct Struct1 { 108 | tuple: (), 109 | } 110 | let data = deserialize::(json!({ 111 | "tuple": null, 112 | })) 113 | .unwrap(); 114 | 115 | // we can't create tuple of one elements, rust is going to complain and get rids of the parenthesis 116 | 117 | assert_debug_snapshot!(data, @r###" 118 | Struct1 { 119 | tuple: (), 120 | } 121 | "###); 122 | 123 | #[allow(dead_code)] 124 | #[derive(Debug, Deserr)] 125 | struct Struct2 { 126 | tuple: (bool, char), 127 | } 128 | let data = deserialize::(json!({ 129 | "tuple": [true, 'c'], 130 | })) 131 | .unwrap(); 132 | 133 | assert_debug_snapshot!(data, @r###" 134 | Struct2 { 135 | tuple: ( 136 | true, 137 | 'c', 138 | ), 139 | } 140 | "###); 141 | 142 | #[allow(dead_code)] 143 | #[derive(Debug, Deserr)] 144 | struct Struct3 { 145 | tuple: (bool, char, u8), 146 | } 147 | let data = deserialize::(json!({ 148 | "tuple": [true, 'c', 2], 149 | })) 150 | .unwrap(); 151 | 152 | assert_debug_snapshot!(data, @r###" 153 | Struct3 { 154 | tuple: ( 155 | true, 156 | 'c', 157 | 2, 158 | ), 159 | } 160 | "###); 161 | } 162 | 163 | #[test] 164 | fn array() { 165 | #[allow(dead_code)] 166 | #[derive(Debug, Deserr)] 167 | struct Struct0 { 168 | arr: [u8; 0], 169 | } 170 | let data = deserialize::(json!({ 171 | "arr": [], 172 | })) 173 | .unwrap(); 174 | assert_debug_snapshot!(data, @r###" 175 | Struct0 { 176 | arr: [], 177 | } 178 | "###); 179 | 180 | #[allow(dead_code)] 181 | #[derive(Debug, Deserr)] 182 | struct Struct1 { 183 | arr: [u8; 1], 184 | } 185 | let data = deserialize::(json!({ 186 | "arr": [2], 187 | })) 188 | .unwrap(); 189 | 190 | assert_debug_snapshot!(data, @r###" 191 | Struct1 { 192 | arr: [ 193 | 2, 194 | ], 195 | } 196 | "###); 197 | 198 | #[allow(dead_code)] 199 | #[derive(Debug, Deserr)] 200 | struct Struct2 { 201 | arr: [bool; 2], 202 | } 203 | let data = deserialize::(json!({ 204 | "arr": [true, false], 205 | })) 206 | .unwrap(); 207 | 208 | assert_debug_snapshot!(data, @r###" 209 | Struct2 { 210 | arr: [ 211 | true, 212 | false, 213 | ], 214 | } 215 | "###); 216 | 217 | #[allow(dead_code)] 218 | #[derive(Debug, Deserr)] 219 | struct Struct3 { 220 | arr: [bool; 3], 221 | } 222 | let data = deserialize::(json!({ 223 | "arr": [true, false, true], 224 | })) 225 | .unwrap(); 226 | 227 | assert_debug_snapshot!(data, @r###" 228 | Struct3 { 229 | arr: [ 230 | true, 231 | false, 232 | true, 233 | ], 234 | } 235 | "###); 236 | } 237 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | #![allow(clippy::struct_excessive_bools)] 3 | 4 | extern crate test; 5 | 6 | use deserr::Deserr; 7 | use serde_derive::{Deserialize, Serialize}; 8 | use test::Bencher; 9 | 10 | fn input_json() -> String { 11 | std::fs::read_to_string("benches/twitter.json").unwrap() 12 | } 13 | 14 | #[bench] 15 | fn bench_deserialize_deserr(b: &mut Bencher) { 16 | let j = input_json(); 17 | b.iter(|| { 18 | let json = serde_json::from_str::(&j).unwrap(); 19 | let _t: Twitter = deserr::deserialize::<_, _, deserr::errors::JsonError>(json).unwrap(); 20 | }); 21 | } 22 | 23 | #[bench] 24 | fn bench_deserialize_serdejson(b: &mut Bencher) { 25 | let j = input_json(); 26 | b.iter(|| { 27 | serde_json::from_str::(&j).unwrap(); 28 | }); 29 | } 30 | #[bench] 31 | fn bench_deserialize_serdejson_value(b: &mut Bencher) { 32 | let j = input_json(); 33 | b.iter(|| { 34 | serde_json::from_str::(&j).unwrap(); 35 | }); 36 | } 37 | 38 | #[derive(Serialize, Deserialize, Deserr)] 39 | struct Twitter { 40 | statuses: Vec, 41 | search_metadata: SearchMetadata, 42 | } 43 | 44 | #[derive(Serialize, Deserialize, Deserr)] 45 | struct Status { 46 | metadata: Metadata, 47 | created_at: String, 48 | id: u64, 49 | id_str: String, 50 | text: String, 51 | source: String, 52 | truncated: bool, 53 | in_reply_to_status_id: Option, 54 | in_reply_to_status_id_str: Option, 55 | in_reply_to_user_id: Option, 56 | in_reply_to_user_id_str: Option, 57 | in_reply_to_screen_name: Option, 58 | user: User, 59 | geo: (), 60 | coordinates: (), 61 | place: (), 62 | contributors: (), 63 | retweeted_status: Option>, 64 | retweet_count: u32, 65 | favorite_count: u32, 66 | entities: StatusEntities, 67 | favorited: bool, 68 | retweeted: bool, 69 | possibly_sensitive: Option, 70 | lang: String, 71 | } 72 | 73 | #[derive(Serialize, Deserialize, Deserr)] 74 | struct Metadata { 75 | result_type: String, 76 | iso_language_code: String, 77 | } 78 | 79 | #[derive(Serialize, Deserialize, Deserr)] 80 | struct User { 81 | id: u32, 82 | id_str: String, 83 | name: String, 84 | screen_name: String, 85 | location: String, 86 | description: String, 87 | url: Option, 88 | entities: UserEntities, 89 | protected: bool, 90 | followers_count: u32, 91 | friends_count: u32, 92 | listed_count: u32, 93 | created_at: String, 94 | favourites_count: u32, 95 | utc_offset: Option, 96 | time_zone: Option, 97 | geo_enabled: bool, 98 | verified: bool, 99 | statuses_count: u32, 100 | lang: String, 101 | contributors_enabled: bool, 102 | is_translator: bool, 103 | is_translation_enabled: bool, 104 | profile_background_color: String, 105 | profile_background_image_url: String, 106 | profile_background_image_url_https: String, 107 | profile_background_tile: bool, 108 | profile_image_url: String, 109 | profile_image_url_https: String, 110 | profile_banner_url: Option, 111 | profile_link_color: String, 112 | profile_sidebar_border_color: String, 113 | profile_sidebar_fill_color: String, 114 | profile_text_color: String, 115 | profile_use_background_image: bool, 116 | default_profile: bool, 117 | default_profile_image: bool, 118 | following: bool, 119 | follow_request_sent: bool, 120 | notifications: bool, 121 | } 122 | 123 | #[derive(Serialize, Deserialize, Deserr)] 124 | struct UserEntities { 125 | url: Option, 126 | description: UserEntitiesDescription, 127 | } 128 | 129 | #[derive(Serialize, Deserialize, Deserr)] 130 | struct UserUrl { 131 | urls: Vec, 132 | } 133 | 134 | #[derive(Serialize, Deserialize, Deserr)] 135 | struct Url { 136 | url: String, 137 | expanded_url: String, 138 | display_url: String, 139 | indices: Indices, 140 | } 141 | 142 | #[derive(Serialize, Deserialize, Deserr)] 143 | struct UserEntitiesDescription { 144 | urls: Vec, 145 | } 146 | 147 | #[derive(Serialize, Deserialize, Deserr)] 148 | struct StatusEntities { 149 | hashtags: Vec, 150 | symbols: Vec<()>, 151 | urls: Vec, 152 | user_mentions: Vec, 153 | media: Option>, 154 | } 155 | 156 | #[derive(Serialize, Deserialize, Deserr)] 157 | struct Hashtag { 158 | text: String, 159 | indices: Indices, 160 | } 161 | 162 | #[derive(Serialize, Deserialize, Deserr)] 163 | struct UserMention { 164 | screen_name: String, 165 | name: String, 166 | id: u32, 167 | id_str: String, 168 | indices: Indices, 169 | } 170 | 171 | #[derive(Serialize, Deserialize, Deserr)] 172 | struct Media { 173 | id: u64, 174 | id_str: String, 175 | indices: Indices, 176 | media_url: String, 177 | media_url_https: String, 178 | url: String, 179 | display_url: String, 180 | expanded_url: String, 181 | #[deserr(rename = "type")] 182 | #[serde(rename = "type")] 183 | media_type: String, 184 | sizes: Sizes, 185 | source_status_id: Option, 186 | source_status_id_str: Option, 187 | } 188 | 189 | #[derive(Serialize, Deserialize, Deserr)] 190 | struct Sizes { 191 | medium: Size, 192 | small: Size, 193 | thumb: Size, 194 | large: Size, 195 | } 196 | 197 | #[derive(Serialize, Deserialize, Deserr)] 198 | struct Size { 199 | w: u16, 200 | h: u16, 201 | resize: String, 202 | } 203 | 204 | type Indices = (u8, u8); 205 | 206 | #[derive(Serialize, Deserialize, Deserr)] 207 | struct SearchMetadata { 208 | completed_in: f32, 209 | max_id: u64, 210 | max_id_str: String, 211 | next_results: String, 212 | query: String, 213 | refresh_url: String, 214 | count: u8, 215 | since_id: u64, 216 | since_id_str: String, 217 | } 218 | -------------------------------------------------------------------------------- /book/src/attributes/mod.md: -------------------------------------------------------------------------------- 1 | # Attributes 2 | 3 | [Attributes](https://doc.rust-lang.org/reference/attributes.html) are used to customize the `Deserr` 4 | implementations produced by deserr's derive. 5 | 6 | There are three categories of attributes: 7 | 8 | - [**Container attributes**] — apply to a struct or enum declaration. 9 | - [**Variant attributes**] — apply to a variant of an enum. 10 | - [**Field attributes**] — apply to one field in a struct or in an enum variant. 11 | 12 | [**Container attributes**]: container.md 13 | [**Variant attributes**]: variant.md 14 | [**Field attributes**]: field.md 15 | 16 | ```rust 17 | # use deserr::Deserr; 18 | # 19 | #[derive(Deserr)] 20 | #[deserr(deny_unknown_fields)] // <-- this is a container attribute 21 | struct S { 22 | #[deserr(default)] // <-- this is a field attribute 23 | f: i32, 24 | } 25 | 26 | #[derive(Deserr)] 27 | #[deserr(rename_all = camelCase)] // <-- this is also a container attribute 28 | enum E { 29 | #[deserr(rename = "_deserr")] // <-- this is a variant attribute 30 | DeserrIsGreat, 31 | SerdeIsAwesome 32 | } 33 | # 34 | # fn main() {} 35 | ``` 36 | 37 | Note that a single struct, enum, variant, or field may have multiple attributes 38 | on it. 39 | 40 | ## Feature comparison table with serde 41 | 42 | #### Datastructure support 43 | 44 | | datastructure | serde | deserr | note | 45 | |---------------------|-------|--------|------| 46 | | Struct | yes | yes | | 47 | | Tuple struct | yes | no | | 48 | | Untagged Enum | yes | no | | 49 | | Untagged unit Enum | yes | yes | | 50 | | Tagged Enum | yes | yes | | 51 | 52 | #### Container attributes 53 | 54 | | features | serde | deserr | note | 55 | |---------------------|-------|----------------------------------------------|---------------------------------------------------------------------------------| 56 | | rename | yes | no | | 57 | | rename_all | yes | [yes](container.md#deserrrenameall) | | 58 | | deny_unknown_fields | yes | [yes](container.md#deserrdenyunknownfields) | With deserr you can call a custom function when an unknown field is encountered | 59 | | tag | yes | [yes](container.md#deserrtag) | | 60 | | tag+content | yes | no | | 61 | | untagged | yes | no | it's only supported for unit enums | 62 | | bound | yes | no | Can be emulated with `where_predicate` | 63 | | default | yes | no | | 64 | | remote | yes | no | | 65 | | transparent | yes | no | | 66 | | from | yes | [yes](container.md#deserrfrom) | | 67 | | try_from | yes | [yes](container.md#deserrtryfrom) | | 68 | | into | yes | no | | 69 | | crate | yes | no | | 70 | | validate | no | [yes](container.md#deserrvalidate) | Allows you to validate the content of struct **after** it has been deserialized | 71 | | error | no | [yes](container.md#deserrerror) | Specify the error type that should be used while deserializing this structure | 72 | | where_predicate | no | [yes](container.md#deserrwherepredicate) | Let you add where clauses to the generated `Deserr` implementation | 73 | 74 | #### Field attributes 75 | 76 | | features | serde | deserr | note | 77 | |---------------------|-------|--------------------------------------------|---------------------------------------------------------------------------| 78 | | rename | yes | [yes](field.md#deserrrename) | | 79 | | alias | yes | no | | 80 | | default | yes | [yes](field.md#deserrdefault) | | 81 | | flatten | yes | no | serde doesn't support flattening + denying unknown field | 82 | | skip | yes | [yes](field.md#deserrskip) | | 83 | | deserialize_with | yes | no | But it's kinda emulated with `from` and `try_from` | 84 | | with | yes | no | | 85 | | borrow | yes | no | deserr does not support types with references | 86 | | bound | yes | no | | 87 | | map | no | [yes](field.md#deserrmap) | Allows you to map the value **after** it was deserialized | 88 | | from | no | [yes](field.md#deserrfrom) | Deserialize this field from an infallible function | 89 | | try_from | no | [yes](field.md#deserrtry_from) | Deserialize this field from a fallible function | 90 | | missing_field_error | no | [yes](field.md#deserrmissing_field_error) | Allows you to return a custom error if this field is missing | 91 | | error | no | [yes](field.md#deserrerror) | Specify the error type that should be used while deserializing this field | 92 | 93 | -------------------------------------------------------------------------------- /src/serde_json.rs: -------------------------------------------------------------------------------- 1 | use std::ops::ControlFlow; 2 | 3 | use crate::{ 4 | take_cf_content, DeserializeError, Deserr, ErrorKind, IntoValue, Map, Sequence, Value, 5 | ValueKind, ValuePointerRef, 6 | }; 7 | use serde_json::{Map as JMap, Number, Value as JValue}; 8 | 9 | impl Map for JMap { 10 | type Value = JValue; 11 | type Iter = ::IntoIter; 12 | 13 | fn len(&self) -> usize { 14 | self.len() 15 | } 16 | fn remove(&mut self, key: &str) -> Option { 17 | self.remove(key) 18 | } 19 | fn into_iter(self) -> Self::Iter { 20 | ::into_iter(self) 21 | } 22 | } 23 | 24 | impl IntoValue for JValue { 25 | type Sequence = Vec; 26 | type Map = JMap; 27 | 28 | fn into_value(self) -> Value { 29 | match self { 30 | JValue::Null => Value::Null, 31 | JValue::Bool(b) => Value::Boolean(b), 32 | JValue::Number(n) => { 33 | if let Some(n) = n.as_u64() { 34 | Value::Integer(n) 35 | } else if let Some(n) = n.as_i64() { 36 | Value::NegativeInteger(n) 37 | } else if let Some(n) = n.as_f64() { 38 | Value::Float(n) 39 | } else { 40 | panic!(); 41 | } 42 | } 43 | JValue::String(x) => Value::String(x), 44 | JValue::Array(x) => Value::Sequence(x), 45 | JValue::Object(x) => Value::Map(x), 46 | } 47 | } 48 | 49 | fn kind(&self) -> ValueKind { 50 | match self { 51 | JValue::Null => ValueKind::Null, 52 | JValue::Bool(_) => ValueKind::Boolean, 53 | JValue::Number(n) => { 54 | if n.is_u64() { 55 | ValueKind::Integer 56 | } else if n.is_i64() { 57 | ValueKind::NegativeInteger 58 | } else if n.is_f64() { 59 | ValueKind::Float 60 | } else { 61 | panic!(); 62 | } 63 | } 64 | JValue::String(_) => ValueKind::String, 65 | JValue::Array(_) => ValueKind::Sequence, 66 | JValue::Object(_) => ValueKind::Map, 67 | } 68 | } 69 | } 70 | 71 | impl Deserr for JValue { 72 | fn deserialize_from_value( 73 | value: Value, 74 | location: ValuePointerRef, 75 | ) -> Result { 76 | let mut error: Option = None; 77 | Ok(match value { 78 | Value::Null => JValue::Null, 79 | Value::Boolean(b) => JValue::Bool(b), 80 | Value::Integer(x) => JValue::Number(Number::from(x)), 81 | Value::NegativeInteger(x) => JValue::Number(Number::from(x)), 82 | Value::Float(f) => match Number::from_f64(f) { 83 | Some(n) => JValue::Number(n), 84 | None => { 85 | return Err(take_cf_content(E::error::( 86 | error, 87 | ErrorKind::Unexpected { 88 | msg: format!("the float {f} is not representable in JSON"), 89 | }, 90 | location, 91 | ))); 92 | } 93 | }, 94 | Value::String(s) => JValue::String(s), 95 | Value::Sequence(seq) => { 96 | let mut jseq = Vec::with_capacity(seq.len()); 97 | for (index, value) in seq.into_iter().enumerate() { 98 | let result = Self::deserialize_from_value( 99 | value.into_value(), 100 | location.push_index(index), 101 | ); 102 | match result { 103 | Ok(value) => { 104 | jseq.push(value); 105 | } 106 | Err(e) => { 107 | error = match E::merge(error, e, location.push_index(index)) { 108 | ControlFlow::Continue(e) => Some(e), 109 | ControlFlow::Break(e) => return Err(e), 110 | }; 111 | } 112 | } 113 | } 114 | if let Some(e) = error { 115 | return Err(e); 116 | } else { 117 | JValue::Array(jseq) 118 | } 119 | } 120 | Value::Map(map) => { 121 | let mut jmap = JMap::with_capacity(map.len()); 122 | for (key, value) in map.into_iter() { 123 | let result = 124 | Self::deserialize_from_value(value.into_value(), location.push_key(&key)); 125 | match result { 126 | Ok(value) => { 127 | jmap.insert(key, value); 128 | } 129 | Err(e) => { 130 | error = match E::merge(error, e, location.push_key(&key)) { 131 | ControlFlow::Continue(e) => Some(e), 132 | ControlFlow::Break(e) => return Err(e), 133 | }; 134 | } 135 | } 136 | } 137 | if let Some(e) = error { 138 | return Err(e); 139 | } else { 140 | JValue::Object(jmap) 141 | } 142 | } 143 | }) 144 | } 145 | } 146 | 147 | impl From> for JValue { 148 | fn from(value: Value) -> Self { 149 | match value { 150 | Value::Null => JValue::Null, 151 | Value::Boolean(b) => JValue::Bool(b), 152 | Value::Integer(n) => JValue::Number(Number::from(n)), 153 | Value::NegativeInteger(i) => JValue::Number(Number::from(i)), 154 | // if we can't parse the float then its set to `null` 155 | Value::Float(f) => Number::from_f64(f) 156 | .map(JValue::Number) 157 | .unwrap_or(JValue::Null), 158 | Value::String(s) => JValue::String(s), 159 | Value::Sequence(s) => JValue::Array( 160 | s.into_iter() 161 | .map(IntoValue::into_value) 162 | .map(JValue::from) 163 | .collect(), 164 | ), 165 | Value::Map(m) => m 166 | .into_iter() 167 | .map(|(k, v)| (k, JValue::from(v.into_value()))) 168 | .collect(), 169 | } 170 | } 171 | } 172 | 173 | #[cfg(test)] 174 | mod test { 175 | use super::*; 176 | use serde_json::json; 177 | 178 | #[test] 179 | fn from_value_to_deserr_and_back() { 180 | let value = json!({ "The": "best", "doggos": ["are"], "the": { "bernese": "mountain" }}); 181 | let deserr = value.clone().into_value(); 182 | 183 | insta::assert_debug_snapshot!(deserr, @r###" 184 | Map( 185 | { 186 | "The": String("best"), 187 | "doggos": Array [ 188 | String("are"), 189 | ], 190 | "the": Object { 191 | "bernese": String("mountain"), 192 | }, 193 | }, 194 | ) 195 | "###); 196 | 197 | let deserr: JValue = deserr.into(); 198 | insta::assert_debug_snapshot!(deserr, @r###" 199 | Object { 200 | "The": String("best"), 201 | "doggos": Array [ 202 | String("are"), 203 | ], 204 | "the": Object { 205 | "bernese": String("mountain"), 206 | }, 207 | } 208 | "###); 209 | 210 | assert_eq!(value, deserr); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![doc( 3 | html_favicon_url = "https://raw.githubusercontent.com/meilisearch/deserr/main/assets/deserr.ico?raw=true" 4 | )] 5 | #![doc( 6 | html_logo_url = "https://raw.githubusercontent.com/meilisearch/deserr/main/assets/deserr_squared.png?raw=true" 7 | )] 8 | 9 | #[cfg(feature = "actix-web")] 10 | pub mod actix_web; 11 | #[cfg(feature = "axum")] 12 | pub mod axum; 13 | pub mod errors; 14 | mod impls; 15 | #[cfg(feature = "serde-cs")] 16 | pub mod serde_cs; 17 | #[cfg(feature = "serde-json")] 18 | pub mod serde_json; 19 | mod value; 20 | 21 | extern crate self as deserr; 22 | 23 | /** 24 | It is possible to derive the `Deserr` trait for structs and enums with named fields. 25 | The derive proc macro accept many arguments, explained below: 26 | 27 | The basic usage is as follows: 28 | ``` 29 | use deserr::Deserr; 30 | 31 | #[derive(Deserr)] 32 | struct MyStruct { 33 | x: bool, 34 | y: u8, 35 | } 36 | ``` 37 | This will implement `impl Deserr MyStruct` for all `E: DeserializeError`. 38 | 39 | To use it on enums, the attribute `tag` must be added: 40 | ``` 41 | use deserr::Deserr; 42 | 43 | #[derive(Deserr)] 44 | #[deserr(tag = "my_enum_tag")] 45 | enum MyEnum { 46 | A, 47 | B { x: bool, y: u8 } 48 | } 49 | ``` 50 | This will correctly deserialize the given enum for values of this shape: 51 | ```json 52 | { 53 | "my_enum_tag": "A" 54 | } 55 | // or 56 | { 57 | "my_enum_tag": "B", 58 | "x": true, 59 | "y": 1 60 | } 61 | ``` 62 | 63 | It is possible to change the name of the keys corresponding to each field using the `rename` and `rename_all` 64 | attributes: 65 | 66 | ```rust 67 | use deserr::Deserr; 68 | #[derive(Deserr)] 69 | #[deserr(rename_all = camelCase)] 70 | struct MyStruct { 71 | my_field: bool, 72 | #[deserr(rename = "goodbye_world")] 73 | hello_world: u8, 74 | } 75 | ``` 76 | will parse the following: 77 | ```json 78 | { 79 | "myField": 1, 80 | "goodbye_world": 2 81 | } 82 | ``` 83 | */ 84 | pub use deserr_internal::Deserr; 85 | pub use value::{IntoValue, Map, Sequence, Value, ValueKind, ValuePointer, ValuePointerRef}; 86 | 87 | use std::ops::ControlFlow; 88 | 89 | /// A trait for types that can be deserialized from a [`Value`]. The generic type 90 | /// parameter `E` is the custom error that is returned when deserialization fails. 91 | pub trait Deserr: Sized { 92 | /// Attempts to deserialize `Self` from the given value. Note that this method is an 93 | /// implementation detail. You probably want to use the [`deserialize`] function directly instead. 94 | fn deserialize_from_value( 95 | value: Value, 96 | location: ValuePointerRef, 97 | ) -> Result; 98 | } 99 | 100 | /// Deserialize the given value. 101 | /// 102 | /// This function has three generic arguments, two of which can often be inferred. 103 | /// 1. `Ret` is the type we want to deserialize to. For example: `MyStruct` 104 | /// 2. `Val` is the type of the value given as argument. For example: `serde_json::Value` 105 | /// 3. `E` is the error type we want to get when deserialization fails. For example: `MyError` 106 | pub fn deserialize(value: Val) -> Result 107 | where 108 | Ret: Deserr, 109 | Val: IntoValue, 110 | E: DeserializeError, 111 | { 112 | Ret::deserialize_from_value(value.into_value(), ValuePointerRef::Origin) 113 | } 114 | 115 | /// A trait which describes how to combine two errors together. 116 | pub trait MergeWithError: Sized { 117 | /// Merge two errors together. 118 | /// 119 | /// ## Arguments: 120 | /// - `self_`: the existing error, if any 121 | /// - `other`: the new error 122 | /// - `merge_location`: the location where the merging happens. 123 | /// 124 | /// ## Return value 125 | /// It should return the merged error inside a `Result`. 126 | /// 127 | /// The variant of the returned result should be `Ok(e)` to signal that the deserialization 128 | /// should continue (to accumulate more errors), or `Err(e)` to stop the deserialization immediately. 129 | /// 130 | /// Note that in both cases, the deserialization should eventually fail. 131 | /// 132 | /// ## Example 133 | /// Imagine you have the following json: 134 | /// ```json 135 | /// { 136 | /// "w": true, 137 | /// "x" : { "y": 1 } 138 | /// } 139 | /// ``` 140 | /// It may be that deserializing the first field, `w`, fails with error `suberror: E`. This is the 141 | /// first deserialization error we encounter, so the current error value is `None`. The function `Self::merge` 142 | /// is called as follows: 143 | /// ```ignore 144 | /// // let mut error = None; 145 | /// // let mut location : ValuePointerRef::Origin; 146 | /// error = Some(Self::merge(error, suberror, location.push_key("w"))?); 147 | /// // if the returned value was Err(e), then we returned early from the deserialize method 148 | /// // otherwise, `error` is now set 149 | /// ``` 150 | /// Later on, we encounter a new suberror originating from `x.y`. The `merge` function is called again: 151 | /// ```ignore 152 | /// // let mut error = Some(..); 153 | /// // let mut location : ValuePointerRef::Origin; 154 | /// error = Some(Self::merge(error, suberror, location.push_key("x"))?); 155 | /// // if the returned value was Err(e), then we returned early from the deserialize method 156 | /// // otherwise, `error` is now the result of its merging with suberror. 157 | /// ``` 158 | /// Note that even though the suberror originated at `x.y`, the `merge_location` argument was `x` 159 | /// because that is where the merge happened. 160 | fn merge( 161 | self_: Option, 162 | other: T, 163 | merge_location: ValuePointerRef, 164 | ) -> ControlFlow; 165 | } 166 | 167 | pub enum ErrorKind<'a, V: IntoValue> { 168 | IncorrectValueKind { 169 | actual: Value, 170 | accepted: &'a [ValueKind], 171 | }, 172 | MissingField { 173 | field: &'a str, 174 | }, 175 | UnknownKey { 176 | key: &'a str, 177 | accepted: &'a [&'a str], 178 | }, 179 | UnknownValue { 180 | value: &'a str, 181 | accepted: &'a [&'a str], 182 | }, 183 | BadSequenceLen { 184 | actual: V::Sequence, 185 | expected: usize, 186 | }, 187 | Unexpected { 188 | msg: String, 189 | }, 190 | } 191 | 192 | /// A trait for errors returned by [`deserialize_from_value`](Deserr::deserialize_from_value). 193 | pub trait DeserializeError: Sized + MergeWithError { 194 | fn error( 195 | self_: Option, 196 | error: ErrorKind, 197 | location: ValuePointerRef, 198 | ) -> ControlFlow; 199 | } 200 | 201 | /// Used by the derive proc macro. Do not use. 202 | #[doc(hidden)] 203 | pub enum FieldState { 204 | Missing, 205 | Err, 206 | Some(T), 207 | } 208 | 209 | impl FieldState { 210 | pub fn is_missing(&self) -> bool { 211 | matches!(self, FieldState::Missing) 212 | } 213 | 214 | #[track_caller] 215 | pub fn unwrap(self) -> T { 216 | match self { 217 | FieldState::Some(x) => x, 218 | _ => panic!("Unwrapping an empty field state"), 219 | } 220 | } 221 | 222 | #[track_caller] 223 | pub fn unwrap_or(self, value: T) -> T { 224 | match self { 225 | FieldState::Some(x) => x, 226 | FieldState::Missing => value, 227 | FieldState::Err => value, 228 | } 229 | } 230 | 231 | #[track_caller] 232 | pub fn ok_or(self, err: E) -> Result { 233 | match self { 234 | FieldState::Some(x) => Ok(x), 235 | FieldState::Missing => Err(err), 236 | FieldState::Err => Err(err), 237 | } 238 | } 239 | 240 | pub fn map(self, f: impl Fn(T) -> U) -> FieldState { 241 | match self { 242 | FieldState::Some(x) => FieldState::Some(f(x)), 243 | FieldState::Missing => FieldState::Missing, 244 | FieldState::Err => FieldState::Err, 245 | } 246 | } 247 | } 248 | 249 | /// Extract the `ControlFlow` result if it's the same type. 250 | pub fn take_cf_content(r: ControlFlow) -> T { 251 | match r { 252 | ControlFlow::Continue(x) => x, 253 | ControlFlow::Break(x) => x, 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/value.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::Infallible, 3 | fmt::{Debug, Display}, 4 | }; 5 | 6 | /// A location within a [`Value`]. 7 | /// 8 | /// Conceptually, it is a list of choices that one has to make to go to a certain place within 9 | /// the value. In practice, it is used to locate the origin of a deserialization error. 10 | /// 11 | /// ## Example 12 | /// ``` 13 | /// use deserr::ValuePointerRef; 14 | /// 15 | /// let pointer = ValuePointerRef::Origin; 16 | /// let pointer = pointer.push_key("a"); 17 | /// let pointer = pointer.push_index(2); 18 | /// // now `pointer` points to "a".2 19 | /// ``` 20 | /// 21 | /// A `ValuePointerRef` is an immutable data structure, so it is cheap to extend and to copy. 22 | /// However, if you want to store it inside an owned type, you may want to convert it to a 23 | /// [`ValuePointer`] instead using [`self.to_owned()`](ValuePointerRef::to_owned). 24 | #[derive(Clone, Copy)] 25 | pub enum ValuePointerRef<'a> { 26 | Origin, 27 | Key { 28 | key: &'a str, 29 | prev: &'a ValuePointerRef<'a>, 30 | }, 31 | Index { 32 | index: usize, 33 | prev: &'a ValuePointerRef<'a>, 34 | }, 35 | } 36 | 37 | impl Default for ValuePointerRef<'_> { 38 | fn default() -> Self { 39 | Self::Origin 40 | } 41 | } 42 | 43 | impl<'a> ValuePointerRef<'a> { 44 | /// Extend `self` such that it points to the next subvalue at the given `key`. 45 | #[must_use] 46 | pub fn push_key(&'a self, key: &'a str) -> Self { 47 | Self::Key { key, prev: self } 48 | } 49 | 50 | #[must_use] 51 | /// Extend `self` such that it points to the next subvalue at the given index. 52 | pub fn push_index(&'a self, index: usize) -> Self { 53 | Self::Index { index, prev: self } 54 | } 55 | 56 | /// Return true if the pointer is at the origin. 57 | pub fn is_origin(&self) -> bool { 58 | matches!(self, ValuePointerRef::Origin) 59 | } 60 | 61 | /// Return the last field encountered if there is one. 62 | pub fn last_field(&self) -> Option<&str> { 63 | match self { 64 | ValuePointerRef::Origin => None, 65 | ValuePointerRef::Key { key, .. } => Some(key), 66 | ValuePointerRef::Index { prev, .. } => prev.last_field(), 67 | } 68 | } 69 | 70 | /// Return the first field encountered if there is one. 71 | /// Eg; 72 | /// "toto.tata[42].lol" -> "toto" 73 | /// "toto" -> "toto" 74 | /// "[1][2][3]" -> None 75 | /// "" -> None 76 | pub fn first_field(&self) -> Option<&str> { 77 | match self { 78 | ValuePointerRef::Origin => None, 79 | ValuePointerRef::Key { key, prev } => prev.first_field().or(Some(key)), 80 | ValuePointerRef::Index { prev, .. } => prev.first_field(), 81 | } 82 | } 83 | 84 | /// Convert `self` to its owned version 85 | pub fn to_owned(&self) -> ValuePointer { 86 | let mut cur = self; 87 | let mut components = vec![]; 88 | loop { 89 | match cur { 90 | ValuePointerRef::Origin => break, 91 | ValuePointerRef::Key { key, prev } => { 92 | components.push(ValuePointerComponent::Key(key.to_string())); 93 | cur = prev; 94 | } 95 | ValuePointerRef::Index { index, prev } => { 96 | components.push(ValuePointerComponent::Index(*index)); 97 | cur = prev; 98 | } 99 | } 100 | } 101 | let components = components.into_iter().rev().collect(); 102 | ValuePointer { path: components } 103 | } 104 | } 105 | 106 | /// Part of a [`ValuePointer`] 107 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] 108 | pub enum ValuePointerComponent { 109 | Key(String), 110 | Index(usize), 111 | } 112 | 113 | /// The owned version of a [`ValuePointerRef`]. 114 | #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] 115 | pub struct ValuePointer { 116 | pub path: Vec, 117 | } 118 | 119 | /// Equivalent to [`Value`] but without the associated data. 120 | #[derive(Clone, Copy, PartialEq, Eq)] 121 | pub enum ValueKind { 122 | Null, 123 | Boolean, 124 | Integer, 125 | NegativeInteger, 126 | Float, 127 | String, 128 | Sequence, 129 | Map, 130 | } 131 | 132 | impl Display for ValueKind { 133 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 134 | match self { 135 | ValueKind::Null => write!(f, "Null"), 136 | ValueKind::Boolean => write!(f, "Boolean"), 137 | ValueKind::Integer => write!(f, "Integer"), 138 | ValueKind::NegativeInteger => write!(f, "NegativeInteger"), 139 | ValueKind::Float => write!(f, "Float"), 140 | ValueKind::String => write!(f, "String"), 141 | ValueKind::Sequence => write!(f, "Sequence"), 142 | ValueKind::Map => write!(f, "Map"), 143 | } 144 | } 145 | } 146 | 147 | impl Debug for ValueKind { 148 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 149 | Display::fmt(self, f) 150 | } 151 | } 152 | 153 | /// `Value` is a view into the parsed serialization data (of type `V`) that 154 | /// is readable by Deserr. 155 | /// 156 | /// It is an enum with a variant for each possible value kind. The content of the variants 157 | /// is either a simple value, such as `bool` or `String`, or an abstract [`Sequence`] or 158 | /// [`Map`], which are views into the rest of the serialized data. 159 | #[derive(Debug)] 160 | pub enum Value { 161 | Null, 162 | Boolean(bool), 163 | Integer(u64), 164 | NegativeInteger(i64), 165 | Float(f64), 166 | String(String), 167 | Sequence(V::Sequence), 168 | Map(V::Map), 169 | } 170 | 171 | impl Value { 172 | pub fn kind(&self) -> ValueKind { 173 | match self { 174 | Value::Null => ValueKind::Null, 175 | Value::Boolean(_) => ValueKind::Boolean, 176 | Value::Integer(_) => ValueKind::Integer, 177 | Value::NegativeInteger(_) => ValueKind::NegativeInteger, 178 | Value::Float(_) => ValueKind::Float, 179 | Value::String(_) => ValueKind::String, 180 | Value::Sequence(_) => ValueKind::Sequence, 181 | Value::Map(_) => ValueKind::Map, 182 | } 183 | } 184 | } 185 | 186 | /// A trait for a value that can be deserialized via [`Deserr`]. 187 | pub trait IntoValue: Sized { 188 | type Sequence: Sequence; 189 | type Map: Map; 190 | 191 | fn kind(&self) -> ValueKind; 192 | fn into_value(self) -> Value; 193 | } 194 | 195 | /// A sequence of values conforming to [`IntoValue`]. 196 | pub trait Sequence { 197 | type Value: IntoValue; 198 | type Iter: Iterator; 199 | 200 | fn len(&self) -> usize; 201 | fn into_iter(self) -> Self::Iter; 202 | 203 | fn is_empty(&self) -> bool { 204 | self.len() == 0 205 | } 206 | } 207 | 208 | /// A keyed map of values conforming to [`IntoValue`]. 209 | pub trait Map { 210 | type Value: IntoValue; 211 | type Iter: Iterator; 212 | 213 | fn len(&self) -> usize; 214 | fn remove(&mut self, key: &str) -> Option; 215 | fn into_iter(self) -> Self::Iter; 216 | 217 | fn is_empty(&self) -> bool { 218 | self.len() == 0 219 | } 220 | } 221 | 222 | impl IntoValue for Infallible { 223 | type Sequence = Self; 224 | type Map = Self; 225 | 226 | fn kind(&self) -> ValueKind { 227 | unreachable!() 228 | } 229 | 230 | fn into_value(self) -> Value { 231 | unreachable!() 232 | } 233 | } 234 | 235 | impl Sequence for Infallible { 236 | type Value = Self; 237 | type Iter = std::iter::Empty; 238 | 239 | fn len(&self) -> usize { 240 | unreachable!() 241 | } 242 | 243 | fn into_iter(self) -> Self::Iter { 244 | unreachable!() 245 | } 246 | } 247 | 248 | impl Map for Infallible { 249 | type Value = Self; 250 | type Iter = std::iter::Empty<(String, Infallible)>; 251 | 252 | fn len(&self) -> usize { 253 | unreachable!() 254 | } 255 | 256 | fn remove(&mut self, _key: &str) -> Option { 257 | unreachable!() 258 | } 259 | 260 | fn into_iter(self) -> Self::Iter { 261 | unreachable!() 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /derive/src/derive_enum.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | use crate::parse_type::{ 5 | CommonDerivedTypeInfo, 6 | VariantData::{Named, Unit}, 7 | VariantInfo, 8 | }; 9 | 10 | /// Return a token stream that implements `Deserr` for the given derived enum with internal tag 11 | pub fn generate_derive_tagged_enum_impl( 12 | info: CommonDerivedTypeInfo, 13 | tag: String, 14 | variants: Vec, 15 | ) -> TokenStream { 16 | // `variant_impls` is the token stream of the code responsible for deserialising 17 | // all the fields of the enum variants and returning the fully deserialised enum. 18 | let variants_impls = variants 19 | .into_iter() 20 | .map(|v| generate_derive_tagged_enum_variant_impl(&info, &v)) 21 | .collect::>(); 22 | 23 | let CommonDerivedTypeInfo { 24 | impl_trait_tokens, 25 | err_ty, 26 | validate, 27 | } = info; 28 | 29 | quote! { 30 | #impl_trait_tokens { 31 | fn deserialize_from_value(deserr_value__: ::deserr::Value, deserr_location__: ::deserr::ValuePointerRef) -> ::std::result::Result { 32 | // The value must always be a map 33 | let deserr_final__ = match deserr_value__ { 34 | ::deserr::Value::Map(mut deserr_map__) => { 35 | let tag_value = ::deserr::Map::remove(&mut deserr_map__, #tag).ok_or_else(|| { 36 | ::deserr::take_cf_content(<#err_ty as ::deserr::DeserializeError>::error::( 37 | None, 38 | ::deserr::ErrorKind::MissingField { 39 | field: #tag, 40 | }, 41 | deserr_location__ 42 | )) 43 | })?; 44 | let tag_value_string = match tag_value.into_value() { 45 | ::deserr::Value::String(x) => x, 46 | v => { 47 | return ::std::result::Result::Err( 48 | ::deserr::take_cf_content(<#err_ty as ::deserr::DeserializeError>::error::( 49 | None, 50 | ::deserr::ErrorKind::IncorrectValueKind { 51 | actual: v, 52 | accepted: &[::deserr::ValueKind::String], 53 | }, 54 | deserr_location__.push_key(#tag) 55 | )) 56 | ); 57 | } 58 | }; 59 | 60 | match tag_value_string.as_str() { 61 | #(#variants_impls)* 62 | // this is the case where the tag exists and is a string, but its value does not 63 | // correspond to any valid enum variant name 64 | _ => { 65 | ::std::result::Result::Err( 66 | ::deserr::take_cf_content(<#err_ty as ::deserr::DeserializeError>::error::( 67 | None, 68 | // TODO: expected one of {expected_tags_list}, found {actual_tag} error message 69 | ::deserr::ErrorKind::Unexpected { 70 | msg: "Incorrect tag value".to_string(), 71 | }, 72 | deserr_location__ 73 | )) 74 | ) 75 | } 76 | } 77 | }, 78 | // this is the case where the value is not a map 79 | v => { 80 | ::std::result::Result::Err( 81 | ::deserr::take_cf_content(<#err_ty as ::deserr::DeserializeError>::error::( 82 | None, 83 | ::deserr::ErrorKind::IncorrectValueKind { 84 | actual: v, 85 | accepted: &[::deserr::ValueKind::Map], 86 | }, 87 | deserr_location__ 88 | )) 89 | ) 90 | } 91 | }?; 92 | #validate 93 | } 94 | } 95 | } 96 | } 97 | 98 | /// Create a token stream that deserialises all the fields of the enum variant and return 99 | /// the fully deserialised enum. 100 | /// 101 | /// The context of the token stream is: 102 | /// 103 | /// ```ignore 104 | /// let map: Map 105 | /// match tag_value_string.as_str() { 106 | /// === here === 107 | /// key => { .. } 108 | /// } 109 | /// ``` 110 | /// 111 | fn generate_derive_tagged_enum_variant_impl( 112 | info: &CommonDerivedTypeInfo, 113 | variant: &VariantInfo, 114 | ) -> TokenStream { 115 | let CommonDerivedTypeInfo { err_ty, .. } = info; 116 | 117 | let VariantInfo { 118 | ident: variant_ident, 119 | data, 120 | key_name: variant_key_name, 121 | } = variant; 122 | 123 | match data { 124 | Unit => { 125 | // If the enum variant is a unit variant, there is nothing else to do. 126 | quote! { 127 | #variant_key_name => { 128 | ::std::result::Result::Ok(Self::#variant_ident) 129 | } 130 | } 131 | } 132 | Named(fields) => { 133 | let fields_impl = crate::generate_named_fields_impl( 134 | fields, 135 | err_ty, 136 | quote! { Self :: #variant_ident }, 137 | ); 138 | // The code here is virtually identical to the code of `generate_derive_struct_impl` 139 | quote! { 140 | #variant_key_name => { 141 | let mut deserr_error__ = None; 142 | #fields_impl 143 | } 144 | } 145 | } 146 | } 147 | } 148 | 149 | /// Create a token stream that deserialises all the fields of the enum variant and return 150 | /// the fully deserialised enum. 151 | /// /!\ Currently, we only support untagged enum that only contains unit variants. 152 | /// 153 | /// The context of the token stream is: 154 | /// 155 | /// ```ignore 156 | /// let map: Map 157 | /// match tag_value_string.as_str() { 158 | /// === here === 159 | /// key => { .. } 160 | /// } 161 | /// ``` 162 | /// 163 | pub fn generate_derive_untagged_enum_impl( 164 | info: CommonDerivedTypeInfo, 165 | variants: Vec, 166 | ) -> TokenStream { 167 | // all the variant of the enum as a slice of `&str` 168 | let all_variants_as_str = variants 169 | .iter() 170 | .map(|v| &v.key_name) 171 | .map(|v| quote!(#v, )) 172 | .collect::(); 173 | let all_variants_as_str = quote!(&[#all_variants_as_str]); 174 | 175 | // `variant_impls` is the token stream of the code responsible for deserialising 176 | // the enum variants and returning the correct variant. Since we've already ensured 177 | // the enum was untagged, we can re-use the `generate_derive_tagged_enum_variant_impl` 178 | // function and only use the `Unit` part of the match. 179 | let variants_impls = variants 180 | .into_iter() 181 | .map(|v| generate_derive_tagged_enum_variant_impl(&info, &v)) 182 | .collect::>(); 183 | 184 | let CommonDerivedTypeInfo { 185 | impl_trait_tokens, 186 | err_ty, 187 | validate, 188 | } = info; 189 | 190 | quote! { 191 | #impl_trait_tokens { 192 | fn deserialize_from_value(deserr_value__: ::deserr::Value, deserr_location__: ::deserr::ValuePointerRef) -> ::std::result::Result { 193 | // The value must always be a string 194 | let deserr_final__ = match deserr_value__ { 195 | ::deserr::Value::String(s) => { 196 | match s.as_str() { 197 | #(#variants_impls)* 198 | // this is the case where the tag exists and is a string, but its value does not 199 | // correspond to any valid enum variant name 200 | s => { 201 | ::std::result::Result::Err( 202 | ::deserr::take_cf_content(<#err_ty as ::deserr::DeserializeError>::error::( 203 | None, 204 | ::deserr::ErrorKind::UnknownValue { 205 | value: s, 206 | accepted: #all_variants_as_str, 207 | }, 208 | deserr_location__ 209 | )) 210 | ) 211 | } 212 | } 213 | }, 214 | // this is the case where the value is not a String 215 | v => { 216 | ::std::result::Result::Err( 217 | ::deserr::take_cf_content(<#err_ty as ::deserr::DeserializeError>::error::( 218 | None, 219 | ::deserr::ErrorKind::IncorrectValueKind { 220 | actual: v, 221 | accepted: &[::deserr::ValueKind::String], 222 | }, 223 | deserr_location__ 224 | )) 225 | ) 226 | } 227 | }?; 228 | #validate 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /book/src/attributes/field.md: -------------------------------------------------------------------------------- 1 | # Field attributes 2 | 3 | ### `#[deserr(rename = "...")]` 4 | 5 | Deserialize this field with the given name instead of its Rust name. 6 | 7 | ```rust 8 | use deserr::{Deserr, deserialize, errors::JsonError}; 9 | use serde_json::json; 10 | 11 | #[derive(Deserr, Debug, PartialEq, Eq)] 12 | struct Search { 13 | query: String, 14 | #[deserr(rename = "atr")] 15 | attributes_to_retrieve: Vec, 16 | } 17 | 18 | let data = deserialize::( 19 | json!({ "query": "doggo", "atr": ["age", "name"] }), 20 | ) 21 | .unwrap(); 22 | assert_eq!(data, Search { 23 | query: String::from("doggo"), 24 | attributes_to_retrieve: vec![String::from("age"), String::from("name")], 25 | }); 26 | ``` 27 | 28 | [Also available as a variant attribute.](variant.md#deserrrename) 29 | 30 | ### `#[deserr(from)]` 31 | 32 | Deserializing a type from a function instead of a `Value`. 33 | You need to provide the following information; 34 | 1. The input type of the function (here `&String`) 35 | 2. The path of the function (here, we're simply using the std `FromStr` implementation) 36 | 37 | deserr will first try to deserialize the given type using its `Deserr` implementation. 38 | That means the input type of the `from` can be complex. Then deserr will call your 39 | function. 40 | 41 | - [If your function can fail, consider using `try_from` instead](#deserrtryfrom) 42 | - [The container attribute may interests you as well](container.md#deserrfrom) 43 | 44 | ```rust 45 | use deserr::{Deserr, deserialize, errors::JsonError}; 46 | use serde_json::json; 47 | 48 | #[derive(Deserr, Debug, PartialEq, Eq)] 49 | #[deserr(from(String) = From::from)] 50 | enum Wildcard { 51 | Wildcard, 52 | Value(String), 53 | } 54 | 55 | impl From for Wildcard { 56 | fn from(s: String) -> Self { 57 | if s == "*" { 58 | Wildcard::Wildcard 59 | } else { 60 | Wildcard::Value(s) 61 | } 62 | } 63 | } 64 | 65 | #[derive(Deserr, Debug, PartialEq, Eq)] 66 | struct Search { 67 | query: String, 68 | #[deserr(from(String) = From::from)] 69 | field: Wildcard, 70 | } 71 | 72 | let data = deserialize::( 73 | json!({ "query": "doggo", "field": "catto" }), 74 | ) 75 | .unwrap(); 76 | assert_eq!(data, Search { query: String::from("doggo"), field: Wildcard::Value(String::from("catto")) }); 77 | 78 | let data = deserialize::( 79 | json!({ "query": "doggo", "field": "*" }), 80 | ) 81 | .unwrap(); 82 | assert_eq!(data, Search { query: String::from("doggo"), field: Wildcard::Wildcard }); 83 | ``` 84 | 85 | ### `#[deserr(try_from)]` 86 | 87 | Try deserializing a type from a function instead of a `Value`. 88 | You need to provide the following information; 89 | 1. The input type of the function (here `&String`) 90 | 2. The path of the function (here, we're simply using the std `FromStr` implementation) 91 | 3. The error type that this function can return (here `ParseIntError`) 92 | 93 | deserr will first try to deserialize the given type using its `Deserr` implementation. 94 | That means the input type of the `try_from` can be complex. Then deserr will call your 95 | function and accumulate the specified error against the error type of the caller. 96 | 97 | - [If your function cannot fail, consider using `from` instead](#deserrfrom) 98 | - [The container attribute may interests you as well](container.md#deserrtryfrom) 99 | 100 | ```rust 101 | use deserr::{Deserr, deserialize, errors::JsonError}; 102 | use serde_json::json; 103 | use std::convert::Infallible; 104 | use std::str::FromStr; 105 | use std::num::ParseIntError; 106 | 107 | #[derive(Deserr, Debug, PartialEq, Eq)] 108 | struct Search { 109 | query: String, 110 | #[deserr(try_from(&String) = FromStr::from_str -> ParseIntError)] 111 | limit: usize, 112 | 113 | } 114 | 115 | let data = deserialize::( 116 | json!({ "query": "doggo", "limit": "12" }), 117 | ) 118 | .unwrap(); 119 | assert_eq!(data, Search { query: String::from("doggo"), limit: 12 }); 120 | 121 | let error = deserialize::( 122 | json!({ "query": "doggo", "limit": 12 }), 123 | ) 124 | .unwrap_err(); 125 | assert_eq!(error.to_string(), "Invalid value type at `.limit`: expected a string, but found a positive integer: `12`"); 126 | ``` 127 | 128 | ### `#[deserr(default)]` 129 | 130 | Allows you to specify a default value for a field. 131 | 132 | Note that, unlike serde, by default, `Option` doesn't automatically use this attribute. 133 | Here you need to explicitly define whether your type can get a default value. 134 | This makes it less error-prone and easier to make an optional field mandatory. 135 | 136 | ```rust 137 | use deserr::{Deserr, deserialize, errors::JsonError}; 138 | use serde_json::json; 139 | 140 | #[derive(Deserr, Debug, PartialEq, Eq)] 141 | struct Search { 142 | #[deserr(default)] 143 | query: Option, 144 | #[deserr(default = 20)] 145 | limit: usize, 146 | } 147 | 148 | let data = deserialize::( 149 | json!({ "query": "doggo", "limit": 4 }), 150 | ) 151 | .unwrap(); 152 | assert_eq!(data, Search { query: Some(String::from("doggo")), limit: 4 }); 153 | 154 | let data = deserialize::( 155 | json!({ "query": "doggo" }), 156 | ) 157 | .unwrap(); 158 | assert_eq!(data, Search { query: Some(String::from("doggo")), limit: 20 }); 159 | ``` 160 | 161 | ### `#[deserr(skip)]` 162 | 163 | Allows you to skip the deserialization of a field. 164 | It won't show up in the list of fields generated by `deny_unknown_fields` or in the 165 | `UnknownKey` variant of the `ErrorKind` type. 166 | 167 | ```rust 168 | use deserr::{Deserr, deserialize, errors::JsonError}; 169 | use serde_json::json; 170 | 171 | #[derive(Deserr, Debug, PartialEq, Eq)] 172 | struct Search { 173 | query: String, 174 | // A field can be skipped if it implements `Default` or if the `default` attribute is specified. 175 | #[deserr(skip)] 176 | hidden: usize, 177 | } 178 | 179 | let data = deserialize::( 180 | json!({ "query": "doggo" }), 181 | ) 182 | .unwrap(); 183 | assert_eq!(data, Search { query: String::from("doggo"), hidden: 0 }); 184 | 185 | // if you try to specify the field, it is ignored 186 | let data = deserialize::( 187 | json!({ "query": "doggo", "hidden": 2 }), 188 | ) 189 | .unwrap(); 190 | assert_eq!(data, Search { query: String::from("doggo"), hidden: 0 }); 191 | 192 | // Here, we're going to see how skip interacts with `deny_unknown_fields` 193 | 194 | #[derive(Deserr, Debug, PartialEq, Eq)] 195 | #[deserr(deny_unknown_fields)] 196 | struct Search2 { 197 | query: String, 198 | // A field can be skipped if it implements `Default`. 199 | #[deserr(skip)] 200 | hidden: usize, 201 | } 202 | 203 | let error = deserialize::( 204 | json!({ "query": "doggo", "hidden": 1 }), 205 | ) 206 | .unwrap_err(); 207 | // NOTE: `hidden` isn't in the list of expected fields + `hidden` is effectively considered as a non-existing field. 208 | assert_eq!(error.to_string(), "Unknown field `hidden`: expected one of `query`"); 209 | ``` 210 | 211 | ### `#[deserr(map)]` 212 | 213 | Map a field **after** it has been deserialized. 214 | 215 | ```rust 216 | use deserr::{Deserr, deserialize, errors::JsonError}; 217 | use serde_json::json; 218 | 219 | #[derive(Deserr, Debug, PartialEq, Eq)] 220 | struct Search { 221 | query: String, 222 | #[deserr(map = add_one)] 223 | limit: usize, 224 | } 225 | 226 | fn add_one(n: usize) -> usize { 227 | n.saturating_add(1) 228 | } 229 | 230 | let data = deserialize::( 231 | json!({ "query": "doggo", "limit": 0 }), 232 | ) 233 | .unwrap(); 234 | assert_eq!(data, Search { query: String::from("doggo"), limit: 1 }); 235 | 236 | // Let's see how `map` interacts with the `default` attributes. 237 | #[derive(Deserr, Debug, PartialEq, Eq)] 238 | struct Search2 { 239 | query: String, 240 | #[deserr(default, map = add_one)] 241 | limit: usize, 242 | } 243 | 244 | let data = deserialize::( 245 | json!({ "query": "doggo" }), 246 | ) 247 | .unwrap(); 248 | // As we can see, the `map` attribute is applied AFTER the `default`. 249 | assert_eq!(data, Search2 { query: String::from("doggo"), limit: 1 }); 250 | ``` 251 | 252 | ### `#[deserr(missing_field_error)]` 253 | 254 | Gives you the opportunity to customize the error message if this specific field 255 | is missing. 256 | 257 | ```rust 258 | use deserr::{Deserr, DeserializeError, ValuePointerRef, ErrorKind, deserialize, errors::JsonError}; 259 | use serde_json::json; 260 | use std::convert::Infallible; 261 | 262 | #[derive(Deserr, Debug, PartialEq, Eq)] 263 | struct Search { 264 | #[deserr(missing_field_error = missing_query_field)] 265 | query: String, 266 | limit: usize, 267 | } 268 | 269 | fn missing_query_field(_field_name: &str, location: ValuePointerRef) -> E { 270 | deserr::take_cf_content(E::error::( 271 | None, 272 | ErrorKind::Unexpected { 273 | msg: String::from("I really need the query field, please give it to me uwu"), 274 | }, 275 | location, 276 | )) 277 | } 278 | 279 | let error = deserialize::( 280 | json!({ "limit": 0 }), 281 | ) 282 | .unwrap_err(); 283 | assert_eq!(error.to_string(), "Invalid value: I really need the query field, please give it to me uwu"); 284 | ``` 285 | 286 | ### `#[deserr(error)]` 287 | 288 | Customize the error type that can be returned when deserializing this structure 289 | instead of keeping it generic. 290 | 291 | ```rust 292 | use deserr::{Deserr, DeserializeError, ValuePointerRef, ErrorKind, deserialize, errors::JsonError}; 293 | use serde_json::json; 294 | 295 | // Since the error returned by the `Search` structure needs to implements `MergeWithError` 296 | // we also need to specify the `error` attribute as a `JsonError`. But as you will see later there are 297 | // other solutions. 298 | #[derive(Deserr, Debug, PartialEq, Eq)] 299 | #[deserr(error = JsonError)] 300 | struct Search { 301 | #[deserr(error = JsonError)] 302 | query: A, 303 | limit: usize, 304 | } 305 | ``` 306 | 307 | ### `#[deserr(needs_predicate)]` 308 | 309 | Automatically adds `where_predicate = FieldType: Deserr` for each field with this attribute. 310 | 311 | ```rust 312 | use deserr::{Deserr, DeserializeError, MergeWithError, deserialize, errors::JsonError}; 313 | use serde_json::json; 314 | 315 | #[derive(Deserr, Debug, PartialEq, Eq)] 316 | struct Search { 317 | #[deserr(needs_predicate)] 318 | query: A, 319 | limit: usize, 320 | } 321 | ``` 322 | 323 | Is strictly equivalent to the following: 324 | 325 | ```rust 326 | use deserr::{Deserr, DeserializeError, MergeWithError, deserialize, errors::JsonError}; 327 | use serde_json::json; 328 | 329 | // `__Deserr_E` represents the Error returned by the generated `Deserr` implementation. 330 | #[derive(Deserr, Debug, PartialEq, Eq)] 331 | #[deserr(where_predicate = A: Deserr<__Deserr_E>)] 332 | struct Search { 333 | query: A, 334 | limit: usize, 335 | } 336 | ``` 337 | -------------------------------------------------------------------------------- /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 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /book/src/attributes/container.md: -------------------------------------------------------------------------------- 1 | # Container attributes 2 | 3 | ### `#[deserr(rename_all = ...)]` 4 | 5 | Rename all the fields (if this is a struct) or variants (if this is an enum) according to the given case convention. 6 | The possible values are: `lowercase`, `camelCase`. 7 | 8 | If you need more values please open an issue, it's easy to implement and was simply not implemented because it isn't required for Meilisearch at the moment. 9 | 10 |
11 | 12 | Unlike `serde`, you don't need to put the double-quotes (`"`) around the name of the case, e.g.: `#[deserr(rename_all = camelCase)]`. 13 | 14 |
15 | 16 | ```rust 17 | use deserr::{Deserr, deserialize, errors::JsonError}; 18 | use serde_json::json; 19 | 20 | #[derive(Deserr, Debug, PartialEq, Eq)] 21 | #[deserr(rename_all = camelCase)] 22 | struct Search { 23 | query: String, 24 | attributes_to_retrieve: Vec, 25 | } 26 | 27 | let data = deserialize::( 28 | json!({ "query": "doggo", "attributesToRetrieve": ["age", "name"] }), 29 | ) 30 | .unwrap(); 31 | assert_eq!(data, Search { 32 | query: String::from("doggo"), 33 | attributes_to_retrieve: vec![String::from("age"), String::from("name")], 34 | }); 35 | ``` 36 | 37 | ### `#[deserr(deny_unknown_fields)]` 38 | 39 | Always error during deserialization when encountering unknown fields. 40 | When this attribute is not present, by default unknown fields are silently ignored. 41 | 42 | ```rust 43 | use deserr::{Deserr, deserialize, errors::JsonError}; 44 | use serde_json::json; 45 | 46 | #[derive(Deserr, Debug)] 47 | #[deserr(deny_unknown_fields)] 48 | struct Search { 49 | query: String, 50 | } 51 | 52 | let err = deserialize::( 53 | json!({ "query": "doggo", "doggo": "bork" }), 54 | ) 55 | .unwrap_err(); 56 | 57 | assert_eq!(err.to_string(), "Unknown field `doggo`: expected one of `query`"); 58 | ``` 59 | 60 | 61 |
62 | 63 | Unlike `serde`, with `deserr` you can specify provide a custom function to handle the error. 64 | 65 | ```rust 66 | use deserr::{Deserr, deserialize, ErrorKind, DeserializeError, ValuePointerRef, take_cf_content, errors::JsonError}; 67 | use std::convert::Infallible; 68 | use serde_json::json; 69 | 70 | #[derive(Deserr, Debug)] 71 | #[deserr(deny_unknown_fields = unknown_fields_search)] 72 | struct Search { 73 | query: String, 74 | } 75 | 76 | fn unknown_fields_search( 77 | field: &str, 78 | accepted: &[&str], 79 | location: ValuePointerRef, 80 | ) -> E { 81 | // `E::error` returns a `ControlFlow`, which returns the error and indicates 82 | // whether we should keep accumulating errors or not. However, here we simply 83 | // want to retrieve the error's value. This is what `take_cf_content` does. 84 | match field { 85 | "doggo" => take_cf_content(E::error::( 86 | None, 87 | ErrorKind::Unexpected { 88 | msg: String::from("can I pet the doggo? uwu") 89 | }, 90 | location, 91 | )), 92 | _ => take_cf_content(E::error::( 93 | None, 94 | deserr::ErrorKind::UnknownKey { key: field, accepted }, 95 | location, 96 | )), 97 | } 98 | } 99 | 100 | let err = deserialize::( 101 | json!({ "query": "doggo", "doggo": "bork" }), 102 | ) 103 | .unwrap_err(); 104 | 105 | assert_eq!(err.to_string(), "Invalid value: can I pet the doggo? uwu"); 106 | 107 | let err = deserialize::( 108 | json!({ "query": "doggo", "catto": "jorts" }), 109 | ) 110 | .unwrap_err(); 111 | 112 | assert_eq!(err.to_string(), "Unknown field `catto`: expected one of `query`"); 113 | ``` 114 | 115 |
116 | 117 | ### `#[deserr(tag)]` 118 | 119 | Externally tag an enum. 120 | 121 |
122 | 123 | Deserr does not support internally tagging your enum yet, which means you'll always need to use this attribute if you're deserializing an enum. 124 | 125 |
126 | 127 | For complete unit enums, deserr can deserialize their value from a string, though. 128 | 129 | ```rust 130 | use deserr::{Deserr, deserialize, errors::JsonError}; 131 | use serde_json::json; 132 | 133 | #[derive(Deserr, Debug, PartialEq, Eq)] 134 | struct Search { 135 | query: Query, 136 | } 137 | 138 | #[derive(Deserr, Debug, PartialEq, Eq)] 139 | #[deserr(tag = "type")] 140 | enum Query { 141 | Single { 142 | search: String, 143 | }, 144 | Multi { 145 | searches: Vec, 146 | } 147 | } 148 | 149 | let data = deserialize::( 150 | json!({ "query": { "type": "Single", "search": "bork" } }), 151 | ) 152 | .unwrap(); 153 | assert_eq!(data, Search { 154 | query: Query::Single { 155 | search: String::from("bork"), 156 | }, 157 | }); 158 | ``` 159 | 160 | ### `#[deserr(from)]` 161 | 162 | Deserializing a type from a function instead of a `Value`. 163 | You need to provide the following information; 164 | 1. The input type of the function (here `&String`) 165 | 2. The path of the function (here, we're simply using the std `FromStr` implementation) 166 | 167 | deserr will first try to deserialize the given type using its `Deserr` implementation. 168 | That means the input type of the `from` can be complex. Then deserr will call your 169 | function. 170 | 171 | - [If your function can fail, consider using `try_from` instead](#deserrtryfrom) 172 | - [The field attribute may interests you as well](field.md#deserrfrom) 173 | 174 | ```rust 175 | use deserr::{Deserr, deserialize, errors::JsonError}; 176 | use serde_json::json; 177 | 178 | #[derive(Deserr, Debug, PartialEq, Eq)] 179 | #[deserr(from(String) = From::from)] 180 | enum Wildcard { 181 | Wildcard, 182 | Value(String), 183 | } 184 | 185 | impl From for Wildcard { 186 | fn from(s: String) -> Self { 187 | if s == "*" { 188 | Wildcard::Wildcard 189 | } else { 190 | Wildcard::Value(s) 191 | } 192 | } 193 | } 194 | 195 | let data = deserialize::( 196 | json!("doggo"), 197 | ) 198 | .unwrap(); 199 | assert_eq!(data, Wildcard::Value(String::from("doggo"))); 200 | 201 | let data = deserialize::( 202 | json!("*"), 203 | ) 204 | .unwrap(); 205 | assert_eq!(data, Wildcard::Wildcard); 206 | ``` 207 | 208 | ### `#[deserr(try_from)]` 209 | 210 | Try deserializing a type from a function instead of a `Value`. 211 | You need to provide the following information; 212 | 1. The input type of the function (here `&String`) 213 | 2. The path of the function (here, we're simply using the std `FromStr` implementation) 214 | 3. The error type that this function can return (here `AsciiStringError`) 215 | 216 | deserr will first try to deserialize the given type using its `Deserr` implementation. 217 | That means the input type of the `try_from` can be complex. Then deserr will call your 218 | function and accumulate the specified error against the error type of the caller. 219 | 220 | - [If your function cannot fail, consider using `from` directly](#deserrfrom) 221 | - [The field attribute may interests you as well](field.md#deserrtryfrom) 222 | 223 | ```rust 224 | use deserr::{Deserr, deserialize, errors::JsonError}; 225 | use serde_json::json; 226 | use std::str::FromStr; 227 | use std::fmt; 228 | 229 | // Notice how the `try_from` allows us to leverage the deserr limitation on tuple struct. 230 | #[derive(Deserr, Debug, PartialEq, Eq)] 231 | #[deserr(try_from(&String) = FromStr::from_str -> AsciiStringError)] 232 | struct AsciiString(String); 233 | 234 | #[derive(Debug)] 235 | struct AsciiStringError(char); 236 | 237 | impl fmt::Display for AsciiStringError { 238 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 239 | write!( 240 | f, 241 | "Encountered invalid character: `{}`, only ascii characters are accepted", 242 | self.0 243 | ) 244 | } 245 | } 246 | impl std::error::Error for AsciiStringError {} 247 | 248 | impl FromStr for AsciiString { 249 | type Err = AsciiStringError; 250 | 251 | fn from_str(s: &str) -> Result { 252 | if let Some(c) = s.chars().find(|c| !c.is_ascii()) { 253 | Err(AsciiStringError(c)) 254 | } else { 255 | Ok(Self(s.to_string())) 256 | } 257 | } 258 | } 259 | 260 | let data = deserialize::( 261 | json!("doggo"), 262 | ) 263 | .unwrap(); 264 | assert_eq!(data, AsciiString(String::from("doggo"))); 265 | 266 | let error = deserialize::( 267 | json!("👉👈"), 268 | ) 269 | .unwrap_err(); 270 | assert_eq!(error.to_string(), "Invalid value: Encountered invalid character: `👉`, only ascii characters are accepted"); 271 | ``` 272 | 273 | ### `#[deserr(validate)]` 274 | 275 | Validate a structure **after** it has been deserialized. 276 | This is typically useful when your validation logic needs to take multiple fields into account. 277 | 278 | ```rust 279 | use deserr::{Deserr, DeserializeError, ErrorKind, ValuePointerRef, deserialize, errors::JsonError}; 280 | use serde_json::json; 281 | use std::convert::Infallible; 282 | 283 | // `__Deserr_E` represents the Error returned by the generated `Deserr` implementation. 284 | #[derive(Deserr, Debug, PartialEq, Eq)] 285 | #[deserr(validate = validate_range -> __Deserr_E)] 286 | struct Range { 287 | min: u8, 288 | max: u8, 289 | } 290 | 291 | fn validate_range( 292 | range: Range, 293 | location: ValuePointerRef, 294 | ) -> Result { 295 | if range.min > range.max { 296 | Err(deserr::take_cf_content(E::error::( 297 | None, 298 | ErrorKind::Unexpected { 299 | msg: format!( 300 | "`max` (`{}`) should be greater than `min` (`{}`)", 301 | range.max, range.min 302 | ), 303 | }, 304 | location, 305 | ))) 306 | } else { 307 | Ok(range) 308 | } 309 | } 310 | 311 | let data = deserialize::( 312 | json!({ "min": 2, "max": 4 }), 313 | ) 314 | .unwrap(); 315 | assert_eq!(data, Range { min: 2, max: 4 }); 316 | 317 | let error = deserialize::( 318 | json!({ "min": 4, "max": 2 }), 319 | ) 320 | .unwrap_err(); 321 | assert_eq!(error.to_string(), "Invalid value: `max` (`2`) should be greater than `min` (`4`)"); 322 | ``` 323 | 324 | ### `#[deserr(error)]` 325 | 326 | Customize the error type that can be returned when deserializing this structure 327 | instead of keeping it generic. 328 | 329 | ```rust 330 | use deserr::{Deserr, DeserializeError, ValuePointerRef, ErrorKind, deserialize, errors::JsonError}; 331 | use serde_json::json; 332 | 333 | #[derive(Deserr, Debug, PartialEq, Eq)] 334 | #[deserr(error = JsonError)] 335 | struct Search { 336 | query: String, 337 | limit: usize, 338 | } 339 | 340 | // As we can see, rust is able to infer the error type. 341 | let data = deserialize::( 342 | json!({ "query": "doggo", "limit": 1 }), 343 | ) 344 | .unwrap(); 345 | assert_eq!(data, Search { query: String::from("doggo"), limit: 1 }); 346 | ``` 347 | 348 | ### `#[deserr(where_predicate)]` 349 | 350 | Let you add `where` clauses to the `Deserr` implementation that deserr will generate. 351 | 352 | ```rust 353 | use deserr::{Deserr, DeserializeError, MergeWithError, deserialize, errors::JsonError}; 354 | use serde_json::json; 355 | 356 | // Here we can constraint the generic `__Deserr_E` type used by deserr to implements `MergeWithError`. 357 | // Now instead of constraining the final error type it stays generic if it's able to accumulate with 358 | // with a `JsonError`. 359 | #[derive(Deserr, Debug, PartialEq, Eq)] 360 | #[deserr(where_predicate = __Deserr_E: MergeWithError, where_predicate = A: Deserr)] 361 | struct Search
{ 362 | #[deserr(error = JsonError)] 363 | query: A, 364 | limit: usize, 365 | } 366 | ``` 367 | 368 | [For simple cases, see also the `needs_predicate` field attribute.](field.md#deserrneedspredicate) 369 | -------------------------------------------------------------------------------- /book/src/overview.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # Overview 4 | 5 | Deserr is a crate for deserializing data, with the ability to return 6 | custom, type-specific errors upon failure. It was also designed with 7 | user-facing APIs in mind and thus provides better defaults than serde for 8 | this use case. 9 | 10 | Unlike serde, deserr does not parse the data in its serialization format itself 11 | but offloads the work to other crates. Instead, it deserializes 12 | the already-parsed serialized data into the final type. For example: 13 | 14 | ```rust,ignore 15 | // bytes of the serialized value 16 | let s: &str = ".." ; 17 | // parse serialized data using another crate, such as `serde_json` 18 | let json: serde_json::Value = serde_json::from_str(s).unwrap(); 19 | // finally deserialize with deserr 20 | let data = T::deserialize_from_value(json.into_value()).unwrap(); 21 | // `T` must implement `Deserr`. 22 | ``` 23 | 24 | ## Why would I use it 25 | 26 | The main place where you should use deserr is on your user-facing API, 27 | especially if it's supposed to be read by a human. 28 | Since deserr gives you full control over your error types, you can improve 29 | the quality of your error messages. 30 | Here is a little preview of what you can do with deserr: 31 | 32 | 33 | Let's say I sent this payload to update my [Meilisearch](https://docs.meilisearch.com/reference/api/settings.html#settings) settings: 34 | ```json 35 | { 36 | "filterableAttributes": ["doggo.age", "catto.age"], 37 | "sortableAttributes": ["uploaded_at"], 38 | "typoTolerance": { 39 | "minWordSizeForTypos": { 40 | "oneTypo": 1000, "twoTypo": 80 41 | }, 42 | "enabled": true 43 | }, 44 | "displayedAttributes": ["*"], 45 | "searchableAttributes": ["doggo.name", "catto.name"] 46 | } 47 | ``` 48 | 49 | #### With serde 50 | 51 | With serde, we don't have much customization; this is the typical kind of message we would get in return: 52 | 53 | ```json 54 | { 55 | "message": "Json deserialize error: invalid value: integer `1000`, expected u8 at line 6 column 21", 56 | "code": "bad_request", 57 | "type": "invalid_request", 58 | "link": "https://docs.meilisearch.com/errors#bad_request" 59 | } 60 | ``` 61 | 62 | ##### The message 63 | 64 | > Json deserialize error: invalid value: integer `1000`, expected u8 at line 6 column 21 65 | 66 | - The message uses the word `u8`, which definitely won't help a user who doesn't know rust or is unfamiliar with types. 67 | - The location is provided in terms of lines and columns. While this is generally good, when most of your users 68 | read this message in their terminal, it doesn't actually help much. 69 | 70 | #### The rest of the payload 71 | 72 | Since serde returned this error, we cannot know what happened or on which field it happened. Thus, the best we 73 | can do is generate a code `bad_request` that is common for our whole API. We then use this code to generate 74 | a link to our documentation to help our users. But such a generic link does not help our users because it 75 | can be thrown by every single route of Meilisearch. 76 | 77 | #### With deserr 78 | 79 | ```json 80 | { 81 | "message": "Invalid value at `.typoTolerance.minWordSizeForTypos.oneTypo`: value: `1000` is too large to be deserialized, maximum value authorized is `255`", 82 | "code": "invalid_settings_typo_tolerance", 83 | "type": "invalid_request", 84 | "link": "https://docs.meilisearch.com/errors#invalid-settings-typo-tolerance" 85 | } 86 | ``` 87 | 88 | ##### The message 89 | 90 | > Invalid value at `.typoTolerance.minWordSizeForTypos.oneTypo`: value: `1000` is too large to be deserialized, maximum value authorized is `255` 91 | 92 | - We get a more human-readable location; `.typoTolerance.minWordSizeForTypos.oneTypo`. It gives us the faulty field. 93 | - We also get a non-confusing and helpful message this time; it explicitly tells us that the maximum value authorized is `255`. 94 | 95 | ##### The rest of the payload 96 | 97 | Since deserr called one of our functions in the process, we were able to use a custom error code + link to redirect 98 | our user to the documentation specific to this feature and this field. 99 | 100 | #### More possibilities with deserr that were impossible with serde 101 | 102 | ##### Adding constraints on multiples fields 103 | 104 | In Meilisearch, there is another constraint on this `minWordSizeForTypos`, the `twoTypo` field **must be** greater than 105 | the `oneType` field. 106 | 107 | Serde doesn't provide any feature to do that. You could write your own implementation of `Deserialize` for the 108 | entire sub-object `minWordSizeForTypos`, but that's generally hard and wouldn't even let you customize the 109 | error type. 110 | Thus, that's the kind of thing you're going to check by hand in your code later on. This is error-prone and 111 | may bring inconsistencies between most of the deserialization error messages and your error message. 112 | 113 | With deserr, we provide attributes that allow you to validate your structure once it's deserialized. 114 | 115 | ##### When a field is missing 116 | 117 | It's possible to provide your own function when a field is missing. 118 | 119 | ```rust,ignore 120 | pub fn missing_field(field: &str, location: ValuePointerRef) -> E { 121 | todo!() 122 | } 123 | ``` 124 | 125 | At Meilisearch, we use this function to specify a custom error code, but we keep the default error message which is pretty accurate. 126 | 127 | ##### When an unknown field is encountered 128 | 129 | It's possible to provide your own function when a field is missing. 130 | 131 | ```rust,ignore 132 | fn unknown_field( 133 | field: &str, 134 | accepted: &[&str], 135 | location: ValuePointerRef, 136 | ) -> E { 137 | todo!() 138 | } 139 | ``` 140 | 141 | Here is a few ideas we have or would like to implement at Meilisearch; 142 | - In the case of a resource you can `PUT` with some fields, but can't `PATCH` all its fields. We can throw a special `immutable field x` error instead of an `unknown field x`. 143 | - Detecting when you use the field name of an alternative; for example, we use `q` to make a `query` while some Meilisearch alternatives use `query`. 144 | We could help our users with a `did you mean?` message that corrects the field to its proper name in Meilisearch. 145 | - Trying to guess what the user was trying to say by computing the [levenstein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) 146 | between what the user typed and what is accepted to provide a `did you mean?` message that attempts to correct typos. 147 | 148 | ##### When multiple errors are encountered 149 | 150 | Deserr lets you accumulate multiple errors with its `MergeWithError` trait while trying to deserialize the value into your type. 151 | This is a good way to improve your user experience by reducing the number of interactions 152 | a user needs to have to fix an invalid payload. 153 | 154 | ----------- 155 | 156 | The main parts of deserr are: 157 | 1. `Deserr` is the main trait for deserialization, unlike Serde, it's very easy to deserialize this trait manually, see the `implements_deserr_manually.rs` file in our examples directory. 158 | 2. `IntoValue` and `Value` describes the shape that the parsed serialized data must have 159 | 3. `DeserializeError` is the trait that all deserialization errors must conform to 160 | 4. `MergeWithError` describe how to combine multiple errors together. It allows deserr 161 | to return multiple deserialization errors at once. 162 | 5. `ValuePointerRef` and `ValuePointer` point to locations within the value. They are 163 | used to locate the origin of an error. 164 | 6. `deserialize` is the main function to use to deserialize a value. 165 | - `Ret` is the returned value or the structure you want to deserialize. 166 | - `Val` is the value type you want to deserialize from. Currently, only an implementation for `serde_json::Value` is provided 167 | in this crate, but you could add your own! Feel free to look into our `serde_json` module. 168 | - `E` is the error type that should be used if an error happens during the deserialization. 169 | 7. The `Deserr` derive proc macro 170 | 171 | ## Example 172 | 173 | ### Implementing deserialize for a custom type with a custom error 174 | 175 | In the following example, we're going to deserialize a structure containing a bunch of fields and 176 | uses a custom error type that accumulates all the errors encountered while deserializing the structure. 177 | 178 | ```rust 179 | use deserr::{deserialize, DeserializeError, Deserr, ErrorKind, errors::JsonError, Value, ValueKind, IntoValue, take_cf_content, MergeWithError, ValuePointerRef, ValuePointer}; 180 | use serde_json::json; 181 | use std::str::FromStr; 182 | use std::ops::ControlFlow; 183 | use std::fmt; 184 | use std::convert::Infallible; 185 | 186 | /// This is our custom error type. It'll accumulate multiple `JsonError`. 187 | #[derive(Debug)] 188 | struct MyError(Vec); 189 | 190 | impl DeserializeError for MyError { 191 | /// Create a new error with the custom message. 192 | /// 193 | /// Return `ControlFlow::Continue` to continue deserializing even though an error was encountered. 194 | /// We could return `ControlFlow::Break` as well to stop right here. 195 | fn error(self_: Option, error: ErrorKind, location: ValuePointerRef) -> ControlFlow { 196 | /// The `take_cf_content` return the inner error in a `ControlFlow`. 197 | let error = take_cf_content(JsonError::error(None, error, location)); 198 | 199 | let errors = if let Some(MyError(mut errors)) = self_ { 200 | errors.push(error); 201 | errors 202 | } else { 203 | vec![error] 204 | }; 205 | ControlFlow::Continue(MyError(errors)) 206 | } 207 | } 208 | 209 | /// We have to implements `MergeWithError` between our error type _aaand_ our error type. 210 | impl MergeWithError for MyError { 211 | fn merge(self_: Option, mut other: MyError, _merge_location: ValuePointerRef) -> ControlFlow { 212 | if let Some(MyError(mut errors)) = self_ { 213 | other.0.append(&mut errors); 214 | } 215 | ControlFlow::Continue(other) 216 | } 217 | } 218 | 219 | #[derive(Debug, Deserr, PartialEq, Eq)] 220 | #[deserr(deny_unknown_fields)] 221 | struct Search { 222 | #[deserr(default = String::new())] 223 | query: String, 224 | #[deserr(try_from(&String) = FromStr::from_str -> IndexUidError)] 225 | index: IndexUid, 226 | #[deserr(from(String) = From::from)] 227 | field: Wildcard, 228 | #[deserr(default)] 229 | filter: Option, 230 | // Even though this field is an `Option` it IS mandatory. 231 | limit: Option, 232 | #[deserr(default)] 233 | offset: usize, 234 | } 235 | 236 | /// An `IndexUid` can only be composed of ascii characters. 237 | #[derive(Debug, PartialEq, Eq)] 238 | struct IndexUid(String); 239 | /// If we encounter a non-ascii character this is the error type we're going to throw. 240 | struct IndexUidError(char); 241 | 242 | impl FromStr for IndexUid { 243 | type Err = IndexUidError; 244 | 245 | fn from_str(s: &str) -> Result { 246 | if let Some(c) = s.chars().find(|c| !c.is_ascii()) { 247 | Err(IndexUidError(c)) 248 | } else { 249 | Ok(Self(s.to_string())) 250 | } 251 | } 252 | } 253 | 254 | impl fmt::Display for IndexUidError { 255 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 256 | write!( 257 | f, 258 | "Encountered invalid character: `{}`, only ascii characters are accepted in the index", 259 | self.0 260 | ) 261 | } 262 | } 263 | 264 | /// We need to define how the `IndexUidError` error is going to be merged with our 265 | /// custom error type. 266 | impl MergeWithError for MyError { 267 | fn merge(self_: Option, other: IndexUidError, merge_location: ValuePointerRef) -> ControlFlow { 268 | // To be consistent with the other error and automatically get the position of the error we re-use the `JsonError` 269 | // type and simply define ourself as an `Unexpected` error. 270 | let error = take_cf_content(JsonError::error::(None, ErrorKind::Unexpected { msg: other.to_string() }, merge_location)); 271 | let errors = if let Some(MyError(mut errors)) = self_ { 272 | errors.push(error); 273 | errors 274 | } else { 275 | vec![error] 276 | }; 277 | ControlFlow::Continue(MyError(errors)) 278 | } 279 | } 280 | 281 | /// A `Wildcard` can either contains a normal value or be a unit wildcard. 282 | #[derive(Deserr, Debug, PartialEq, Eq)] 283 | #[deserr(from(String) = From::from)] 284 | enum Wildcard { 285 | Wildcard, 286 | Value(String), 287 | } 288 | 289 | impl From for Wildcard { 290 | fn from(s: String) -> Self { 291 | if s == "*" { 292 | Wildcard::Wildcard 293 | } else { 294 | Wildcard::Value(s) 295 | } 296 | } 297 | } 298 | 299 | // Here is an example of a typical payload we could deserialize: 300 | let data = deserialize::( 301 | json!({ "index": "mieli", "field": "doggo", "filter": ["id = 1", ["catto = jorts"]], "limit": null }), 302 | ).unwrap(); 303 | assert_eq!(data, Search { 304 | query: String::new(), 305 | index: IndexUid(String::from("mieli")), 306 | field: Wildcard::Value(String::from("doggo")), 307 | filter: Some(json!(["id = 1", ["catto = jorts"]])), 308 | limit: None, 309 | offset: 0, 310 | }); 311 | 312 | // And here is what happens when everything goes wrong at the same time: 313 | let error = deserialize::( 314 | json!({ "query": 12, "index": "mieli 🍯", "field": true, "offset": "🔢" }), 315 | ).unwrap_err(); 316 | // We're going to stringify all the error so it's easier to read 317 | assert_eq!(error.0.into_iter().map(|error| error.to_string()).collect::>().join("\n"), 318 | "\ 319 | Invalid value type at `.query`: expected a string, but found a positive integer: `12` 320 | Invalid value type at `.offset`: expected a positive integer, but found a string: `\"🔢\"` 321 | Invalid value at `.index`: Encountered invalid character: `🍯`, only ascii characters are accepted in the index 322 | Invalid value type at `.field`: expected a string, but found a boolean: `true` 323 | Missing field `limit`\ 324 | "); 325 | ``` 326 | -------------------------------------------------------------------------------- /tests/number-range-error-messages.rs: -------------------------------------------------------------------------------- 1 | use deserr::errors::JsonError; 2 | use serde_json::json; 3 | use std::num::{ 4 | NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, 5 | NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, 6 | }; 7 | 8 | #[allow(unused)] 9 | #[derive(Debug, deserr::Deserr)] 10 | #[deserr(deny_unknown_fields)] 11 | struct Test { 12 | #[deserr(default)] 13 | u8: u8, 14 | #[deserr(default)] 15 | u16: u16, 16 | #[deserr(default)] 17 | u32: u32, 18 | #[deserr(default)] 19 | u64: u64, 20 | #[deserr(default)] 21 | usize: usize, 22 | #[deserr(default = NonZeroU8::MIN)] 23 | non_zero_u8: NonZeroU8, 24 | #[deserr(default = NonZeroU16::MIN)] 25 | non_zero_u16: NonZeroU16, 26 | #[deserr(default = NonZeroU32::MIN)] 27 | non_zero_u32: NonZeroU32, 28 | #[deserr(default = NonZeroU64::MIN)] 29 | non_zero_u64: NonZeroU64, 30 | #[deserr(default = NonZeroU128::MIN)] 31 | non_zero_u128: NonZeroU128, 32 | #[deserr(default = NonZeroUsize::MIN)] 33 | non_zero_usize: NonZeroUsize, 34 | #[deserr(default = NonZeroI8::MIN)] 35 | non_zero_i8: NonZeroI8, 36 | #[deserr(default = NonZeroI16::MIN)] 37 | non_zero_i16: NonZeroI16, 38 | #[deserr(default = NonZeroI32::MIN)] 39 | non_zero_i32: NonZeroI32, 40 | #[deserr(default = NonZeroI64::MIN)] 41 | non_zero_i64: NonZeroI64, 42 | #[deserr(default = NonZeroI128::MIN)] 43 | non_zero_i128: NonZeroI128, 44 | #[deserr(default = NonZeroIsize::MIN)] 45 | non_zero_isize: NonZeroIsize, 46 | #[deserr(default)] 47 | i8: i8, 48 | #[deserr(default)] 49 | i16: i16, 50 | #[deserr(default)] 51 | i32: i32, 52 | #[deserr(default)] 53 | i64: i64, 54 | #[deserr(default)] 55 | isize: isize, 56 | } 57 | 58 | #[test] 59 | fn positive_integer() { 60 | // ensuring it deserialize correctly over the whole range of number. 61 | for i in u8::MIN..=u8::MAX { 62 | deserr::deserialize::(json!({ "u8": i })).unwrap(); 63 | } 64 | 65 | let ret = 66 | deserr::deserialize::(json!({ "u8": u8::MAX as u16 + 1 })).unwrap_err(); 67 | insta::assert_debug_snapshot!(ret, @r###" 68 | JsonError( 69 | "Invalid value at `.u8`: value: `256` is too large to be deserialized, maximum value authorized is `255`", 70 | ) 71 | "###); 72 | let ret = deserr::deserialize::(json!({ "u8": -1 })).unwrap_err(); 73 | insta::assert_debug_snapshot!(ret, @r###" 74 | JsonError( 75 | "Invalid value type at `.u8`: expected a positive integer, but found a negative integer: `-1`", 76 | ) 77 | "###); 78 | 79 | let ret = deserr::deserialize::(json!({ "u16": u16::MAX as u32 + 1 })) 80 | .unwrap_err(); 81 | insta::assert_debug_snapshot!(ret, @r###" 82 | JsonError( 83 | "Invalid value at `.u16`: value: `65536` is too large to be deserialized, maximum value authorized is `65535`", 84 | ) 85 | "###); 86 | let ret = deserr::deserialize::(json!({ "u16": -1 })).unwrap_err(); 87 | insta::assert_debug_snapshot!(ret, @r###" 88 | JsonError( 89 | "Invalid value type at `.u16`: expected a positive integer, but found a negative integer: `-1`", 90 | ) 91 | "###); 92 | 93 | let ret = deserr::deserialize::(json!({ "u32": u32::MAX as u64 + 1 })) 94 | .unwrap_err(); 95 | insta::assert_debug_snapshot!(ret, @r###" 96 | JsonError( 97 | "Invalid value at `.u32`: value: `4294967296` is too large to be deserialized, maximum value authorized is `4294967295`", 98 | ) 99 | "###); 100 | let ret = deserr::deserialize::(json!({ "u32": -1 })).unwrap_err(); 101 | insta::assert_debug_snapshot!(ret, @r###" 102 | JsonError( 103 | "Invalid value type at `.u32`: expected a positive integer, but found a negative integer: `-1`", 104 | ) 105 | "###); 106 | 107 | let ret = deserr::deserialize::(json!({ "u64": -1 })).unwrap_err(); 108 | insta::assert_debug_snapshot!(ret, @r###" 109 | JsonError( 110 | "Invalid value type at `.u64`: expected a positive integer, but found a negative integer: `-1`", 111 | ) 112 | "###); 113 | 114 | let ret = deserr::deserialize::(json!({ "usize": -1 })).unwrap_err(); 115 | insta::assert_debug_snapshot!(ret, @r###" 116 | JsonError( 117 | "Invalid value type at `.usize`: expected a positive integer, but found a negative integer: `-1`", 118 | ) 119 | "###); 120 | 121 | // we can't test the u64 and usize because we have no way to create a value that overflow since it's `serde_json` that doesn't support u128 yet. 122 | 123 | // let ret = deserr::deserialize::(json!({ "u64": u64::MAX as u128 + 1 })) 124 | // .unwrap_err(); 125 | // insta::assert_debug_snapshot!(ret, @""); 126 | 127 | // let ret = 128 | // deserr::deserialize::(json!({ "usize": -1 })) 129 | // .unwrap_err(); 130 | // insta::assert_debug_snapshot!(ret, @""); 131 | } 132 | 133 | #[test] 134 | fn non_zero_positive_integer() { 135 | // ensuring it deserialize correctly over the whole range of number. 136 | for i in u8::from(NonZeroU8::MIN)..=u8::from(NonZeroU8::MAX) { 137 | deserr::deserialize::(json!({ "non_zero_u8": i })).unwrap(); 138 | } 139 | 140 | let ret = 141 | deserr::deserialize::(json!({ "non_zero_u8": u8::MAX as u16 + 1 })) 142 | .unwrap_err(); 143 | insta::assert_debug_snapshot!(ret, @r###" 144 | JsonError( 145 | "Invalid value at `.non_zero_u8`: value: `256` is too large to be deserialized, maximum value authorized is `255`", 146 | ) 147 | "###); 148 | let ret = deserr::deserialize::(json!({ "non_zero_u8": 0 })).unwrap_err(); 149 | insta::assert_debug_snapshot!(ret, @r###" 150 | JsonError( 151 | "Invalid value at `.non_zero_u8`: a non-zero integer value lower than `255` was expected, but found a zero", 152 | ) 153 | "###); 154 | let ret = deserr::deserialize::(json!({ "non_zero_u8": -1 })).unwrap_err(); 155 | insta::assert_debug_snapshot!(ret, @r###" 156 | JsonError( 157 | "Invalid value type at `.non_zero_u8`: expected a positive integer, but found a negative integer: `-1`", 158 | ) 159 | "###); 160 | 161 | let ret = 162 | deserr::deserialize::(json!({ "non_zero_u16": u16::MAX as u32 + 1 })) 163 | .unwrap_err(); 164 | insta::assert_debug_snapshot!(ret, @r###" 165 | JsonError( 166 | "Invalid value at `.non_zero_u16`: value: `65536` is too large to be deserialized, maximum value authorized is `65535`", 167 | ) 168 | "###); 169 | let ret = deserr::deserialize::(json!({ "non_zero_u16": 0 })).unwrap_err(); 170 | insta::assert_debug_snapshot!(ret, @r###" 171 | JsonError( 172 | "Invalid value at `.non_zero_u16`: a non-zero integer value lower than `65535` was expected, but found a zero", 173 | ) 174 | "###); 175 | let ret = deserr::deserialize::(json!({ "non_zero_u16": -1 })).unwrap_err(); 176 | insta::assert_debug_snapshot!(ret, @r###" 177 | JsonError( 178 | "Invalid value type at `.non_zero_u16`: expected a positive integer, but found a negative integer: `-1`", 179 | ) 180 | "###); 181 | 182 | let ret = 183 | deserr::deserialize::(json!({ "non_zero_u32": u32::MAX as u64 + 1 })) 184 | .unwrap_err(); 185 | insta::assert_debug_snapshot!(ret, @r###" 186 | JsonError( 187 | "Invalid value at `.non_zero_u32`: value: `4294967296` is too large to be deserialized, maximum value authorized is `4294967295`", 188 | ) 189 | "###); 190 | let ret = deserr::deserialize::(json!({ "non_zero_u32": 0 })).unwrap_err(); 191 | insta::assert_debug_snapshot!(ret, @r###" 192 | JsonError( 193 | "Invalid value at `.non_zero_u32`: a non-zero integer value lower than `4294967295` was expected, but found a zero", 194 | ) 195 | "###); 196 | let ret = deserr::deserialize::(json!({ "non_zero_u32": -1 })).unwrap_err(); 197 | insta::assert_debug_snapshot!(ret, @r###" 198 | JsonError( 199 | "Invalid value type at `.non_zero_u32`: expected a positive integer, but found a negative integer: `-1`", 200 | ) 201 | "###); 202 | 203 | let ret = deserr::deserialize::(json!({ "non_zero_u64": 0 })).unwrap_err(); 204 | insta::assert_debug_snapshot!(ret, @r###" 205 | JsonError( 206 | "Invalid value at `.non_zero_u64`: a non-zero integer value lower than `18446744073709551615` was expected, but found a zero", 207 | ) 208 | "###); 209 | 210 | let ret = deserr::deserialize::(json!({ "non_zero_u64": -1 })).unwrap_err(); 211 | insta::assert_debug_snapshot!(ret, @r###" 212 | JsonError( 213 | "Invalid value type at `.non_zero_u64`: expected a positive integer, but found a negative integer: `-1`", 214 | ) 215 | "###); 216 | 217 | let ret = 218 | deserr::deserialize::(json!({ "non_zero_usize": 0 })).unwrap_err(); 219 | insta::assert_debug_snapshot!(ret, @r###" 220 | JsonError( 221 | "Invalid value at `.non_zero_usize`: a non-zero integer value lower than `18446744073709551615` was expected, but found a zero", 222 | ) 223 | "###); 224 | 225 | let ret = 226 | deserr::deserialize::(json!({ "non_zero_usize": -1 })).unwrap_err(); 227 | insta::assert_debug_snapshot!(ret, @r###" 228 | JsonError( 229 | "Invalid value type at `.non_zero_usize`: expected a positive integer, but found a negative integer: `-1`", 230 | ) 231 | "###); 232 | 233 | // we can't test the u64 and usize because we have no way to create a value that overflow since it's `serde_json` that doesn't support u128 yet. 234 | 235 | // let ret = deserr::deserialize::(json!({ "non_zero_u64": u64::MAX as u128 + 1 })) 236 | // .unwrap_err(); 237 | // insta::assert_debug_snapshot!(ret, @""); 238 | 239 | // let ret = 240 | // deserr::deserialize::(json!({ "non_zero_usize": -1 })) 241 | // .unwrap_err(); 242 | // insta::assert_debug_snapshot!(ret, @""); 243 | } 244 | 245 | #[test] 246 | fn negative_integer() { 247 | // ensuring it deserialize correctly over the whole range of number. 248 | for i in i8::MIN..=i8::MAX { 249 | deserr::deserialize::(json!({ "i8": i })).unwrap(); 250 | } 251 | 252 | let ret = 253 | deserr::deserialize::(json!({ "i8": i8::MAX as i16 + 1 })).unwrap_err(); 254 | insta::assert_debug_snapshot!(ret, @r###" 255 | JsonError( 256 | "Invalid value at `.i8`: value: `128` is too large to be deserialized, maximum value authorized is `127`", 257 | ) 258 | "###); 259 | let ret = 260 | deserr::deserialize::(json!({ "i8": i8::MIN as i16 - 1 })).unwrap_err(); 261 | insta::assert_debug_snapshot!(ret, @r###" 262 | JsonError( 263 | "Invalid value at `.i8`: value: `-129` is too small to be deserialized, minimum value authorized is `-128`", 264 | ) 265 | "###); 266 | 267 | let ret = deserr::deserialize::(json!({ "i16": i16::MAX as i32 + 1 })) 268 | .unwrap_err(); 269 | insta::assert_debug_snapshot!(ret, @r###" 270 | JsonError( 271 | "Invalid value at `.i16`: value: `32768` is too large to be deserialized, maximum value authorized is `32767`", 272 | ) 273 | "###); 274 | let ret = deserr::deserialize::(json!({ "i16": i16::MIN as i32 - 1 })) 275 | .unwrap_err(); 276 | insta::assert_debug_snapshot!(ret, @r###" 277 | JsonError( 278 | "Invalid value at `.i16`: value: `-32769` is too small to be deserialized, minimum value authorized is `-32768`", 279 | ) 280 | "###); 281 | 282 | let ret = deserr::deserialize::(json!({ "i32": i32::MAX as i64 + 1 })) 283 | .unwrap_err(); 284 | insta::assert_debug_snapshot!(ret, @r###" 285 | JsonError( 286 | "Invalid value at `.i32`: value: `2147483648` is too large to be deserialized, maximum value authorized is `2147483647`", 287 | ) 288 | "###); 289 | let ret = deserr::deserialize::(json!({ "i32": i32::MIN as i64 - 1 })) 290 | .unwrap_err(); 291 | insta::assert_debug_snapshot!(ret, @r###" 292 | JsonError( 293 | "Invalid value at `.i32`: value: `-2147483649` is too small to be deserialized, minimum value authorized is `-2147483648`", 294 | ) 295 | "###); 296 | 297 | // we can't test the i64 and isize because we have no way to create a value that overflow since it's `serde_json` that doesn't support i128 yet. 298 | } 299 | 300 | #[test] 301 | fn non_zero_negative_integer() { 302 | // ensuring it deserialize correctly over the whole range of negative numbers. 303 | for i in i8::from(NonZeroI8::MIN)..-1 { 304 | deserr::deserialize::(json!({ "non_zero_i8": i })).unwrap(); 305 | } 306 | // ensuring it deserialize correctly over the whole range of positive numbers. 307 | for i in 1..i8::from(NonZeroI8::MAX) { 308 | deserr::deserialize::(json!({ "non_zero_i8": i })).unwrap(); 309 | } 310 | 311 | let ret = 312 | deserr::deserialize::(json!({ "non_zero_i8": i8::MAX as i16 + 1 })) 313 | .unwrap_err(); 314 | insta::assert_debug_snapshot!(ret, @r###" 315 | JsonError( 316 | "Invalid value at `.non_zero_i8`: value: `128` is too large to be deserialized, maximum value authorized is `127`", 317 | ) 318 | "###); 319 | let ret = 320 | deserr::deserialize::(json!({ "non_zero_i8": i8::MIN as i16 - 1 })) 321 | .unwrap_err(); 322 | insta::assert_debug_snapshot!(ret, @r###" 323 | JsonError( 324 | "Invalid value at `.non_zero_i8`: value: `-129` is too small to be deserialized, minimum value authorized is `-128`", 325 | ) 326 | "###); 327 | let ret = deserr::deserialize::(json!({ "non_zero_i8": 0 })).unwrap_err(); 328 | insta::assert_debug_snapshot!(ret, @r###" 329 | JsonError( 330 | "Invalid value at `.non_zero_i8`: a non-zero integer value higher than `-128` was expected, but found a zero", 331 | ) 332 | "###); 333 | 334 | let ret = deserr::deserialize::(json!({ "i16": i16::MAX as i32 + 1 })) 335 | .unwrap_err(); 336 | insta::assert_debug_snapshot!(ret, @r###" 337 | JsonError( 338 | "Invalid value at `.i16`: value: `32768` is too large to be deserialized, maximum value authorized is `32767`", 339 | ) 340 | "###); 341 | let ret = deserr::deserialize::(json!({ "i16": i16::MIN as i32 - 1 })) 342 | .unwrap_err(); 343 | insta::assert_debug_snapshot!(ret, @r###" 344 | JsonError( 345 | "Invalid value at `.i16`: value: `-32769` is too small to be deserialized, minimum value authorized is `-32768`", 346 | ) 347 | "###); 348 | let ret = deserr::deserialize::(json!({ "non_zero_i16": 0 })).unwrap_err(); 349 | insta::assert_debug_snapshot!(ret, @r###" 350 | JsonError( 351 | "Invalid value at `.non_zero_i16`: a non-zero integer value higher than `-32768` was expected, but found a zero", 352 | ) 353 | "###); 354 | 355 | let ret = deserr::deserialize::(json!({ "i32": i32::MAX as i64 + 1 })) 356 | .unwrap_err(); 357 | insta::assert_debug_snapshot!(ret, @r###" 358 | JsonError( 359 | "Invalid value at `.i32`: value: `2147483648` is too large to be deserialized, maximum value authorized is `2147483647`", 360 | ) 361 | "###); 362 | let ret = deserr::deserialize::(json!({ "non_zero_i32": 0 })).unwrap_err(); 363 | insta::assert_debug_snapshot!(ret, @r###" 364 | JsonError( 365 | "Invalid value at `.non_zero_i32`: a non-zero integer value higher than `-2147483648` was expected, but found a zero", 366 | ) 367 | "###); 368 | let ret = deserr::deserialize::(json!({ "i32": i32::MIN as i64 - 1 })) 369 | .unwrap_err(); 370 | insta::assert_debug_snapshot!(ret, @r###" 371 | JsonError( 372 | "Invalid value at `.i32`: value: `-2147483649` is too small to be deserialized, minimum value authorized is `-2147483648`", 373 | ) 374 | "###); 375 | let ret = deserr::deserialize::(json!({ "non_zero_i64": 0 })).unwrap_err(); 376 | insta::assert_debug_snapshot!(ret, @r###" 377 | JsonError( 378 | "Invalid value at `.non_zero_i64`: a non-zero integer value higher than `-9223372036854775808` was expected, but found a zero", 379 | ) 380 | "###); 381 | let ret = 382 | deserr::deserialize::(json!({ "non_zero_isize": 0 })).unwrap_err(); 383 | insta::assert_debug_snapshot!(ret, @r###" 384 | JsonError( 385 | "Invalid value at `.non_zero_isize`: a non-zero integer value higher than `-9223372036854775808` was expected, but found a zero", 386 | ) 387 | "###); 388 | } 389 | --------------------------------------------------------------------------------