jquense / yup

Dead simple Object schema validation
MIT License
22.95k stars 935 forks source link

How to supply error messages & labels separately #2074

Open thany opened 1 year ago

thany commented 1 year ago

It would be convenient if labels, normally passed through the label() function into a schema, could be passed into a schema separately. This would make it more convenient when loading in labels from a separate source like, say, a JSON file. Same for error messages.

So imagine a simple schema like this:

const schema = yup.object().shape({
  name: yup.string().required(),
  age: yup.number().required().positive(),
});

This is great. This schema only contains the functional validation requirements, and no text content. Text content is fetched from somewhere else, all at once, through an object. Could be anything, so please try not to solve where our content is coming from. It comes from a CMS and there's nothing I can do about it.

I also don't want to hardcode error messages in every field in every schema. I want to dynamically construct a set of labels and error messages and pour that into a schema in a function that is generic and separate from schema creation.

It boils down to this: I want to add labels and error messages after having created the schema. Or before. But I can't have it intermixed with the schema as it is.

So what if I want to do something like this:

const schemaButNowWithLabels = schema.labels({
  name: 'Your name',
  age: 'How old are you'
});

Now as for error messages:

constschemaButNowWithErrorMessages = schema.messages({
  all: {
    required: 'This field is required'
  },
  age: {
    required: 'We need you to disclose your age please'
  }
})

There are 2 required fields. One is for all field, for only the current schema, but not the entire application. This is currently not possible at all, afaik. The one inside age is specific to the age field.

Please note that setLocale is global to the entire application, so it cannot be used to cover only a single schema.

With this, those messages & labels objects can be created dynamically, and the schema creation can remain mean and lean, maybe a little bit like this. Those withLabels and withMessages would be some kind of functionality (written by me) that somehow enriches the passed schema with labels and messages:

const schema = withLabels(withMessages(
  yup.object().shape({
    name: yup.string().required(),
    age: yup.number().required().positive(),
  })
));

This leaves us with three levels of error messages:

  1. Global, via the setLocale function
  2. Schema-wide
  3. Specific to a field

There's not much use in proving a runnable reproduction case, since I just simply haven't found out how or where to do this in the current latest version. It just simply seems to be impossible to pass labels and messages into a schema after having created it.

MaximGB commented 1 year ago

Warning: use it on your own risk, I don't know if the proposed solution will be applicable for the future versions of YUP.

This is what I use in the current project, which uses pretty outdated YUP version.

Every schema test method recieves an error message. The type of the message:

export type Message<Extra extends Record<string, unknown> = any> =
  | string
  | ((params: Extra & MessageParams) => unknown)
  | Record<PropertyKey, unknown>;

The function case is the escape hatch I use. A message might be a function, which recieves validation related parameters, like requried length for yup.string().length(...) for example, and it returns unknown. So YUP makes no assumptions about what to do this the returned value and thus returns it as is in the exception which will be thrown by YUP schema validate() method. In my case I provide YUP tests with a function similar to the code below:

const defferError = <TDeferredError extends string>(errorType: TDeferredError) => <TExtra extends Record<string, unknown>(params: Extra & MessageParams) => ({
    errorType,
    ...params
})

and use it like this:

const stringSchema = yup.string().required(defferError('required'))

in the exception throw by sctringSchema.validate objects returned by defferError('required') resulting function will be present as is - just error type + parameters YUP provides. So you can analyze it and then format according to whatever locale you need.

thany commented 1 year ago

I expect that passing a function as an error message will continue to exist, and it could indeed be used as a way to defer building the actual string messages. But it doesn't solve the underlying problem: a schema, once built, is effectively immutable and no functions exist to transform it with extended information like labels or messages separately from the validation rules.

MaximGB commented 1 year ago

My experience shows that to defer errors is enough to build any message you might need later. I might have a schema like:

const formSchema = yup.objectSchema({
    field: yup.string.required(deferError('required'))
})

Producing a ValidationException with inner error:

{
    errorType: 'required',
    path: 'field'
    originalValue: undefined
    value: undefined,
    label: 'field'
}

Which I can easily format to: [Some real field name] is requried

To defer an error message allows you to create schema to validate an entity, as well as to make decision regarding how defered error type relate to acutal fields later at the validation schema applicaton time.

thany commented 1 year ago

Can't do the same with labels, right?

MaximGB commented 1 year ago

As far as I get it, the point of labels is to substitute schema fields with human readable names in the string type error messages, so I see no reason one needs labels if he/she controls entire message.

thany commented 1 year ago

And if one does not control the entire message (I guess you meant schema)?

Labels of fields might be supplied by a different part of the system, somewhere "far away" from the schema. And maybe I can't just pass the labels down into whereever the schema gets built - one of the pet peeves, I guess, of applications that are slightly more complicated than simple examples from a getting started guide.

jquense commented 1 year ago

Yup's label is really meant for simple cases, for complex or really dynamic messages I'd suggest using message keys or objects and handle the message rendering at validation like: https://github.com/jquense/yup#localization-and-i18n