Object and array validations should return all invalid fields instead of just the first one.
typeofweb opened this issue ยท 20 comments
for example:
try {
validate(object({ foo: string() }))({ foo: 123 })
} catch(err) {
err.errors // { foo: { expected: 'string', got: 'number', message: 'foo has to be a string' } }
}
But we need to figure out how to present the errors of other schema types (like oneOf or just string)
Hi! I was thinking about it a bit and what seems to be needed for errors is
- the unique name of refinement or modifier so that we know what failed
- args supplied to modifier so that it is possible to construct a custom error message
Idea for error structure:
// Maybe just an object because we are interested in first error only?
type ErrorDetails = Array<{
// string, minStringLength or any custom refinement/modifier name
name: string;
expected: string;
got: string;
// generated message?
message: string;
// actual value (is it needed?)
value: unknown;
// args passed to modifiers e.g. [10] for minStringLength(10)
args?: Array<any>;
}>
type Errors = ErrorDetails | {
[attr: string]: ErrorDetails
}
In general, this will allow acting in many various ways just based on error results. We will be able to create e.g. custom, user-friendly error messages to be displayed for users (see #59 ). We could also consider different ways of supporting custom error messages e.g
validate(schema, {
'default': 'This field is required',
'validatorName': (error) => 'Please enter at least ${error.args[0]} characters. Please :pray:'
})
Yup is doing it great by providing setLocale
functionality: https://github.com/jquense/yup#using-a-custom-locale-dictionary although I don't like it because it is global...
Thank you for your comment! I actually started working on this today and your input is very helpful.
@all-contributors please add @darkowic for ideas
@mmiszy
I've put up a pull request to add @darkowic! ๐
I was curious too, I dived into code and started working on POC and I have something dirty already :P its evolving and I will be happy to help with the API
I don't have time today but tomorrow/Friday I can create a draft with my proposition. Give me some time ๐
@darkowic what do you think of this? https://codesandbox.io/s/formik-example-forked-rjns8?fontsize=14&hidenavigation=1&theme=dark
@mmiszy yes, looks promising. Now, how will it work when you add pipe(string, minStringLength(10)
? Or create any custom refinement like pipe(number, min(10), max(50))
? How do I get information about what is exactly wrong? The value is too small or too high? I see that the expected
is just a field name which is useless from user's perspective.
If you think that it is out of the scope of this library and it should be simple TS types verification for developers then I need to find something else :) To display a nice message for users we need more details... Btw this error:
ValidationError: Invalid type! Expected { email: string } but got {"email":"d"}!
is not clear even for us. should be more like:
ValidationError: Invalid type! Expected { email: email } but got {"email":"d"}!
I really like the super simple API for creating the schema and would be great to use it for my forms usecase ;) I will work a bit on it now.
See #62 - my proposition implementation. I'm not experienced in functional programming so feedback appreciated ;)
@darkowic that comment was RE me previous one #13 (comment)
I'll take a look at your implementation later today, thank you!
Aaand I was late one day... I planned to finish my PR today...
Anyways, I tried your implementation @mmiszy. First of all many things are not consistent. Here is my resolver:
React hook form
import { Resolver } from 'react-hook-form';
import { validate, ValidationError } from '@typeofweb/schema';
type ErrorData = {
args?: unknown[];
expected: string;
got: unknown;
};
function errorToMessage(error: ErrorData): string {
switch (error.expected) {
case 'minStringLength':
return `Enter at least ${error.args[0]} characters`;
case 'max':
return `Maximum allowed value is ${error.args[0]}`;
case 'min':
return `Minimum allowed value is ${error.args[0]}`;
default:
return 'This field is required.';
}
}
const isErrorData = (fields: unknown): fields is ErrorData =>
typeof fields === 'object' && 'expected' in fields;
type ErrorMessages = string | Record<string, ErrorMessages>;
const errorsToObject = (error: ValidationError): ErrorMessages => {
if (isErrorData(error.details.fields)) {
return errorToMessage(error.details.fields);
}
if (typeof error.details.fields === 'string') {
// in case of required value there is no details...
return errorToMessage({
expected: 'required',
got: error.details.fields,
});
}
return Object.entries(error.details.fields).reduce((acc, [name, field]) => {
acc[name] = errorsToObject(field as ValidationError);
return acc;
}, {} as ErrorMessages);
};
export const schemaResolver = (schema): Resolver<any> => {
const validator = validate(schema);
return (values) => {
try {
return {
values: validator(values),
errors: {},
};
} catch (error) {
if (error instanceof ValidationError) {
console.log('validation error!', error.details);
return {
values: {},
errors: errorsToObject(error),
};
}
throw error;
}
};
};
The schema:
const schema = object({
name: pipe(minStringLength(10), string),
energyDemand: pipe(max(10000), min(100), number),
})();
It was not that obvious how to get the error details and also the details are not consistent:
Here the details.fields
is empty string:
ValidationError.details
is the only public field. details.fields
is sometimes string
, sometimes ValidationError
and sometimes ErrorData
...
Also, I expected to get some feedback about my implementation but I did not get anything...
Reverted #61, it was premature and wasn't working as expected.
I like your PR! When do you think you could finish it?
@mmiszy I think I'm done. I would appreciate some review and feedback ;)