Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning multiple errors in Joi

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

like image 396
Joel Jacobsen Avatar asked Dec 23 '22 21:12

Joel Jacobsen


2 Answers

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!

like image 199
a1300 Avatar answered Dec 26 '22 01:12

a1300


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.",
  }),
});
like image 26
Joel Jacobsen Avatar answered Dec 26 '22 00:12

Joel Jacobsen