-
Notifications
You must be signed in to change notification settings - Fork 224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
custom error messages #568
Comments
Hey @jota can you explain your use case fully? Eg. what the specific struct is used to validate, where these error messages are going, etc. (with code samples) |
Hi Ian, sure. Basically I would like to override the generated error message. Let's say I have an object with a "date" field where I validate the format YYYY-MM-DD e.g. 2020-12-01. I would like to change the error message to something else like "date should match format YYYY-MM-DD" because i would like to directly show those messages in a user interface. I have created a jsfiddle here I'm currently using yup, where I can use the following: Does that make sense to you? |
My general recommendation is that I wouldn't necessarily show these error messages directly to a user, because Superstruct provides no guarantee that they are user-facing. Depending on the structure of your validation they could be very confusing. I don't think I want to add a That said, I'd be open to a The other solution for you is to define a custom type of your own, especially if you are planning to re-use those date strings in other places in your app, for example: import { define } from 'superstruct'
const datestring = () => define('datestring', value => ... || 'My custom message.') |
Your second suggestion pointed me in the right direction. Now i'm using: const datestring = define('datestring', (value, context) => {
if (/^\d\d\d\d-\d\d-\d\d$/.test(String(value))) {
return [];
} else {
return {
path: [],
message: `${context.path} must have format YYYY-MM-DD`,
};
}
}); and the error message is "date must have format YYYY-MM-DD". That works for me, thank you for taking the time! |
@ianstormtaylor Just curious... are you saying that we could create a utility function ( This library is by far the best, the only thing I'm needing is user facing error messages. |
@richarddavenport totally agree with u, we realy need this feature |
Hey @richarddavenport can you give me an idea for what your use case is for user-facing error messages? There are a few different ways I might recommend doing it. |
@ianstormtaylor thanks for being open. Let me through together some notes/code. |
@ianstormtaylor hi, i've created a new PR with custom errors and i think in this way all of us wanted see custom errors in others functions |
@EvgenBabenko I think the goal of this issue is to be able to see User facing error messages. For example if a form input is required (like name) then you would validate with superstruct and then if you need to show the user a message you could do it with a custom message. Example: const User = object({
name: string(),
});
const data = {
name: null,
};
assert(data, User);
// produces:
{
"message": "At path: name -- Expected a string, but received: null",
"value": null,
"type": "string",
"path": [
"name"
],
"branch": [
{
"name": null
},
null
]
} Given the previous I would not show the user a toast notification displaying "At path: name -- Expected a string, but received: null". Superstruct is very useful for the developer but not the user... I would rather show the user "Name is required" |
@richarddavenport so, we have different approaches for show messages to users, cuz we're using react-hook-form and we're trying connect react-hook-form with superstruct instead Yup |
@EvgenBabenko I agree with what you're saying. Nothing I've said goes against what you've said. We're actually discussing achieving the same result. It's how we get there is in question. PR #611 (which has some typos FYI) is only good for creating required structs. What I'd like to see is the ability to return a particular message given a certain error, so not just required. Here's another example: const User = object({
name: size(string(), 1, 15),
}); Requiredassert({ name: null }, User);
// produces message: "At path: name -- Expected a string, but received: null"
Too Shortassert({ name: '' }, User);
// "At path: name -- Expected a string with a length between `1` and `10` but received one with a length of `0`"
Too Longassert({ name: 'Very Long Name That is Too Long' }, User);
//"At path: name -- Expected a string with a length between `1` and `10` but received one with a length of `31`",
What I am trying achieve is the ability to show different messages based on the output of the validation. Given your currently solution, how would I show multiple messages for the following? I need one message for missing, one for too short, and one for too long. const User = object({
name: size(string(), 1, 15),
}); |
@richarddavenport thanks for find some typos, i haven't noticed, now i see I think, your expected result is impossible, cuz in your result need customize each border (don't forget about internalization) or in your case maybe need write more custom structs: and my PR just showing, that in every function we're expecting |
@EvgenBabenko Yes, I was hoping you'd come to the same conclusion I would. With your solution it is not possible. That's why I want to propose is different than what you are proposing currently. I need to give it more thought, but basically I don't think we should couple the user facing messages with the superstruct messages. This is what @ianstormtaylor is suggesting in comments previously.
What we're trying to get to is a composable approach. |
I also use react-hook-form. I want to output an error message using a different format for each Input. For example, "size" outputs the same error string as an array and a string, but it needs to be changed depending on the context. I also want to use the i18n library. I think that it can be solved by outputting all the error information. For example, there are two types of errors that // This code doesn't work
interface StringValidationError {
class: "invalidType";
expect: "string";
actually: string;
}
interface SizeValidationError {
class: "invalidSize";
min: number;
max: number;
actually: number;
}
const stringStruct: Struct<string, null, StringValidationError> = string();
const sizeStruct: Struct<string, null, SizeValidationError | StringValidationError> = size(string(), 1, 10); The user can format the error message according to the nature of the application and i18n requirements. I think this can be implemented by extending the current StructError. |
I'm working on a POC here. It is considered to break compatibility. https://github.com/ianstormtaylor/superstruct/compare/main...kamijin-fanta:typed-error?expand=1 @ianstormtaylor Do you have any plans to type the error? |
Hey @kamijin-fanta, I'm open to adding types to the errors. The only two issues I see with that approach currently are (a) that it doesn't appear to apply to places where the error is thrown which makes it less useful, and (b) that it eliminates the ease of Are there ways to solve those issues while still typing the errors? |
Thank you! I was thinking the same. I think it can be solved by adding the expected type name to the define and refine functions and the logic that accepts boolean, string and undefined. It may be possible to create new functions like I have no good idea about Throw. Struct that may throw an error may be solved by using for example interface ThrowErrorDetail extends ErrorDetail {
class:'throw';
error: any;
} |
Utility-function based approach that worked for us: Function: const message = <T>(struct: Struct<T, any>, message: string): Struct<T, any> =>
define('message', (value) => (is(value, struct) ? true : message)); Usage: fieldA: message(string(), 'required'), When validating with react-hook-form resolver we get this validation error: {message: "required", type: "message"} We then pass the |
Any chance |
I don't think this issue should be closed. What do you think @ianstormtaylor ? I tried @orhels solution as it seems like a good approach. CodeSandbox: It includes:
Hopefully the example is useful to someone. |
I'm open to the idea of a And I also still would love the approach @kamijin-fanta mentioned and started implementing in #634. |
It would be nice to be able to define an optional general assertion message that would augment the superstuct's validation message. Like: import { assert } from "superstruct";
assert(user, UserInfo, "Failed to parse user!");
// or
const valid = create(user, UserInfo, "Failed to parse user!"); So that in logs, when you see a random validation error, you can tell that it was user related. I'm currently using a wrapper around superstruct's functions which isn't very nice. I think this in the kind of feature that should be handled by the library itself. @ianstormtaylor, let me know if you'd be open to a PR for this. My Workaroundimport {
Struct,
assert as test,
create as parse,
StructError,
} from "superstruct";
export function assert<T, S>(
value: unknown,
struct: Struct<T, S>,
message = ""
): asserts value is T {
try {
test(value, struct);
} catch (error) {
if (error instanceof StructError && message) {
error.message = message + "\n" + error.message;
}
throw error;
}
}
export function create<T, S>(
value: unknown,
struct: Struct<T, S>,
message = ""
) {
try {
return parse(value, struct);
} catch (error) {
if (error instanceof StructError && message) {
error.message = message + "\n" + error.message;
}
throw error;
}
} |
@Azarattum that's a nice idea. I'd be open to a PR. I think if the message is provided, it should override the error message completely, instead of concatenating them. But not sure if that would make the feature useless to you, in which case maybe it's better left to userland. |
@ianstormtaylor I think we can put the original message into Error.cause which is a standard web API and seems like a perfect place for it. So, developers can access it if needed. Now the question is, which APIs should we augment? |
@Azarattum wow learned something new about |
Done in #1141. @ianstormtaylor, could you take a look? |
I am seriously considering porting some They provide a resolver that works quite well, besides the for-robots error messages: |
@ianstormtaylor thoughts on the last comment above? |
@MinervaBot and anyone else interested, I had a crude go at making a resolver for react-hook-form with custom error messages. Here's my first pass. Can certainly be improved. It's basically what the resolver library already provides, with the addition of an error message display map. import { FieldErrors, FieldValues, Resolver } from 'react-hook-form'
import * as s from 'superstruct'
const safelyValidateSchema = <TSchema>(input: unknown, schema: s.Struct<TSchema>) => {
const result: ValidationResult<TSchema> = { data: undefined, error: null }
try {
result.data = s.create(input, schema)
} catch (e) {
if (e instanceof s.StructError) {
result.error = e
}
result.data = input as TSchema
}
return result
}
export const resolverFactory =
<TSchema extends FieldValues>(schema: s.Struct<TSchema>, options: ResolverOptions<TSchema>): Resolver<TSchema> =>
(input) => {
const { data, error } = safelyValidateSchema(input, schema)
if (!error && data) {
return {
values: data,
errors: {},
}
}
if (!error && !data) {
console.error('No error and no data, this should not be happening!')
return {
values: {},
errors: {},
}
}
if (error) {
const errors: Record<string, s.Failure> = {}
for (const failure of error.failures()) {
const customErrorMessage = options.errorMap[failure.key as keyof ListingsParams]
if (customErrorMessage) errors[failure.key] = { ...failure, message: customErrorMessage }
else errors[failure.key] = failure
}
return {
values: {},
errors: errors as unknown as FieldErrors<TSchema>,
}
}
throw new Error('Bad news bears')
}
type ValidationResult<TSchema> = {
data: TSchema | undefined;
error: s.StructError | null;
}
type ResolverOptions<TSchema> = {
errorMap: Partial<Record<keyof TSchema, string>>
} Usage const resolver = resolverFactory(Schema, { errorMap: { maxDaysOnSite: 'Oopsie' } })
useForm({ resolver }) |
Anyone found a solution for react-hook-form on this one? i'd like to custom the error msg based on locale but also the error so the same key/input can have different error msg depending on the error. not sure how to do that with that i've seen above. the current solution i see offered is to make custom validation type for every input and msg we have :/ on an basic input let's say i want to be able to say something like:
not sure i found in the doc how to do that :/ |
@wcastand The approach I experimented with was to copy the superstruct resolver code in It seems daunting to need to maintain/replace the provided resolver, but actually the code is VERY short and never changes. This is actually a pretty clean solution and there is a certain charm in pushing the presentation display code into the display layer, but I did end up sticking with Zod. |
Hey there,
great work with superstruct. I was especially happy to see that it does not bloat the browser bundle like other validation libs do.
One thing is holding me back to switch to superstruct though,
I could not find an easy way to provide a custom error message.
I was looking for something as easy as
pattern(string(), /^\d\d\d\d-\d\d-\d\d$/, 'custom error message, the string should be in date format YYYY-MM-DD')
or
message(pattern(...), 'custom error message, the string should be in date format YYYY-MM-DD')
The default error message provides the regex used which is not something I would like to bother a user with and there are some other use cases like localization.
Is there a way to do that already?
The text was updated successfully, but these errors were encountered: