4
votes

I'm trying to return multiple custom error messages from my Joi validation schema. Here is the schema

const Joi = require("@hapi/joi");
const string = Joi.string();
const emailSchema = string.email();
const usernameSchema = string
  .min(3)
  .max(30)
  .error(() => "Username must be between 3 and 30 characters");
const passwordSchema = string
  .min(6)
  .error(() => "Password must be at least 6 characters");
const confirmPasswordSchema = Joi.valid(Joi.ref("passwordSchema")).error(
  () => "Passwords must match"
);

const localRegistrationSchema = Joi.object().keys({
  email: emailSchema.required().error(() => "Email is required"),
  username: usernameSchema.required().error(() => "Username is required"),
  password: passwordSchema.required().error(() => "Password is required"),
  confirmPassword: confirmPasswordSchema
});

and here is where I am using the schema

const { error } = localRegistrationSchema.validate(req.body, {
        abortEarly: false
      });
console.log(error);
if (error) throw Boom.boomify(error);

But I keep getting an TypeError: Cannot read property 'filter' of undefined which looks to be caused by

details.push({
    message,
    path: item.path.filter((v) => typeof v !== 'object'),
    type: item.code,
    context: item.local
});

which is part of Joi's error handling code

I do not get this error when I don't attach the .error() part but I cannot get more than one error to show if I use .error(new Error("custom error message")

I can't figure out what is going wrong and I haven't been able to get any other way of returning multiple custom error messages to work

3
Joel Jacobson I found a more elegant solution. You can use the Joi.any.messages() option. For a complete example see: stackoverflow.com/a/58198097/5536304 - a1300
Thank you!! That works fantastic. However, I did find one caveat that also affects the original code you gave as well as the code in the example you linked. When you do string().required() it throws a string.empty error rather than an any.required error, so checking any.required does not work. You have to check string.empty. I do not know if it is the same for other types. - Joel Jacobsen

3 Answers

6
votes

Errors

I debugged your code and simply returning () => 'some error message' does not work for your solution. We need to return a function. You got an error because your path property on the custom error message was undefined.

enter image description here


Error Chaining does not work

const schema = Joi.object({
  prop: Joi.string()
           .min(9)
           .error(() => 'min error message')
           .required()
           .error(() => 'required error message');
});

Only one switched Error Message works

const schema = Joi.object({
  username: Joi.string()
            .min(9)
            .required()
            .error((errors) => {
              for (err of errors) {
                switch (err.code) {
                  case ('string.min'): {
                    return simpleErrorMsgFunc("prop min error message", ["prop"])(); // invoke
                  }
                  case 'any.required': {
                    return simpleErrorMsgFunc("prop is required", ["prop"])(); // invoke
                  }
                  default: {
                    return simpleErrorMsgFunc("prop has error", ["prop"])(); // invoke
                  }
                }
              }
            }),
});


Helper Function

The heart of my solution is the following function. It returns a function which returns an custom error object.:

function simpleErrorMsgFunc(message, path) {
  return () => {
    return {
      toString: () => message,
      message,
      path,
    }
  };
}


Whole Solution

const Joi = require("@hapi/joi");

function simpleErrorMsgFunc(message, path) {
  return () => {
    return {
      toString: () => message,
      message,
      path,
    }
  };
}

const localRegistrationSchema = Joi.object().keys({
  // email is simple, we only need 1 error message
  email: Joi.string()
            .email()
            .required()
            .error(simpleErrorMsgFunc("Email is required", ["email"])),

  // username is advanced, we need 2 error message
  username: Joi.string()
            .min(3)
            .max(30)
            .required()
            .error((errors) => {
              for (err of errors) {
                switch (err.code) {
                  case ('string.min' || 'string.max'): {
                    return simpleErrorMsgFunc("username must be between 3 and 30 characters", ["username"])(); // invoke
                  }
                  case 'any.required': {
                    return simpleErrorMsgFunc("username is required", ["username"])(); // invoke
                  }
                  default: {
                    return simpleErrorMsgFunc("username has error", ["username"])(); // invoke
                  }
                }
              }
            }),

// password is advanced, we need 2 error message
  password: Joi.string()
               .min(6)
               .required()
               .error((errors) => {
                for (err of errors) {
                  switch (err.code) {
                    case ('string.min'): {
                      return simpleErrorMsgFunc("Password must be at least 6 characters", ["password"])(); // invoke
                    }
                    case 'any.required': {
                      return simpleErrorMsgFunc("Password is required", ["password"])(); // invoke
                    }
                    default: {
                      return simpleErrorMsgFunc("password has error", ["password"])(); // invoke
                    }
                  }
                }
              }),

  confirmPassword: Joi.valid(Joi.ref("password"))
                     .error(simpleErrorMsgFunc("Passwords must match", ['confirmPassword']))
});

const req = {
  body: {
    email: '[email protected]',
    username: 'hee',
     password: '45645656',
     confirmPassword: '45645656_',
  },
};

const { error } = localRegistrationSchema.validate(req.body, {
  abortEarly: false
});
console.log(JSON.stringify(error, null, 2));

P.S. I noticed that your confirmPassword property is not required!

2
votes

Seening that this is still getting a lot of views, I'd like to add that this is how I am currently handling multiple errors with Joi. I'm just stringing together all my validation/sanitization functions and then rather than using a switch statement, all of my custom messages go in at the end in .messages({}). The same works with yup if you're using that on the frontend. This makes it much cleaner and more concise than a switch statement and an error message helper function.

const string = Joi.string();
const passPattern =
  "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*\\W)[a-zA-Z0-9\\S]{8,}$";

export const signupLocalSchema = Joi.object({
  email: string.email().trim().lowercase().required().messages({
    "string.email": "Not a valid email address.",
    "string.empty": "Email is required.",
  }),
  username: string.min(3).max(30).trim().lowercase().required().messages({
    "string.min": "Username must be between 3 and 30 characters.",
    "string.max": "Username must be between 3 and 30 characters.",
    "string.empty": "Username is required.",
  }),
  password: string.pattern(new RegExp(passPattern)).messages({
    "string.pattern.base":
      "Password must be at least 8 characters and contain at least 1 lowercase, 1 uppercase, 1 number and 1 special character.",
  }),
  confirmPassword: Joi.valid(Joi.ref("password")).messages({
    "any.only": "Passwords must match.",
  }),
});
0
votes

You can try something like this

email: emailSchema.required().error(() => {
    return {
      message: "Email is required."
    };
}),