Node.js + Joi how to display a custom error messages?
Asked Answered
L

17

89

It seems pretty straight forward to validate user's input in Node.js RESTapi with Joi.

But the problem is that my app is not written in English. That means I need to send a custom written messages to the frontend user.

I have googled for this and only found issues.

Maybe could someone give a solution for this?

This is the code I am using to validate with the Joi system:

    var schema = Joi.object().keys({
      firstName: Joi.string().min(5).max(10).required(),
      lastName: Joi.string().min(5).max(10).required()
      ..
    });

    Joi.validate(req.body, schema, function(err, value) {
      if (err) {
        return catched(err.details); 
      }
    });

    function catched(reject) {
      res.json({
        validData: false,
        errors: reject
      });
    }

Plus, is there a way to use Joi in client side?

Thanks!

Lockup answered 10/2, 2018 at 12:48 Comment(1)
Interesting read about using Joi for front-facing error messages: github.com/hapijs/joi/issues/546Allophone
L
29

A solution I have found is to set:

var schema = Joi.object().keys({
  firstName: Joi.string().min(5).max(10).required().label("Your error message in here"),
  lastName: Joi.string().min(5).max(10).required()
  ..
});

Then print the label from the callback error variable

Lockup answered 10/2, 2018 at 20:7 Comment(2)
The "label" is only overrides the key name in error messages. While other solutions displayed how to customize the error message.Protestantism
How do I print the lablel message as an objectBotheration
C
115

Original answer:

The current way (I personally find it better) is to use .messages() (or .prefs({messages})).

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

const joiSchema = Joi.object({
  a: Joi.string()
    .min(2)
    .max(10)
    .required()
    .messages({
      'string.base': `"a" should be a type of 'text'`,
      'string.empty': `"a" cannot be an empty field`,
      'string.min': `"a" should have a minimum length of {#limit}`,
      'any.required': `"a" is a required field`
    })
});

const validationResult = joiSchema.validate({ a: 2 }, { abortEarly: false });
console.log(validationResult.error); // expecting ValidationError: "a" should be a type of 'text'

Usage of .errors() is not recommended just to update default message with custom message.

.prefs({ messages }) is an elaborate way to provide more options as preferences. The other options for prefs are taken directly from options of .validate()

Further read: https://github.com/hapijs/joi/issues/2158


Update 1: I saw that the above explanation did not work out for some folks, so I have put up some code to test yourself. Check it here: https://runkit.com/embed/fnfaq3j0z9l2

Also updated the code snippet shared previously to have details from package inclusion, to usage, to calling the actual validation method.


Update 2: The list of joi error types and their description (for .messages() - like string.base, array.unique, date.min etc..) is available here.


Update 3: Joi has moved from hapi project to standalone: https://www.npmjs.com/package/joi. Make sure you are using latest version (or at least above v17).

Carbamate answered 4/10, 2019 at 10:5 Comment(6)
TypeError: Joi.string(...).messages is not a functionCarissacarita
What version of Joi are you using? Please check it here. Also, the documentation specifies the usage of any.messages hereCarbamate
Benefit of this answer, is it also works properly with 3rd party packages such as react-hook-formPlumbo
Just a heads up, the package @hapi/joi is now moved back to joi. Per the authors message here: npmjs.com/package/@hapi/joi, "joi is leaving the @hapi organization and moving back to 'joi' (github.com/sideway/joi/issues/2411)"Indulgent
@RvyPandey this looks like the answer I need but am having trouble. I posted new question at #66070968. Thanks for any help!Ope
To find the key e.g. string.base , All you need to check the type key in the default response and just copy and paste that key in messages functions and it would workBarbados
A
80

Extending on Ashish Kadam's answer, if you have many different error types, you can check which type of error is present, and set its message accordingly:

Joi v17.4.0

v17.4.0 uses err.code

var schema = Joi.object().keys({
  firstName: Joi.string().min(5).max(10).required().error(errors => {
    errors.forEach(err => {
      switch (err.code) {
        case "any.empty":
          err.message = "Value should not be empty!";
          break;
        case "string.min":
          err.message = `Value should have at least ${err.local.limit} characters!`;
          break;
        case "string.max":
          err.message = `Value should have at most ${err.local.limit} characters!`;
          break;
        default:
          break;
      }
    });
    return errors;
  }),
  // ...
});

You can check the list of errors here: Joi 17.4.0 API Reference > Errors > List of errors

Joi v14.3.1

v14.3.1 uses err.type

var schema = Joi.object().keys({
  firstName: Joi.string().min(5).max(10).required().error(errors => {
    errors.forEach(err => {
      switch (err.type) {
        case "any.empty":
          err.message = "Value should not be empty!";
          break;
        case "string.min":
          err.message = `Value should have at least ${err.context.limit} characters!`;
          break;
        case "string.max":
          err.message = `Value should have at most ${err.context.limit} characters!`;
          break;
        default:
          break;
      }
    });
    return errors;
  }),
  // ...
});

You can check the list of errors here: Joi 14.3.1 API Reference > Errors > List of errors

Also you can check the any.error reference for more information. Quoting the docs:

Overrides the default joi error with a custom error if the rule fails where:

  • err can be:
    • an instance of Error - the override error.
    • a function(errors), taking an array of errors as argument, where it must either:
      • return a string - substitutes the error message with this text
      • return a single object or an Array of it, where:
        • type - optional parameter providing the type of the error (eg. number.min).
        • message - optional parameter if template is provided, containing the text of the error.
        • template - optional parameter if message is provided, containing a template string, using the same format as usual joi language errors.
        • context - optional parameter, to provide context to your error if you are using the template.
      • return an Error - same as when you directly provide an Error, but you can customize the error message based on the errors.
  • options:
    • self - Boolean value indicating whether the error handler should be used for all errors or only for errors occurring on this property (true value). This concept only makes sense for array or object schemas as other values don't have children. Defaults to false.
Aviator answered 12/2, 2019 at 19:50 Comment(3)
i liked this answer the most.Tuinal
I've created a blog post regarding adding error code(s) to Hapi response based on this answer. Thanks for the solution. Anyone who's interested in can check it out at midnightcodr.github.io/2019/06/08/…Banff
excellent answer! - btw i used case "string.empty" instead of case "any.empty", saying about v17.4.0Deterioration
E
43

Joi Version 14.0.0

const SchemaValidation = {
  coins: Joi.number()
    .required()
    .error(() => {
      return {
        message: 'Coins is required.',
      };
    }),
  challenge_name: Joi.string()
    .required()
    .error(() => {
      return {
        message: 'Challenge name is required.',
      };
    }),
  challengeType: Joi.string()
    .required()
    .error(() => {
      return {
        message: 'Challenge type is required.',
      };
    }),
  challengeDescription: Joi.string()
    .required()
    .error(() => {
      return {
        message: 'Challenge description is required.',
      };
    }),
};

In errors object you can get, error type and change message according.

Eleph answered 17/10, 2018 at 12:31 Comment(2)
This is not right as error() must return an JS Error object.Regimen
not sure if this is the right way, but seems to work .error( err => { err[0].message= "Custom Error Message"; return err; } )Magniloquent
H
39

Using templates

I had to dig through the source to find an example of how to do context-dependent templating / formatting of messages since it doesn't seem to be documented:

messages: {
  'string.alphanum': '{{#label}} must only contain alpha-numeric characters',
  'string.base': '{{#label}} must be a string',
  'string.base64': '{{#label}} must be a valid base64 string',
  'string.creditCard': '{{#label}} must be a credit card',
  'string.dataUri': '{{#label}} must be a valid dataUri string',
  'string.domain': '{{#label}} must contain a valid domain name',
  'string.email': '{{#label}} must be a valid email',
  'string.empty': '{{#label}} is not allowed to be empty',
  'string.guid': '{{#label}} must be a valid GUID',
  'string.hex': '{{#label}} must only contain hexadecimal characters',
  'string.hexAlign': '{{#label}} hex decoded representation must be byte aligned',
  'string.hostname': '{{#label}} must be a valid hostname',
  'string.ip': '{{#label}} must be a valid ip address with a {{#cidr}} CIDR',
  'string.ipVersion': '{{#label}} must be a valid ip address of one of the following versions {{#version}} with a {{#cidr}} CIDR',
  'string.isoDate': '{{#label}} must be in iso format',
  'string.isoDuration': '{{#label}} must be a valid ISO 8601 duration',
  'string.length': '{{#label}} length must be {{#limit}} characters long',
  'string.lowercase': '{{#label}} must only contain lowercase characters',
  'string.max': '{{#label}} length must be less than or equal to {{#limit}} characters long',
  'string.min': '{{#label}} length must be at least {{#limit}} characters long',
  'string.normalize': '{{#label}} must be unicode normalized in the {{#form}} form',
  'string.token': '{{#label}} must only contain alpha-numeric and underscore characters',
  'string.pattern.base': '{{#label}} with value {:[.]} fails to match the required pattern: {{#regex}}',
  'string.pattern.name': '{{#label}} with value {:[.]} fails to match the {{#name}} pattern',
  'string.pattern.invert.base': '{{#label}} with value {:[.]} matches the inverted pattern: {{#regex}}',
  'string.pattern.invert.name': '{{#label}} with value {:[.]} matches the inverted {{#name}} pattern',
  'string.trim': '{{#label}} must not have leading or trailing whitespace',
  'string.uri': '{{#label}} must be a valid uri',
  'string.uriCustomScheme': '{{#label}} must be a valid uri with a scheme matching the {{#scheme}} pattern',
  'string.uriRelativeOnly': '{{#label}} must be a valid relative uri',
  'string.uppercase': '{{#label}} must only contain uppercase characters'
}

An example of using a templated message:

const Joi = require("joi");

const schema = Joi.object({
  nested: Joi.object({
    name: Joi.string().required().messages({
      "any.required": "{{#label}} is required!!",
      "string.empty": "{{#label}} can't be empty!!",
    }),
  }),
});

const result = schema.validate({
  nested: {
    // comment/uncomment to see the other message
    // name: "",
  },
});

console.log(result.error.details);

When using the template syntax, the context values that seem to be passed are something like the following, though specific rules / validators may pass more context:

{
 ​key: "name", // this key, without ancestry
 ​label: `"nested.name"`, // full path with dots as separators, in quotes
 ​value: "", // the value that was validated
}
Hurwit answered 23/6, 2021 at 2:32 Comment(1)
Their docs are just awful...Madrigal
L
29

A solution I have found is to set:

var schema = Joi.object().keys({
  firstName: Joi.string().min(5).max(10).required().label("Your error message in here"),
  lastName: Joi.string().min(5).max(10).required()
  ..
});

Then print the label from the callback error variable

Lockup answered 10/2, 2018 at 20:7 Comment(2)
The "label" is only overrides the key name in error messages. While other solutions displayed how to customize the error message.Protestantism
How do I print the lablel message as an objectBotheration
N
24

You can use .error(new Error('message')), And its work for me

var schema = Joi.object().keys({
  firstName: Joi.string().min(5).max(10).required().error(new Error('Give your error message here for first name')),
  lastName: Joi.string().min(5).max(10).required().error(new Error('Give your error message here for last name'))
  ..
});

Joi.validate(req.body, schema, function(err, value) {
  if (err) {
    console.log(err.message)
    return catched(err.message); 
  }
});
Northcutt answered 22/2, 2018 at 6:54 Comment(1)
But it will bypass the abortEarly :(Carissacarita
C
11

You can also show messages for a particular property

const Joi = require('Joi');

const schema = Joi.object({
    username: Joi.string()
      .min(2)
      .max(30)
      .required()
      .pattern(new RegExp(/^(?!.*\.\.)(?!.*\.$)[^\W][\w.]{0,29}$/))
      .message({"string.pattern.base":"Invalid username",
                "string.min":"minimum 2 character required",
                "string.max":"maximum 30 characters allowed"})
  });

You can refer this for message object keys.

messages: {
      'any.custom': [Object],
      'any.default': [Object],
      'any.failover': [Object],
      'any.invalid': [Object],
      'any.only': [Object],
      'any.ref': [Object],
      'any.required': [Object],
      'any.unknown': [Object],
      'string.alphanum': [Object],
      'string.base': [Object],
      'string.base64': [Object],
      'string.creditCard': [Object],
      'string.dataUri': [Object],
      'string.domain': [Object],
      'string.email': [Object],
      'string.empty': [Object],
      'string.guid': [Object],
      'string.hex': [Object],
      'string.hexAlign': [Object],
      'string.hostname': [Object],
      'string.ip': [Object],
      'string.ipVersion': [Object],
      'string.isoDate': [Object],
      'string.isoDuration': [Object],
      'string.length': [Object],
      'string.lowercase': [Object],
      'string.max': [Object],
      'string.min': [Object],
      'string.normalize': [Object],
      'string.token': [Object],
      'string.pattern.base': [Object],
      'string.pattern.name': [Object],
      'string.pattern.invert.base': [Object],
      'string.pattern.invert.name': [Object],
      'string.trim': [Object],
      'string.uri': [Object],
      'string.uriCustomScheme': [Object],
      'string.uriRelativeOnly': [Object],
      'string.uppercase': [Object]
    }
Collection answered 6/5, 2021 at 10:36 Comment(0)
M
8

Solution to add custom messages: Simply add another chained function to throw error while defining your schema.
In your case

 firstName: Joi.string().min(5).max(10).required().error(new Error('I am a custom error and I know it!')),

Rest will remain same.

Solution to use Joi at client side (Your 2nd question)

Joi-Browser is the package which enables usage of the same schema at the client side.

Here is an interesting discussion you can have a look at.

Cheers!

Mahound answered 8/5, 2018 at 22:46 Comment(0)
B
4

Just call "message()" function :

firstName: Joi.string().message("Your custom message")
Braziel answered 19/4, 2020 at 13:4 Comment(0)
P
3

let schema = Joi.object({ foo: Joi.number().min(0).error(() => '"foo" requires a positive number') });

Docs link

Polymorphous answered 25/5, 2018 at 14:15 Comment(0)
F
3

In the latest version use message as.

var schema = Joi.object().keys({
  firstName: Joi.string().min(5).max(10).required().messages({
    "string.base": `"username" should be a type of 'text'`,
    "string.empty": `"username" cannot be an empty field`,
    "any.required": `"username" is a required.`,
  }),
  lastName: Joi.string().min(5).max(10).required().messages({
    "string.base": `"lastName" should be a type of 'text'`,
    "string.empty": `"lastName" cannot be an empty field`,
    "any.required": `"lastName" is a required.`,
  }),
  [...]
});
Ferriferous answered 15/7, 2020 at 6:57 Comment(1)
i have question ... what is condition for email valid formatTweedsmuir
S
2

For anyone who like me was looking to validate a number with a custom message, this is from the source code :

'number.base': '{{#label}} must be a number',
'number.greater': '{{#label}} must be greater than {{#limit}}',
'number.infinity': '{{#label}} cannot be infinity',
'number.integer': '{{#label}} must be an integer',
'number.less': '{{#label}} must be less than {{#limit}}',
'number.max': '{{#label}} must be less than or equal to {{#limit}}',
'number.min': '{{#label}} must be greater than or equal to {{#limit}}',
'number.multiple': '{{#label}} must be a multiple of {{#multiple}}',
'number.negative': '{{#label}} must be a negative number',
'number.port': '{{#label}} must be a valid port',
'number.positive': '{{#label}} must be a positive number',
'number.precision': '{{#label}} must have no more than {{#limit}} decimal places',
'number.unsafe': '{{#label}} must be a safe number'
Shithead answered 2/5, 2022 at 15:54 Comment(0)
A
1
let schema = Joi.object().keys({
   Joi.string().required().options({language: {any: {required: "First name is required"}}})
});
Angelia answered 26/2, 2019 at 5:42 Comment(1)
TypeError: Joi.string(...).messages is not a functionCarissacarita
M
1

For anyone having a problem with

...messages is not a function

errors, you must install joi with npm install @hapi/joi, and importing it with @hapi/joi. I've made the mistake of installing joi without the @hapi/ prefix and it took me a while to find the error.

Marlomarlon answered 19/10, 2019 at 23:7 Comment(1)
Also, while requiring the module, specifying it as const Joi = require('@hapi/joi') is necessary, if you have Joi installed globally.Openeyed
F
1

Here's an example of how I did it in using joi 17.6

VerifyAuthOtpValidator (otpProps: OtpProps) {
        const otpSchema = Joi.object<OtpProps>({
            otp_code: Joi.string().required().messages({
                "string.empty": "No OTP code provided",
                "any.required": "No OTP code provided",
            })
        });
        const {error, value} = otpSchema.validate(otpProps);
        //you can log the error property, as this would enable you
        // see the error type, which you can then, customise
        // the message
        // console.log("\n\t error: ", error)
        if(error){
            return error.message
        }
        return value
    }
Fontainebleau answered 9/5, 2022 at 16:20 Comment(0)
D
-1

The best solution I found was :

Create a Middleware for JOI validation

Validator.js - You can create your custom error object

const Joi = require('Joi');

module.exports = schema => (req, res, next) => {
  const result = Joi.validate(req.body, schema);
  if (result.error) {
    return res.status(422).json({
      errorCause: result.error.name,
      missingParams: result.error.details[0].path,
      message: result.error.details[0].message
    });
  }
  next();
};

In the routes or controller pass this middleware function


const joiValidator = require('../utils/validator'); // Wherever you have declare the validator or middlerware

   const  userSchema = joi.object().keys({
    email : joi.string().email().required(),
    password : joi.string().required()
  });

  routes.routes('/').get(joiValidator(userSchema), (req, res) => {
    res.status(200).send('Person Check');
  });

Duelist answered 28/5, 2020 at 17:39 Comment(0)
S
-4

If you need an easy way to pass custom message, use js-flex-validator

import Flex, { validateObject } from "js-flex-validator";

const constraints = [
  Flex("username")
    .string()
    .allowEmpty()
    .min(3, "Username should be at least 3 characters")
    .max(50, "Username should not exceeds 50 characters"),
  Flex("email")
    .email("This email is not valid.")
    .match(/\[email protected]$/, "Should be a edu.com domain")
    .required()
    .min(5, "Email should be at least 3 characters")
    .max(255, "Username should not exceeds 255 characters"),
  Flex("password")
    .string()
    .required()
    .min(5, "Password should be at least 5 characters")
    .max(20, "Password should not exceeds 50 characters"),
];

const data = {username: "geek4", email: "[email protected]", password: "123456" };
const { hasError, errorDetails } = validateObject(data, constraints)
Shortbread answered 29/12, 2021 at 18:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.