joi 参数验证
Imagine you are working on an API endpoint to create a new user, and some user data such as firstname
, lastname
, age
and birthdate
will need to be included in the request. Passing Sally
in as value for age
, or 53
for birthdate
would be undesireable. You don’t want bad data making its way through your application, so what do you do? The answer is Data Validation.
假设您正在使用API端点来创建一个新用户,并且请求中将需要包含一些用户数据,例如firstname
, lastname
, age
和birthdate
。 不希望将Sally
作为age
值或将53
作为birthdate
。 您不希望不良数据通过您的应用程序传播,那么您该怎么办? 答案是数据验证 。
If you have ever used an ORM when building your Node application such as Sequelize, Knex, Mongoose (for MongoDB), etc, you will know that it is possible to set validation constraints for your model schemas. This makes it easier to handle and validate data at the application level before persisting it to the database. When building APIs, the data usually come from HTTP requests to certain endpoints, and the need may soon arise to be able to validate data at the request level.
如果在构建Node应用程序(例如Sequelize , Knex , Mongoose (用于MongoDB)等)时曾经使用过ORM,那么您将知道可以为模型模式设置验证约束。 这使得在将数据持久化到数据库之前,更容易在应用程序级别处理和验证数据。 在构建API时,数据通常来自HTTP请求到某些端点,并且可能很快就会需要能够在请求级别验证数据。
In this tutorial, we will learn how we can use the Joi validation module to validate data at the request level. You can learn more about how to use Joi and the supported schema types by checking out the API Reference. At the end of this tutorial, you should be able to do the following:
在本教程中,我们将学习如何使用Joi验证模块在请求级别验证数据。 您可以通过检出API Reference进一步了解如何使用Joi和受支持的模式类型。 在本教程的最后,您应该能够执行以下操作:
We will pretend we are building a mini school portal and we want to create API endpoints to be able to:
我们将假装我们正在建立一个迷你学校门户,并且我们希望创建API端点以能够:
We will create a very basic REST API for this tutorial using Express just to test our Joi schemas. To begin, jump to your command line terminal and run the following command to setup a new project and install the required dependencies:
我们将使用Express为本教程创建一个非常基本的REST API,仅用于测试我们的Joi模式。 首先,跳到命令行终端并运行以下命令来设置新项目并安装所需的依赖项:
Create a new file named app.js
in your project root directory to setup the Express app. Here is a starter setup for the application.
在项目根目录中创建一个名为app.js
的新文件,以设置Express应用程序。 这是该应用程序的入门设置。
/* app.js */
// load app dependencies
const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');
const Routes = require('./routes');
const app = express();
const port = process.env.NODE_ENV || 3000;
// app configurations
app.set('port', port);
// load app middlewares
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// load our API routes
app.use('/', Routes);
// establish http server connection
app.listen(port, () => { console.log(`App running on port ${port}`) });
It is worth noting that we have added body-parser
middlewares to the request pipeline of our app. These middlewares fetch and parse the body of the current HTTP request for application/json
and application/x-www-form-urlencoded
requests, and make them available in the req.body
of the request’s route handling middleware.
值得注意的是,我们已经将body-parser
中间件添加到了应用程序的请求管道中。 这些中间件获取并解析针对application/json
和application/x-www-form-urlencoded
请求的当前HTTP请求的主体,并使它们在请求的路由处理中间件的req.body
中可用。
From our application setup, we specified that we are fetching our routes from a routes.js
file. Let’s create the file in our project root directory with the following content:
从我们的应用程序设置中,我们指定要从routes.js
文件中获取路由。 让我们在项目根目录中创建具有以下内容的文件:
/* routes.js */
const express = require('express');
const router = express.Router();
// generic route handler
const genericHandler = (req, res, next) => {
res.json({
status: 'success',
data: req.body
});
};
// create a new teacher or student
router.post('/people', genericHandler);
// change auth credentials for teachers
router.post('/auth/edit', genericHandler);
// accept fee payments for students
router.post('/fees/pay', genericHandler);
module.exports = router;
Finally, add a start
script to the scripts
section of your package.json
file. It should look like this:
最后,将start
脚本添加到package.json
文件的scripts
部分。 它看起来应该像这样:
{
...
"scripts": {
"start": "node app.js"
},
...
}
Let’s go ahead to run our app to see where we’ve gotten to and to ensure everything is fine:
让我们继续运行我们的应用程序,以了解到达的位置并确保一切正常:
Note the port that the service is running on. You could test the API endpoints using an application such as Postman. Here is a sample screenshot of testing the POST /people
endpoint on Postman.
记下该服务正在运行的端口。 您可以使用Postman之类的应用程序测试API端点。 这是在Postman上测试POST /people
端点的示例屏幕截图。
A minimal example may help give us an idea of what we hope to achieve in later steps. You may not understand everything in this example at the moment, but you definitely will by the time you are done with this tutorial.
一个最小的例子可以帮助我们了解我们希望在以后的步骤中实现的目标。 您可能暂时无法理解本示例中的所有内容,但是一定会在完成本教程时知道。
About the Example In this example, we will create validation rules using Joi to validate an email, phonenumber and birthday for a request to create a new user. If the validation fails, we send back an error. Otherwise, we return the user data.
关于示例在本示例中,我们将使用Joi创建验证规则,以验证创建新用户的电子邮件,电话号码和生日。 如果验证失败,我们将发回错误。 否则,我们返回用户数据。
Let’s add a test route to the app.js
file. Add the following code snippet to the app.js
file for our simple example.
让我们向app.js
文件添加一条测试路由。 对于我们的简单示例,将以下代码片段添加到app.js
文件中。
/* app.js */
/**
* CODE ADDITION
*
* Defines a POST /test route for our simple example
*
* It must come after the following line:
* app.use('/', Routes);
*/
app.post('/test', (req, res, next) => {
// require the Joi module
const Joi = require('joi');
// fetch the request data
const data = req.body;
// define the validation schema
const schema = Joi.object().keys({
// email is required
// email must be a valid email string
email: Joi.string().email().required(),
// phone is required
// and must be a string of the format XXX-XXX-XXXX
// where X is a digit (0-9)
phone: Joi.string().regex(/^\d{3}-\d{3}-\d{4}$/).required(),
// birthday is not required
// birthday must be a valid ISO-8601 date
// dates before Jan 1, 2014 are not allowed
birthday: Joi.date().max('1-1-2004').iso(),
});
// validate the request data against the schema
Joi.validate(data, schema, (err, value) => {
// create a random number as id
const id = Math.ceil(Math.random() * 9999999);
if (err) {
// send a 422 error response if validation fails
res.status(422).json({
status: 'error',
message: 'Invalid request data',
data: data
});
} else {
// send a success response if validation passes
// attach the random ID to the data response
res.json({
status: 'success',
message: 'User created successfully',
data: Object.assign({id}, value)
});
}
});
});
/* CODE ADDITION ENDS HERE */
Here, we have tried to keep things as simple as possible. We have added some comments to describe what each line does. Don’t worry if everything is not clear to you at the moment. They will be clear to you as you progress in the tutorial. Now we will go ahead to test the example route.
在这里,我们试图使事情尽可能简单。 我们添加了一些注释来描述每一行的功能。 如果目前还不清楚,请不要担心。 在您学习本教程的过程中,它们将对您很清楚。 现在,我们将继续测试示例路线。
Start the app again by running npm start
from your terminal and head to [Postman][get-postman] to test the example route POST /test
. Here is a demo video of doing this:
通过从终端运行npm start
再次启动应用程序,然后转到[Postman] [get-postman]以测试示例路由POST /test
。 这是执行此操作的演示视频:
While this tutorial is not a substitute for the actual documentation and usage guide for the Joi package, we’ll try as much as possible to explore some important parts of the Joi API as required for our sample project.
尽管本教程不能替代Joi软件包的实际文档和使用指南 ,但我们将尽可能地探索示例项目所需的Joi API的一些重要部分。
Joi uses schemas to define validation rules and constraints for data. You can use Joi.any()
to create a simple schema that validates for any data type. This means that any value checked against the schema will be valid. The any()
schema is the base schema which every other Joi schema types inherit from.
Joi使用模式来定义验证规则和数据约束。 您可以使用Joi.any()
创建可验证任何数据类型的简单架构。 这意味着对照该模式检查的任何值都是有效的。 any()
架构是其他所有Joi架构类型都继承的基础架构。
const any = Joi.any();
You can specify more validation constraints to the base schema to control the kind of values that are considered valid. Since each constraint returns a schema instance, it is possible to chain several constraints together via method chaining to define more specific validation rules. Here are some very useful validation constraints that can be attached to the base schema:
您可以为基本架构指定更多验证约束,以控制被视为有效的值的种类。 由于每个约束都返回一个架构实例,因此可以通过方法链接将多个约束链接在一起以定义更具体的验证规则。 以下是一些非常有用的验证约束,可以将它们附加到基本架构:
allow()
Whitelists value or set of values that will always be valid before applying other rules. It can take values as its arguments or an array of values.
allow()
将在应用其他规则之前将始终有效的值或一组值列入白名单。 它可以将值作为其自变量或值的数组。
valid()
Whitelists value or set of values as the only valid value(s) before applying other rules. It can take values as its arguments or an array of values.
valid()
在应用其他规则之前,将值或一组值作为唯一的有效值列入白名单。 它可以将值作为其自变量或值的数组。
invalid()
Blacklists value or set of values that will never be valid. It does the direct opposite of allow()
. It can take values as its arguments or an array of values.
invalid()
将永远invalid()
值或一组值列入黑名单。 它与allow()
直接相反。 它可以将值作为其自变量或值的数组。
required()
Prevents the schema from allowing undefined
as value.
required()
防止架构允许将undefined
作为值。
optional()
The schema can allow undefined
as value. The schema however, will not allow a value of null
. This is the default behaviour.
optional()
模式可以允许将undefined
作为值。 但是,该架构不允许使用null
。 这是默认行为。
raw()
This outputs the original untouched value instead of the casted value.
raw()
这将输出原始的原始值,而不是转换值。
strict()
This enables strict mode - which prevents type casting for the current key and any child keys. In non-strict mode, the values are casted to match the specified validation schema where possible. This is the default behaviour.
strict()
这将启用严格模式 -防止对当前键和任何子键进行类型转换。 在非严格模式下,将值强制转换为与指定的验证模式匹配。 这是默认行为。
default()
This sets a default value if the original value is undefined
. In its simplest form, it takes as its first argument the value to use as default. See the API Reference for a detailed documentation of the default()
constraint.
default()
如果原始值undefined
则设置默认值。 以其最简单的形式,它将用作默认值的值作为其第一个参数。 有关default()
约束的详细文档,请参见API参考 。
Having seen these constraints, we can quickly create a schema that matches only the strings cat
and dog
as follows:
看到了这些约束之后,我们可以快速创建仅与字符串cat
和dog
匹配的架构,如下所示:
const catOrDog = Joi.any().valid(['cat', 'dog']).required();
The Joi.string()
schema allows you to validate only string values. It inherits from the any()
schema, hence all the constraints we saw earlier can be used with it. However, it provides some more constraints for validating strings more effectively. Here are some common ones:
Joi.string()
模式仅允许您验证字符串值。 它继承自any()
模式,因此我们前面看到的所有约束都可以与它一起使用。 但是,它提供了更多约束来更有效地验证字符串。 这是一些常见的:
min()
, max()
, length()
These are used to control the minimum length, maximum length or fixed length of the string. They each take as first argument, an integer
that specifies the length limit.
min()
, max()
, length()
这些用于控制字符串的最小长度,最大长度或固定长度。 它们每个都将第一个参数指定为长度限制的integer
。
email()
, ip()
, guid()
, uri()
, creditCard()
These are used to validate email addresses, IP addresses, GUIDs, URIs or credit card numbers (using the Luhn Algorithm) respectively. They each take an optional options object
as first argument. See the API Docs for detailed information about the supported options.
email()
, ip()
, guid()
, uri()
, creditCard()
这些分别用于验证电子邮件地址,IP地址,GUID,URI或信用卡号(使用Luhn算法 )。 它们每个都将一个可选的options object
作为第一个参数。 有关支持的选项的详细信息,请参阅API文档 。
alphanum()
, hex()
, base64()
These are used to restrict the valid strings to alphanumeric, hexadecimal or base64-encoded strings respectively.
alphanum()
, hex()
, base64()
这些分别用于将有效字符串限制为字母数字,十六进制或base64编码的字符串。
regex()
, replace()
These allow you to specify a custom regular expression that the string must match for it to be considered valid. They each take a RegExp
literal as first argument. regex()
can also take an optional options object
as second argument. replace()
is useful if you want to replace some parts of the matched string with another string. It takes the replacement string
as second argument.
regex()
, replace()
这些允许您指定自定义正则表达式,字符串必须匹配该自定义正则表达式才能被视为有效。 他们每个人都以RegExp
文字作为第一个参数。 regex()
也可以使用可选的options object
作为第二个参数。 如果要将匹配的字符串的某些部分替换为另一个字符串,则replace()
很有用。 它以替换string
作为第二个参数。
lowercase()
, uppercase()
, insensitive()
These are used to force a case or ignore the case (for insensitive()
) on the string during validation. Note that case conversion may happen on the original value except when strict mode is enabled.
lowercase()
, uppercase()
, insensitive()
这些在验证期间用于在字符串上强制使用大小写或忽略大小写(对于insensitive()
)。 请注意,除非启用了严格模式,否则大小写转换可能会针对原始值进行。
Here are some string schema examples:
以下是一些字符串模式示例:
// accepts only valid lowercase email addresses
const email = Joi.string().email().lowercase().required();
// accepts alphanumeric strings at least 7 characters long
const password = Joi.string().min(7).alphanum().required();
// accepts an optional US phone number of the format (XXX) XXX-XXXX or XXX-XXX-XXXX
// it defaults to 111-222-3333 if undefined
// Note: that optional is the default behaviour when there is no explicit required()
const phone = Joi.string().regex(/^(\(\d{3}\) |\d{3}-)\d{3}-\d{4}$/).default('111-222-3333');
The Joi.number()
schema makes it more convenient to validate numeric values. It also inherits from the any()
schema, hence all the constraints on the any()
can be used with it. Here are a few more constraints you can use with the number()
schema:
Joi.number()
模式使验证数字值更加方便。 它还从继承any()
模式,因此所有的约束any()
可以与其一起使用。 以下是一些可以与number()
模式一起使用的约束:
min()
, max()
, greater()
, less()
These are used to put a limit on the range of numbers that are considered valid. They each take as first argument, an integer
that specifies the limit.
min()
, max()
, greater()
, less()
这些用于对认为有效的数字范围进行限制。 它们每个都将第一个参数指定为限制的integer
。
precision()
This specifies the maximum number of decimal places for numbers that are considered valid. It takes an integer
as first argument that specifies the maximum decimal places.
precision()
指定被认为有效的数字的最大小数位数。 它以integer
作为第一个参数,该参数指定最大小数位数。
positive()
, negative()
These are used to restrict the valid numbers to only positive or negative numbers respectively.
positive()
, negative()
这些用于将有效数字分别限制为仅正数或负数。
integer()
This allows only integers (no floating point) to be considered as valid values. In non-strict mode, some type conversion may occur on the value if possible.
integer()
这仅允许将整数(无浮点数)视为有效值。 在非严格模式下,如果可能,可能会对值进行某种类型转换。
Here are some examples:
这里有些例子:
// accepts only positive numbers with max of 2 decimal places
const amount = Joi.number().positive().precision(2).required();
// accepts an optional positive integer that must be greater than 13
const age = Joi.number().greater(13).integer().positive();
Date values can be validated using the Joi.date()
schema. It also inherits from the any()
schema, hence all the constraints on the any()
can be used with it. Here are some additional constraints you can use with the date()
schema:
可以使用Joi.date()
模式验证日期值。 它还从继承any()
模式,因此所有的约束any()
可以与其一起使用。 以下是一些可以与date()
模式一起使用的约束:
min()
, max()
These are used to put a limit on the range of dates that are considered valid. They each take as first argument, a valid Date
value that specifies the limit. You can also pass the string now
to use the current date as limit.
min()
和max()
这些用于对被认为有效的日期范围进行限制。 它们每个都以一个有效的Date
值作为第一个参数,该值指定了限制。 你也可以把这个字符串now
使用当前日期为限。
iso()
This requires the date value to be in valid ISO 8601 format.
iso()
这要求日期值必须为有效的ISO 8601格式。
timestamp()
This requires the date value to be a timestamp interval from Unix Time. It takes an optional string
as first argument that determines the type of timestamp to validate with. The allowed values are unix
and javascript
. Defaults to javascript
.
timestamp()
这要求日期值是从Unix Time开始的时间戳间隔。 它使用一个可选string
作为第一个参数,该string
确定要验证的时间戳记的类型。 允许的值为unix
和javascript
。 默认为javascript
。
Here are some examples:
这里有些例子:
// accepts only dates since Jan 1, 2017 in ISO 8601 format
// ISO 8601 format: 2017-01-01
const since = Joi.date().min('1-1-2017').iso().required();
// accepts an optional JavaScript style timestamp (with milliseconds)
const timestamp = Joi.date().timestamp('javascript');
Now that we have a good grasp of basic Joi schemas, we will go a step further to see how we can define more advanced schemas.
既然我们已经掌握了基本的Joi模式,那么我们将进一步研究如何定义更高级的模式。
Most of the data we will be working with are usually JavaScript objects
that have keys
and values
. We should as a result be able to validate a full object with keys and values. Joi provides several ways to specify schema for an object. Let’s see a quick example of a schema that specifies validation rules for an object containing firstname
, lastname
and age
.
我们将使用的大多数数据通常是具有keys
和values
JavaScript objects
。 因此,我们应该能够使用键和值验证完整的对象。 Joi提供了几种方法来指定对象的架构。 让我们看一下模式的快速示例,该模式为包含firstname
, lastname
和age
的对象指定验证规则。
// Method 1: (Object Literal)
const schema1 = {
firstname: Joi.string().required(),
lastname: Joi.string().required(),
age: Joi.number().integer().greater(10)
};
// Method 2: (Using Joi.object())
const schema2 = Joi.object({
firstname: Joi.string().required(),
lastname: Joi.string().required(),
age: Joi.number().integer().greater(10)
});
// Method 3: (Using Joi.object().keys())
const schema3 = Joi.object().keys({
firstname: Joi.string().required(),
lastname: Joi.string().required(),
age: Joi.number().integer().greater(10)
});
Though each of the above methods creates an object schema, there are some subtle differences. It is recommended that you create object schemas using Joi.object()
or Joi.object().keys()
. When using any of these two methods, you can further control the keys that are allowed in the object using some additional constraints, which will not be possible to do using the object literal method. Here are some additional constraints:
尽管上述每种方法都创建了一个对象模式,但是还是有一些细微的差别。 建议您使用Joi.object()
或Joi.object().keys()
创建对象架构。 使用这两种方法中的任何一种时,您都可以使用一些附加约束来进一步控制对象中允许的键,而使用对象文字方法将无法实现这些约束。 以下是一些其他约束:
pattern()
This allows you specify validation rules for keys that match a given pattern. It takes two arguments. The first is a RegExp
literal for the pattern expression unknown keys will be matched against. The second is the Joi schema with which to validate unknown keys that match the pattern. Here is an example:
pattern()
这允许您为匹配给定模式的键指定验证规则。 它有两个参数。 第一个是RegExp
文字,用于模式表达式,将与未知键进行匹配。 第二个是Joi模式,用于验证与模式匹配的未知密钥。 这是一个例子:
// this matches unknown keys like created_at, last_updated_at, etc
// and ensures that they have a date value in the past (in ISO 8601 format)
const schema = Joi.object({}).pattern(/^([a-z]+)(_[a-z]+)*?_at$/, Joi.date().max('now').iso().required())
and()
, nand()
, or()
, xor()
These allow you specify a relationship between one or more keys. Each accept key names or an array of key names with which to establish the relationship. For and()
, if one of the specified keys is present, then the others must also be present. For nand()
, if one of the specified keys is present, then the others cannot be present. For or()
, at least one of the specified keys must be present. For xor()
, at least one of the specified keys must be present but never all the keys together. See the API Reference for usage examples.
and()
, nand()
or()
, xor()
这些允许您指定一个或多个键之间的关系。 每个接受用于建立关系的键名或键名数组。 对于and()
,如果存在指定的键之一,则还必须存在其他键。 对于nand()
,如果存在指定的键之一,则其他键不能存在。 对于or()
,必须存在至少一个指定键。 对于xor()
,必须至少存在一个指定键,但绝不能将所有键都放在一起。 有关用法示例,请参见API参考 。
with()
, without()
These allow you to specify keys that must or must not appear together with a particular key. They both accept two arguments. The first is the key that is used as a reference. The second is an array
of keys that must or must not appear with the reference key. See the API Reference for usage examples.
with()
, without()
这些允许您指定必须或不能与特定键一起出现的键。 他们俩都接受两个论点。 第一个是用作参考的密钥。 第二个是必须或不能与参考键一起显示的键array
。 有关用法示例,请参见API参考 。
When defining validation rules for a key, it is possible to make reference to another key on the same object schema. To specify a reference to a key, you use Joi.ref()
. It takes as first argument the name of the key to reference or a variable in the schema validation context object. Dot notation is allowed for nested key names.
在定义键的验证规则时,可以引用同一对象模式上的另一个键。 要指定对键的引用,请使用Joi.ref()
。 它以要引用的键的名称或模式验证上下文对象中的变量作为第一个参数。 嵌套键名允许使用点符号。
Here is an example of using key references:
这是使用键引用的示例:
// we use a reference to the min key on max
// to always ensure that max is greater than min
const schema1 = Joi.object({
min: Joi.number().integer().positive().required(),
max: Joi.number().integer().greater(Joi.ref('min')).required()
});
// we use a reference to the password key on confirmPassword
// to always ensure that password and confirmPassword are exactly the same
const schema2 = Joi.object({
password: Joi.string().min(7).required().strict(),
confirmPassword: Joi.string().valid(Joi.ref('password')).required().strict()
});
Sometimes, you may want a value to be either a string or number or something else. This is where alternative schemas come into play. You can define alternative schemas using Joi.alternatives()
. It inherits from the any()
schema, so constraints like required()
can be used with it. There are two validation constraints for creating alternative schemas.
有时,您可能希望一个值可以是字符串,数字或其他值。 这是替代方案起作用的地方。 您可以使用Joi.alternatives()
定义其他模式。 它继承自any()
模式,因此可以使用诸如required()
类的约束。 创建替代方案有两个验证约束。
try()
Using try()
we can specify an array of alternative schemas that will be accepted as valid values. It is equivalent to using just an ordinary array literal containing the alternative schemas. Here is an example:
try()
使用try()
我们可以指定一组替代模式,这些模式将被接受为有效值。 这等效于仅使用包含替代模式的普通数组文字。 这是一个例子:
// a positive integer e.g 32
const number = Joi.number().integer().positive().required();
// a numeric string that represents a positive integer e.g '32'
const stringNumber = Joi.string().regex(/^\d+$/).required();
// schema1 and schema2 are equivalent
const schema1 = Joi.alternatives().try(number, stringNumber);
const schema2 = [number, stringNumber];
when()
Using when()
allows you specify alternative schemas based on another key. It takes the key as first argument and an options object
specifying the conditions as second argument. Here is a quick example:
when()
使用when()
允许您基于另一个键指定替代模式。 它使用键作为第一个参数,并使用一个将条件指定为第二个参数的选项object
。 这是一个简单的示例:
// a positive integer e.g 32
const number = Joi.number().integer().positive().required();
// a numeric string that represents a positive integer e.g '32'
const stringNumber = Joi.string().regex(/^\d+$/).required();
// when useString is true, use either number or stringNumeric schema
// otherwise use only number schema
const schema = Joi.object({
value: Joi.alternatives().when('useString', {
is: true,
then: [number, stringNumber],
otherwise: number
}),
useString: Joi.boolean().default(true)
});
We are now up to speed with Joi schemas. We will go ahead to create the validation schemas for our API routes. Create a new file named schemas.js
in the project route directory and add the following code snippet to it.
现在,我们可以使用Joi模式。 我们将继续为我们的API路由创建验证架构。 在项目路径目录中创建一个名为schemas.js
的新文件, schemas.js
其中添加以下代码段。
/* schemas.js */
// load Joi module
const Joi = require('joi');
// accepts name only as letters and converts to uppercase
const name = Joi.string().regex(/^[A-Z]+$/).uppercase();
// accepts a valid UUID v4 string as id
const personID = Joi.string().guid({version: 'uuidv4'});
// accepts ages greater than 6
// value could be in one of these forms: 15, '15', '15y', '15yr', '15yrs'
// all string ages will be replaced to strip off non-digits
const ageSchema = Joi.alternatives().try([
Joi.number().integer().greater(6).required(),
Joi.string().replace(/^([7-9]|[1-9]\d+)(y|yr|yrs)?$/i, '$1').required()
]);
const personDataSchema = Joi.object().keys({
id: personID.required(),
firstname: name,
lastname: name,
fullname: Joi.string().regex(/^[A-Z]+ [A-Z]+$/i).uppercase(),
type: Joi.string().valid('STUDENT', 'TEACHER').uppercase().required(),
sex: Joi.string().valid(['M', 'F', 'MALE', 'FEMALE']).uppercase().required(),
// if type is STUDENT, then age is required
age: Joi.when('type', {
is: 'STUDENT',
then: ageSchema.required(),
otherwise: ageSchema
})
})
// must have only one between firstname and lastname
.xor('firstname', 'fullname')
// firstname and lastname must always appear together
.and('firstname', 'lastname')
// firstname and lastname cannot appear together with fullname
.without('fullname', ['firstname', 'lastname']);
// password and confirmPassword must contain the same value
const authDataSchema = Joi.object({
teacherId: personID.required(),
email: Joi.string().email().lowercase().required(),
password: Joi.string().min(7).required().strict(),
confirmPassword: Joi.string().valid(Joi.ref('password')).required().strict()
});
// cardNumber must be a valid Luhn number
const feesDataSchema = Joi.object({
studentId: personID.required(),
amount: Joi.number().positive().greater(1).precision(2).required(),
cardNumber: Joi.string().creditCard().required(),
completedAt: Joi.date().timestamp().required()
});
// export the schemas
module.exports = {
'/people': personDataSchema,
'/auth/edit': authDataSchema,
'/fees/pay': feesDataSchema
};
Here, we have created schemas for our API endpoints and exported them in an object with the endpoints as keys. A summary of the schemas is as follows:
在这里,我们为API端点创建了架构,并将其导出到以端点为键的对象中。 模式摘要如下:
personDataSchema
personDataSchema
id is required and must be a valid UUID v4 string.
id是必需的,并且必须是有效的UUID v4字符串。
type must be either TEACHER
or STUDENT
. It can be in any case since we are not using strict mode. However, the final output will be converted to uppercase.
type必须是TEACHER
或STUDENT
。 因为我们没有使用严格模式,所以无论如何都可以。 但是,最终输出将转换为大写。
sex must be one of M
, F
, MALE
or FEMALE
. It can be in any case since we are not using strict mode. However, the final output will be converted to uppercase.
sex必须是M
, F
, MALE
或FEMALE
。 因为我们没有使用严格模式,所以无论如何都可以。 但是,最终输出将转换为大写。
age is optional if type is TEACHER
but required if type is STUDENT
. The age can be an integer or a string made up of an integer followed by y
, yr
or yrs
. The age integer must be 7 and above. When the value is a string, the non-integer parts are stripped off in the final value. So possible values can be: 10
, '10'
, '10y'
, '10yr'
, '10yrs'
, etc.
如果type是TEACHER
,则age是可选的 ,但如果类型是STUDENT
则age是必需的 。 年龄可以是整数,也可以是由整数,后跟y
, yr
或yrs
组成的字符串。 年龄整数必须为7以上。 当该值为字符串时,非整数部分将被剥离为最终值。 所以可能的值可以是: 10
, '10'
'10y'
'10yr'
, '10yrs'
等等。
If fullname is specified, then firstname and lastname must be ommitted. If firstname is specified, then lastname must also be specified. One of either fullname or firstname must be specified.
如果指定了全名 ,则必须省略名字和姓氏 。 如果指定了名字 ,则还必须指定姓氏 。 必须指定全名或名字之一 。
authDataSchema
authDataSchema
teacherId is required and must be a valid UUID v4 string.
TeacherId是必需的,并且必须是有效的UUID v4字符串。
email is required and must be a valid email string.
email是必填项,并且必须是有效的电子邮件字符串。
password is required and must be a string with a minimum of 7 characters.
密码是必需的,并且必须是至少7个字符的字符串。
confirmPassword is required and must be the exact same string as password.
ConfirmPassword是必需的,并且必须与password完全相同。
feesDataSchema
feeDataSchema
studentId is required and must be a valid UUID v4 string.
studentId是必需的,并且必须是有效的UUID v4字符串。
amount is required and must be a positive number greater than 1
. If a floating point number is given, the precision is truncated to a maximum of 2
. Since we are not in strict mode, a number with precision greater than 2 will still be accepted but will be truncated.
金额为必填项,且必须为大于1
的正数。 如果给出浮点数,则精度将被截断为最大值2
。 由于我们不在严格模式下,因此精度大于2的数字仍会被接受,但会被截断。
cardNumber is required and must be a valid [Luhn algorithm][luhn-algorithm] compliant number. Every card number is a valid Luhn algorithm compliant number. For test purposes, use 4242424242424242
as card number.
cardNumber为必填项,并且必须为有效的[Luhn算法] [luhn-algorithm]兼容编号。 每个卡号都是有效的Luhn算法兼容号。 出于测试目的,请使用4242424242424242
作为卡号。
completedAt is required and must be a JavaScript timestamp.
completeAt是必需的,并且必须是JavaScript时间戳。
Let’s go ahead to create a middleware that will intercept every request to our API endpoints and validate the request data before handing control over to the route handler.
让我们继续创建一个中间件,该中间件将在将控制权移交给路由处理程序之前拦截对API端点的每个请求并验证请求数据。
Create a new folder named middlewares
in the project root directory and then create a new file named SchemaValidator.js
inside it. The file should contain the following code for our schema validation middleware.
在项目根目录中创建一个名为middlewares
的新文件夹,然后在其中创建一个名为SchemaValidator.js
的新文件。 该文件应包含以下用于我们的模式验证中间件的代码。
/* middlewares/SchemaValidator.js */
const _ = require('lodash');
const Joi = require('joi');
const Schemas = require('../schemas');
module.exports = (useJoiError = false) => {
// useJoiError determines if we should respond with the base Joi error
// boolean: defaults to false
const _useJoiError = _.isBoolean(useJoiError) && useJoiError;
// enabled HTTP methods for request data validation
const _supportedMethods = ['post', 'put'];
// Joi validation options
const _validationOptions = {
abortEarly: false, // abort after the last validation error
allowUnknown: true, // allow unknown keys that will be ignored
stripUnknown: true // remove unknown keys from the validated data
};
// return the validation middleware
return (req, res, next) => {
const route = req.route.path;
const method = req.method.toLowerCase();
if (_.includes(_supportedMethods, method) && _.has(Schemas, route)) {
// get schema for the current route
const _schema = _.get(Schemas, route);
if (_schema) {
// Validate req.body using the schema and validation options
return Joi.validate(req.body, _schema, _validationOptions, (err, data) => {
if (err) {
// Joi Error
const JoiError = {
status: 'failed',
error: {
original: err._object,
// fetch only message and type from each error
details: _.map(err.details, ({message, type}) => ({
message: message.replace(/['"]/g, ''),
type
}))
}
};
// Custom Error
const CustomError = {
status: 'failed',
error: 'Invalid request data. Please review request and try again.'
};
// Send back the JSON error response
res.status(422).json(_useJoiError ? JoiError : CustomError);
} else {
// Replace req.body with the data after Joi validation
req.body = data;
next();
}
});
}
}
next();
};
};
Here, we have loaded Lodash alongside Joi and our schemas into our middleware module. We are also exporting a factory function that accepts one argument and returns the schema validation middleware. The argument to the factory function is a boolean
value which when true
, indicates that Joi validation errors should be used, otherwise a custom generic error is used for errors in the middleware. It defaults to false
if not specified or a non-boolean value is given.
在这里,我们将Lodash和Joi以及我们的模式一起加载到了中间件模块中。 我们还将导出一个接受一个参数并返回架构验证中间件的工厂函数。 factory函数的参数是一个boolean
值,当为true
,指示应使用Joi验证错误,否则,将自定义通用错误用于中间件中的错误。 如果未指定或给定非布尔值,则默认为false
。
We have also defined the middleware to only handle POST
and PUT
requests. Every other request methods will be skipped by the middleware. You can also configure it if you wish, to add other methods like DELETE
that can take a request body.
我们还将中间件定义为仅处理POST
和PUT
请求。 中间件将跳过所有其他请求方法。 您也可以根据需要对其进行配置,以添加其他方法(如DELETE
来接收请求主体。
The middleware uses the schema that matches the current route key from the Schemas
object we defined earlier to validate the request data. The validation is done using the Joi.validate()
method, with the following signature:
中间件使用与我们之前定义的Schemas
对象中的当前路由键匹配的Schemas
来验证请求数据。 使用具有以下签名的Joi.validate()
方法完成验证:
data is the data to validate which in our case is req.body
.
data是要验证的数据,在我们的例子中是req.body
。
schema is the schema with which to validate the data.
schema是用于验证数据的架构。
options is an object
that specifies the validation options. Here are the validation options we used:
options是一个指定验证选项的object
。 这是我们使用的验证选项:
callback is a callback function
that will be called after validation. It takes two arguments. The first is the Joi ValidationError
object if there were validation errors or null
if no errors. The second argument, is the output data.
callback是在验证后将被调用的回调function
。 它有两个参数。 第一个是Joi ValidationError
对象(如果存在验证错误),如果没有错误则为null
。 第二个参数是输出数据。
Finally, in the callback function of Joi.validate()
we return the formatted error as a JSON response with the 422
HTTP status code if there are errors, or we simply overwrite req.body
with the validation output data and then pass control over to the next middleware.
最后,在Joi.validate()
的回调函数中,如果有错误,我们将格式错误以422
HTTP状态代码的形式返回为JSON响应,或者我们req.body
验证输出数据覆盖req.body
,然后将控制权传递给下一个中间件。
Now we can go ahead to use the middleware on our routes. Modify the routes.js
file as follows:
现在我们可以继续在路由中使用中间件了。 修改routes.js
文件,如下所示:
/* routes.js */
const express = require('express');
const router = express.Router();
const SchemaValidator = require('./middlewares/SchemaValidator');
// We are using the formatted Joi Validation error
// Pass false as argument to use a generic error
const validateRequest = SchemaValidator(true);
// generic route handler
const genericHandler = (req, res, next) => {
res.json({
status: 'success',
data: req.body
});
};
// create a new teacher or student
router.post('/people', validateRequest, genericHandler);
// change auth credentials for teachers
router.post('/auth/edit', validateRequest, genericHandler);
// accept fee payments for students
router.post('/fees/pay', validateRequest, genericHandler);
module.exports = router;
Let’s go ahead and run our app to test what we’ve done. These are sample test data you can use to test the endpoints. You can edit them however you wish. For generating UUIDv4 strings, you can use the Node UUID module or an online UUID Generator.
让我们继续运行我们的应用程序以测试我们所做的事情。 这些是可用于测试端点的示例测试数据。 您可以根据需要编辑它们。 要生成UUIDv4字符串,可以使用Node UUID模块或在线UUID Generator 。
POST /people
POST /people
{
"id": "a967f52a-6aa5-401d-b760-35eef7c68b32",
"type": "Student",
"firstname": "John",
"lastname": "Doe",
"sex": "male",
"age": "12yrs"
}
POST /auth/edit
POST /auth/edit
{
"teacherId": "e3464323-22c1-4e31-9ac5-9bde207d61d2",
"email": "email@domain.com",
"password": "password",
"confirmPassword": "password"
}
POST /fees/pay
POST /fees/pay
{
"studentId": "c77b8a6e-9d26-428a-9df1-e852473f886f",
"amount": 134.9875,
"cardNumber": "4242424242424242",
"completedAt": 1512064288409
}
Here are screenshots of testing the API endpoints on Postman.
这是在Postman上测试API端点的屏幕截图。
In this tutorial, we’ve seen how we can create schemas for validating a collection of data using Joi and how to handle request data validation using a custom schema validation middleware on our HTTP request pipeline. For a complete code sample of this tutorial, check out the joi-schema-validation-sourcecode repository on Github.
在本教程中,我们了解了如何使用Joi创建用于验证数据集合的模式 ,以及如何在HTTP请求管道上使用自定义模式验证中间件处理请求数据验证。 有关本教程的完整代码示例,请查看Github上的joi-schema-validation-sourcecode存储库。
翻译自: https://www.digitalocean.com/community/tutorials/node-api-schema-validation-with-joi
joi 参数验证