Deserr
Introduction
Deserr is a crate for deserializing data, with the ability to return custom, type-specific errors upon failure. It was also designed with user-facing APIs in mind and thus provides better defaults than serde for this use case.
Unlike serde, deserr does not parse the data in its serialization format itself but offloads the work to other crates. Instead, it deserializes the already-parsed serialized data into the final type. For example:
// bytes of the serialized value
let s: &str = ".." ;
// parse serialized data using another crate, such as `serde_json`
let json: Value = from_str.unwrap;
// finally deserialize with deserr
let data = deserialize_from_value.unwrap;
// `T` must implement `Deserr`.
Why would I use it
The main place where you should use deserr is on your user-facing API, especially if it's supposed to be read by a human. Since deserr gives you full control over your error types, you can improve the quality of your error messages. Here is a little preview of what you can do with deserr:
Let's say I sent this payload to update my Meilisearch settings:
With serde
With serde, we don't have much customization; this is the typical kind of message we would get in return:
The message
Json deserialize error: invalid value: integer
1000, expected u8 at line 6 column 21
- The message uses the word
u8, which definitely won't help a user who doesn't know rust or is unfamiliar with types. - The location is provided in terms of lines and columns. While this is generally good, when most of your users read this message in their terminal, it doesn't actually help much.
The rest of the payload
Since serde returned this error, we cannot know what happened or on which field it happened. Thus, the best we
can do is generate a code bad_request that is common for our whole API. We then use this code to generate
a link to our documentation to help our users. But such a generic link does not help our users because it
can be thrown by every single route of Meilisearch.
With deserr
The message
Invalid value at
.typoTolerance.minWordSizeForTypos.oneTypo: value:1000is too large to be deserialized, maximum value authorized is255
- We get a more human-readable location;
.typoTolerance.minWordSizeForTypos.oneTypo. It gives us the faulty field. - We also get a non-confusing and helpful message this time; it explicitly tells us that the maximum value authorized is
255.
The rest of the payload
Since deserr called one of our functions in the process, we were able to use a custom error code + link to redirect our user to the documentation specific to this feature and this field.
More possibilities with deserr that were impossible with serde
Adding constraints on multiples fields
In Meilisearch, there is another constraint on this minWordSizeForTypos, the twoTypo field must be greater than
the oneType field.
Serde doesn't provide any feature to do that. You could write your own implementation of Deserialize for the
entire sub-object minWordSizeForTypos, but that's generally hard and wouldn't even let you customize the
error type.
Thus, that's the kind of thing you're going to check by hand in your code later on. This is error-prone and
may bring inconsistencies between most of the deserialization error messages and your error message.
With deserr, we provide attributes that allow you to validate your structure once it's deserialized.
When a field is missing
It's possible to provide your own function when a field is missing.
At Meilisearch, we use this function to specify a custom error code, but we keep the default error message which is pretty accurate.
When an unknown field is encountered
It's possible to provide your own function when a field is missing.
Here is a few ideas we have or would like to implement at Meilisearch;
- In the case of a resource you can
PUTwith some fields, but can'tPATCHall its fields. We can throw a specialimmutable field xerror instead of anunknown field x. - Detecting when you use the field name of an alternative; for example, we use
qto make aquerywhile some Meilisearch alternatives usequery. We could help our users with adid you mean?message that corrects the field to its proper name in Meilisearch. - Trying to guess what the user was trying to say by computing the levenstein distance
between what the user typed and what is accepted to provide a
did you mean?message that attempts to correct typos.
When multiple errors are encountered
Deserr lets you accumulate multiple errors with its MergeWithError trait while trying to deserialize the value into your type.
This is a good way to improve your user experience by reducing the number of interactions
a user needs to have to fix an invalid payload.
The main parts of deserr are:
Deserr<E>is the main trait for deserialization, unlike Serde, it's very easy to deserialize this trait manually, see theimplements_deserr_manually.rsfile in our examples directory.IntoValueandValuedescribes the shape that the parsed serialized data must haveDeserializeErroris the trait that all deserialization errors must conform toMergeWithError<E>describe how to combine multiple errors together. It allows deserr to return multiple deserialization errors at once.ValuePointerRefandValuePointerpoint to locations within the value. They are used to locate the origin of an error.deserialize<Ret, Val, E>is the main function to use to deserialize a value.Retis the returned value or the structure you want to deserialize.Valis the value type you want to deserialize from. Currently, only an implementation forserde_json::Valueis provided in this crate, but you could add your own! Feel free to look into ourserde_jsonmodule.Eis the error type that should be used if an error happens during the deserialization.
- The
Deserrderive proc macro
Example
Implementing deserialize for a custom type with a custom error
In the following example, we're going to deserialize a structure containing a bunch of fields and uses a custom error type that accumulates all the errors encountered while deserializing the structure.
use ;
use json;
use FromStr;
use ControlFlow;
use fmt;
use Infallible;
/// This is our custom error type. It'll accumulate multiple `JsonError`.
;
/// We have to implements `MergeWithError` between our error type _aaand_ our error type.
/// An `IndexUid` can only be composed of ascii characters.
;
/// If we encounter a non-ascii character this is the error type we're going to throw.
;
/// We need to define how the `IndexUidError` error is going to be merged with our
/// custom error type.
/// A `Wildcard` can either contains a normal value or be a unit wildcard.
// Here is an example of a typical payload we could deserialize:
let data = .unwrap;
assert_eq!;
// And here is what happens when everything goes wrong at the same time:
let error = .unwrap_err;
// We're going to stringify all the error so it's easier to read
assert_eq!;
Supported features
rename_all
Rename all the fields of the struct according to the given case convention.
The possible values are lowercase, camelCase.
If you need more case conventions, please open an issue; adding more is trivial.
use ;
use json;
let data =
.unwrap;
assert_eq!;
deny_unknown_fields
Throw an error when encountering unknown fields. When this attribute is absent, unknown fields are ignored by default.
use ;
use json;
let err =
.unwrap_err;
assert_eq!;
It is also possible to provide a custom function to handle the error.
use ;
use Infallible;
use json;
let err =
.unwrap_err;
assert_eq!;
let err =
.unwrap_err;
assert_eq!;
tag
Externally tag an enum. 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. For complete unit enums, deserr can deserialize their value from a string, though.
use ;
use json;
let data =
.unwrap;
assert_eq!;
from
Deserializing a type from a function instead of a Value.
You need to provide the following information;
- The input type of the function (here
&String) - The path of the function (here, we're simply using the std
FromStrimplementation)
deserr will first try to deserialize the given type using its Deserr<E> implementation.
That means the input type of the from can be complex. Then deserr will call your
function.
See also try_from if your function can fail.
It can be used as a container attribute
use ;
use json;
let data =
.unwrap;
assert_eq!;
let data =
.unwrap;
assert_eq!;
Or as a field attribute
use ;
use json;
let data =
.unwrap;
assert_eq!;
let data =
.unwrap;
assert_eq!;
try_from
Try deserializing a type from a function instead of a Value.
You need to provide the following information;
- The input type of the function (here
&String) - The path of the function (here, we're simply using the std
FromStrimplementation) - The error type that this function can return (here
Infallible)
deserr will first try to deserialize the given type using its Deserr<E> implementation.
That means the input type of the try_from can be complex. Then deserr will call your
function and accumulate the specified error against the error type of the caller.
It can be used as a container attribute
use ;
use json;
use FromStr;
use fmt;
// Notice how the `try_from` allows us to leverage the deserr limitation on tuple struct.
;
;
let data =
.unwrap;
assert_eq!;
let error =
.unwrap_err;
assert_eq!;
Or as a field attribute
use ;
use json;
use Infallible;
use FromStr;
use ParseIntError;
let data =
.unwrap;
assert_eq!;
let error =
.unwrap_err;
assert_eq!;
validate
Validate a structure after it has been deserialized. This is typically useful when your validation logic needs to take multiple fields into account.
use ;
use json;
use Infallible;
// `__Deserr_E` represents the Error returned by the generated `Deserr` implementation.
let data =
.unwrap;
assert_eq!;
let error =
.unwrap_err;
assert_eq!;
default
Allows you to specify a default value for a field.
Note that, unlike serde, by default, Option doesn't automatically use this attribute.
Here you need to explicitly define whether your type can get a default value.
This makes it less error-prone and easier to make an optional field mandatory.
use ;
use json;
let data =
.unwrap;
assert_eq!;
let data =
.unwrap;
assert_eq!;
skip
Allows you to skip the deserialization of a field.
It won't show up in the list of fields generated by deny_unknown_fields or in the
UnknownKey variant of the ErrorKind type.
use ;
use json;
let data =
.unwrap;
assert_eq!;
// if you try to specify the field, it is ignored
let data =
.unwrap;
assert_eq!;
// Here, we're going to see how skip interacts with `deny_unknown_fields`
let error =
.unwrap_err;
// NOTE: `hidden` isn't in the list of expected fields + `hidden` is effectively considered as a non-existing field.
assert_eq!;
map
Map a field after it has been deserialized.
use ;
use json;
let data =
.unwrap;
assert_eq!;
// Let's see how `map` interacts with the `default` attributes.
let data =
.unwrap;
// As we can see, the `map` attribute is applied AFTER the `default`.
assert_eq!;
missing_field_error
Gives you the opportunity to customize the error message if this specific field is missing.
use ;
use json;
use Infallible;
let error =
.unwrap_err;
assert_eq!;
error
Customize the error type that can be returned when deserializing this structure instead of keeping it generic.
use ;
use json;
// As we can see, rust is able to infer the error type.
let data =
.unwrap;
assert_eq!;
It can also be used as a field attribute;
use ;
use json;
// Since the error returned by the `Search` structure needs to implements `MergeWithError<JsonError>`
// we also need to specify the `error` attribute as a `JsonError`. But as you will see later there are
// other solutions.
where_predicate
Let you add where clauses to the Deserr implementation that deserr will generate.
use ;
use json;
// Here we can constraint the generic `__Deserr_E` type used by deserr to implements `MergeWithError`.
// Now instead of constraining the final error type it stays generic if it's able to accumulate with
// with a `JsonError`.
needs_predicate
Automatically adds where_predicate = FieldType: Deserr<ErrType> for each field with this attribute.
use ;
use json;
Is strictly equivalent to the following:
use ;
use json;
// `__Deserr_E` represents the Error returned by the generated `Deserr` implementation.
Comparison with serde
Since deserr needs to first deserialize the payload into a generic Value that allocates
a lot of memory before creating your structure, it's a lot slower than serde.
For example, at Meilisearch for our search route, in case of a valid payload, we observed a 400% slowdown (4 times slower). That made our search request deserialize in 2µs instead of 500ns. This is fast enough for most use cases but could be an issue if most of your time is spent deserializing.
Datastructure support
| datastructure | serde | deserr | note |
|---|---|---|---|
| Struct | yes | yes | |
| Tuple struct | yes | no | |
| Untagged Enum | yes | no | |
| Untagged unit Enum | yes | yes | |
| Tagged Enum | yes | yes |
Container attributes
| features | serde | deserr | note |
|---|---|---|---|
| rename | yes | no | |
| rename_all | yes | yes | |
| deny_unknown_fields | yes | yes | With deserr you can call a custom function when an unknown field is encountered |
| tag | yes | yes | |
| tag+content | yes | no | |
| untagged | yes | no | it's only supported for unit enums |
| bound | yes | no | Can be emulated with where_predicate |
| default | yes | no | |
| remote | yes | no | |
| transparent | yes | no | |
| from | yes | yes | |
| try_from | yes | yes | |
| into | yes | no | |
| crate | yes | no | |
| validate | no | yes | Allows you to validate the content of struct after it has been deserialized |
| error | no | yes | Specify the error type that should be used while deserializing this structure |
| where_predicate | no | yes | Let you add where clauses to the generated Deserr implementation |
Field attributes
| features | serde | deserr | note |
|---|---|---|---|
| rename | yes | no | |
| alias | yes | no | |
| default | yes | yes | |
| flatten | yes | no | serde doesn't support flattening + denying unknown field |
| skip | yes | yes | |
| deserialize_with | yes | no | But it's kinda emulated with from and try_from |
| with | yes | no | |
| borrow | yes | no | deserr does not support types with references |
| bound | yes | no | |
| map | no | yes | Allows you to map the value after it was deserialized |
| from | no | yes | Deserialize this field from an infallible function |
| try_from | no | yes | Deserialize this field from a fallible function |
| missing_field_error | no | yes | Allows you to return a custom error if this field is missing |
| error | no | yes | Specify the error type that should be used while deserializing this field |
Feature flags
serde-json
Import serde_json and provide;
- An implementation of
deserr::IntoValueforserde_json::Valuewhich make it easy to use both crate together. - A default implementation of the
JsonErrortype that provide the best generic error messages possible.
serde-cs
Import serde-cs and provide;
- An implementation of
Deserrforserde_cs::CS<R>.
actix-web
Import actix-web and futures and provide;
- An implementation of a json actix-web extractor if used with the
serde-jsonfeature. - An implementation of
ResponseErrorfor theJsonErrortype if used with theserde-jsonfeature.
FAQ
But why?
At Meilisearch, we wanted to customize the error code we return when we fail the deserialization of a specific field. Some error messages were also not clear at all and impossible to edit.
What about the maintenance?
At Meilisearch we're already using deserr in production; thus, it's well maintained.
Where can I see more examples of usage of this crate?
Currently, you can read our examples in the examples directory of this repository.
You can also look at our integration test; each attribute has a simple-to-read test.
And obviously, you can read the code of Meilisearch where deserr is used on all our routes.
My question is not listed
Please, if you think there is a bug in this lib or would like a new feature, open an issue or a discussion. If you would like to chat more directly with us, you can join us on discord at https://discord.com/invite/meilisearch