Custom error messages and localization
A rule for custom error messages:
#[derive(Validate)]
struct Test {
#[garde(required, message="custom message which is actually a format string: {}")]
a: String,
#[garde(required, message=custom_message_format)]
b: String,
#[garde(required, message=|_: &str, value: &str, _: &()| {
Ok(format!("custom message in closure: {v}"))
})]
c: String,
}
fn custom_message_format(field_name: &str, value: &str, context: &()) -> Result<Cow<'static, str>, Error> {
Ok(format!("custom message: {value}"))
}
Implementation detail: The string version should differentiate between a const string and a format string. It might make sense to generate a temporary function for format strings which does the actual formatting to enforce encapsulation.
Combined with a custom Context, it should be possible to use this to localize error messages:
struct Locale {
/*
- current locale
- mapping of (unlocalized -> localized) strings
*/
}
#[derive(Validate)]
#[garde(context(Locale))]
struct User {
#[garde(length(min=10,max=100), message=|_, v: &str, ctx: &Locale| {
translate!(ctx, "invalid value {v}")
})]
value: String,
}
But it might make sense to support this directly via a translate rule which passes the error message to be further localized - needs some design work.
There are at least two issues which I believe to be pre-requisites:
- https://github.com/jprochazk/garde/issues/1
- https://github.com/jprochazk/garde/issues/64
(1) because message should act as a switch that turns on fail-fast validation for that specific field, so that we don't waste resources constructing errors that will be discarded.
(2) because the current error representation is somewhat inflexible, and adding a custom message would mean emitting Simple(vec![Error::new("message")]) for a field with a custom message. That's a vec allocation just for one error 😢. With a flat representation (described in the issue), this would be cheap.
These two issues would require pretty significant changes in both garde_derive and garde.
I think this and https://github.com/jprochazk/garde/issues/1 can be combined into a single design. There are two use cases for "custom messages":
- Append the custom message as an extra error if any validation fails.
- Replace all errors with just the custom message if any validation fails.
(1) would address https://github.com/jprochazk/garde/issues/63, because you could use this to output the value together with the other errors.
(2) could address https://github.com/jprochazk/garde/issues/1. We would special case the output of the proc macro to not actually produce any errors in this case, and immediately stop all further validation when any of the rules fail. This is fail-fast validation. We could then support using the custom message attribute on both field and struct levels. By using a "Replace" custom message on the struct level, you would have fail-fast validation for the entire struct.
This would require reworking all the rules to support two modes:
- Check only
- Check and emit error
So that we can fully skip producing an error in case we don't need it.
The proc macro will also need to support emitting the field-level validation rules in both fail-fast and error aggregate modes.
One limitation is that this would all be encoded in the derived Validate implementation, there would be no way to "switch" between the two for a single type. This means if you wanted to do "fail-fast" validation, and then fall back to full validation with error aggregation, you would need two types. I think that's fine, because you can just generate the two from a single type using a declarative macro.
i would love to have this feature. Right now we ended up using custom validator to make custom error message. If I want to help is there's anything that I could do?
is there's anything that I could do?
I think adding a comment here with a code snippet showing how you'd use this feature would be super helpful!
Depending on what exactly you need out of this feature, there may be a simpler "MVP" of https://github.com/jprochazk/garde/issues/7#issuecomment-1704289255 that would not be too difficult to implement. It would consist of:
- Adding a
messagerule to accept input as described in the original post. (It could start with only accepting a format string) - Emitting the message into the error report if any other validation on the field failed.
I'm planning to migrate from validator but I'm missing the code and params on the Error struct.
I'd like to apply i18n on the client side, based on pre-defined error codes that maps to messages with interpolated parameters, like max and min.
For example this is how I'm returning error codes to clients: https://github.com/lasantosr/error-info
I want to avoid adding params. Correct me if I'm wrong, but for your use case, wouldn't code alone be enough? You should be able to retrieve the internationalized error message template using code as the key, and then interpolate the value retrieved from the field on the client.
As for adding support for code, I opened an issue to track it here:
- https://github.com/jprochazk/garde/issues/105
@lasantosr I have it like this, with code, message and path:
impl From<garde::Report> for ApiError {
/**
Recursively generate a vector of "ApiErrorBase"s
and fill it with dot-separated "path"es and error messages
*/
fn from(e: garde::Report) -> Self {
e.iter()
.map(|(key, error)| ApiErrorBase::new_with_path(&format!("{key}.invalid"), &error.message().to_string().to_sentence_case(), &format!("{key}")))
.collect_vec()
.into()
}
}
And the method is like so:
#[must_use]
pub fn new_with_path(code: &str, message: &str, path: &str) -> Self {
Self {
code: code.to_string(),
message: message.to_string(),
path: Some(path.to_string()),
params: None,
}
}
I hope it satisfies your need.
I want to avoid adding
params. Correct me if I'm wrong, but for your use case, wouldn'tcodealone be enough?
That's a first step, the problem is if the i18n message associated to the code is something like: "The value must not exceed {max}"
The params are needed in those cases, but I agree that's an overhead not required for every use case, so it can be an opt-in parameter on the struct level macro.
@lasantosr I have it like this, with code, message and path
A field might have different validation rules associated, so I would like to have a different code for each. Instead of a generic "username.invalid" I'd like to have "username.invalid_length" and "username.invalid_characters" for example.
A field might have different validation rules associated, so I would like to have a different code for each. Instead of a generic "username.invalid" I'd like to have "username.invalid_length" and "username.invalid_characters" for example.
So it can be two extra properties in the Error. Since the checks are defined, they can have static values. So for example this code will be something like this by default:
Err(Error::new("Length is lower than {}", vec![min.to_string()], "min_length"))
or
Err(Error::new("Length is lower than {min}", HashMap::from([("min", min.to_string())]), "min_length"))
Which can be customized via attributes.
That's a first step, the problem is if the i18n message associated to the code is something like: "The value must not exceed {max}"
The params are needed in those cases, but I agree that's an overhead not required for every use case, so it can be an opt-in parameter on the struct level macro
This should be addressed by:
- https://github.com/jprochazk/garde/issues/3
Totally missed that one! That would be enough, along with this message customization and the custom error codes
See also this comment https://github.com/jprochazk/garde/issues/3#issuecomment-2643258698 which I made before I found this issue.
I'll copy my proposal from that here:
struct MyForm {
#[garde(length(min = 1, key = "form-field-error-length-too-short"))]
value1: String,
#[garde(length(max = 10, key = "form-field-error-length-too-long"))]
value2: String,
#[garde(length(min = 1, max = 10, key = "form-field-error-length-incorrect"))]
value3: String,
}
example translation file (fluent-rs):
form-field-error-length-too-short = Must be longer than { $min } characters.
form-field-error-length-too-long = Must be shorter than { $max } characters.
form-field-error-length-incorrect = Must be between { $min } and { $max } characters.
I don't know this crate, or egui_form well enough to work out how one could extract the args (min/max) and any key from the rules to add to some report object, but it seems like it should be do-able as per https://github.com/jprochazk/garde/issues/7#issuecomment-2027969156