Open jota opened 3 years ago
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.
Validation: date: pattern(string(), /^\d\d\d\d-\d\d-\d\d$/),
Input: 2020-12-XX
Error message: 'At path: date -- Expected a string matching /^\\d\\d\\d\\d-\\d\\d-\\d\\d$/
but received \"2020-12-XX\"'
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: date: yup.string().required().matches(/^\d\d\d\d-\d\d-\d\d$/, 'date must have format YYYY-MM-DD'),
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 message
argument to every struct because it gets tedious, but also because it makes it hard to compose them if the messages themselves assume things about where the structs are used.
That said, I'd be open to a message(..., string)
utility function if you were down to pull request one.
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!
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
message
argument to every struct because it gets tedious, but also because it makes it hard to compose them if the messages themselves assume things about where the structs are used.That said, I'd be open to a
message(..., string)
utility function if you were down to pull request one.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.')
@ianstormtaylor Just curious... are you saying that we could create a utility function (message()
) that would take in an error struct/something else(?) and it would spit out user facing error messages?
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 with TS create custom structs looks like i just override all library with this tons of generics and types
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 I've prepared codesanbox example. I've created required struct, but it doesn't matter, look on second parameter "message", without it we show defaulted message, i think about this feature by default told @jota, because as i said before, with typescript creating structs with custom errors, lool like "I just override all library in my app"
@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),
});
assert({ name: null }, User);
// produces message: "At path: name -- Expected a string, but received: null"
assert({ name: '' }, User);
// "At path: name -- Expected a string with a length between `1` and `10` but received one with a length of `0`"
assert({ 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)
Maybe solution will be smth like this:
const User = object({ name: size(required(string("Name should be string"), "Name is required"), 1, 15, "Name should be between 1 and 15 letters"), });
(i don't know how to more customize this code, like your, mine looks terrible)
or in your case maybe need write more custom structs:
const User = object({ name: max(min(required(string("Name should be string"), "Name is required"), 1, "Name is too short"), 15, "Name is too long"), });
where required/min/max is custom structs
and my PR just showing, that in every function we're expecting message
argument @ianstormtaylor
@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.
I don't think I want to add a message argument to every struct because it gets tedious, but also because it makes it hard to compose them if the messages themselves assume things about where the structs are used.
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 size(string(), 1, 10)
outputs. Perhaps this is type-safely expressed as follows.
// 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 return true/false
or return 'message'
which makes it harder to define custom structs quickly.
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 defineType(name: string, expect: string, validator: (value, ctx) => boolean | undefined): Struct<T, S, TypeErrorDetail>
.
I have no good idea about Throw. Struct that may throw an error may be solved by using for example TypeErrorDetail | ThrowErrorDetail
.
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 message
to our i18n function to give validation messages to the user. I'm guessing that this function is not a general solution, but it would be great to have something like this in the lib.
Any chance message
can be a part of standard API?
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: https://codesandbox.io/s/react-superstruct-hook-form-custom-errors-translated-83jx90?file=/src/contact-form.tsx (CodeSandbox is being a bit weird sometimes with the dependencies. Might need to refresh a couple times for them to work.)
It includes:
message
utilityHopefully the example is useful to someone.
I'm open to the idea of a message
utility. Although I'd like to see it implemented where it doesn't end up wrapping and creating a new error, but instead just augments the existing error by changing it's message. If someone wants to PR that I'd be open to it!
And I also still would love the approach @kamijin-fanta mentioned and started implementing in https://github.com/ianstormtaylor/superstruct/pull/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.
import {
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? assert
and create
are the obvious ones. But, what do you think about validate
? We already have an optional third argument there. So, shall we do validate(value, User, { coerce: true }, message)
, validate(value, User, { coerce: true, message: message })
or just leave it alone and change nothing?
@Azarattum wow learned something new about Error.cause
, that's a great solution. I think doing it as validate(value, struct, { coerce, message })
sounds like the best way to go, so that everything further up the chain gets it the same way.
Done in https://github.com/ianstormtaylor/superstruct/pull/1141. @ianstormtaylor, could you take a look?
I am seriously considering porting some react-hook-form
forms to this library from zod
, but not being able to clean up the error messages into something for a human in the schema is a significant obstacle. It looks like we can use custom assertations with the PR above, but that would mean never using the built-in functions with react-hook-form,
which is not perfect.
They provide a resolver that works quite well, besides the for-robots error messages: https://react-hook-form.com/get-started#SchemaValidation
@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 })
Result
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:
your name is too long
your name needs to be 3 characters at least
your name is already taken
[...]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 @hookform/resolvers/superstruct
to my own codebase and extend that with personalized error messages based on the error key as demonstrated in https://github.com/ianstormtaylor/superstruct/issues/568#issuecomment-1868421657
It seems daunting to need to maintain/replace the provided resolver, but actually the code is VERY short and never changes. https://github.com/react-hook-form/resolvers/blob/master/superstruct/src/superstruct.ts
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?