验证
介绍
Goyave 提供了一种强大而简单的方法来验证所有传入数据,无论其类型或格式如何,这得益于大量的验证器。验证过程确保用户提供的数据符合服务器的期望,并使将原始动态和无类型数据转换为 DTO 变得安全。简而言之,验证系统提供了类型安全性,并有助于确保业务约束。
验证器可以改变原始数据并将其转换为期望的类型。这意味着当您验证一个字段为数字时,如果验证通过,您可以确保在控制器处理器中使用的数据是 float64
。如果您正在验证 IP,您将获得一个 net.IP
对象。如果您正在验证 int8
,如果提供的数字不是整数或太大无法适应 int8
,请求将被拒绝。
传入请求使用规则集进行验证,这些规则集将验证器与请求中每个期望的字段关联起来。
验证是自动的。您只需定义一个规则集并将其分配给路由。当验证不通过时,请求将停止,验证错误消息将使用正确的语言作为响应发送。验证失败的 HTTP 响应代码是 422 Unprocessable Entity
。
重要
不要忘记添加请求解析中间件。否则将没有数据可验证。
import "goyave.dev/goyave/v5/middleware/parse"
router.GlobalMiddleware(&parse.Middleware{})
生成并返回给客户端的验证错误消息丰富且结构化,以实现最大粒度。以下是一个示例:
{
"error": {
"body": {
"fields": {
"user": {
"fields": {
"name": {
"errors": ["名称不能超过 255 个字符。"]
},
"roles": {
"errors": ["角色不能超过 2 项。"],
"elements": {
"2": {
"errors": [
"角色元素必须具有以下值之一:viewer、admin、moderator。"
]
}
}
}
},
"errors": ["用户必须是一个对象"]
}
}
},
"query": {
"fields": {
"group": {
"errors": ["组 ID 是必需的。"]
}
}
}
}
}
规则集
生成规则集的函数在与实现相关处理器的控制器相同的包中的名为 validation.go
的文件中定义。它们使用与将使用的控制器处理器相同的名称,并附加前缀 Request
。例如,Create
处理器的规则集将命名为 CreateRequest
。如果一个规则集函数可用于多个处理器,请考虑使用适合所有它们的名称。
定义规则集时,建议向验证包导入添加短别名(例如 v
)以缩短语法:
import (
"goyave.dev/goyave/v5"
v "goyave.dev/goyave/v5/validation"
)
规则集函数接收请求作为参数。它可以使用请求的信息动态更改生成的规则集。
// http/controller/product/validation.go
package product
import (
"goyave.dev/goyave/v5"
v "goyave.dev/goyave/v5/validation"
)
func IndexRequest(_ *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Object()}},
{Path: "page", Rules: v.List{v.Int(), v.Min(1)}},
{Path: "perPage", Rules: v.List{v.Int(), v.Between(1, 100)}},
}
}
如您所见,规则集将字段列表(由路径标识)与验证器列表关联起来。验证器实现接口 validation.Validator
。
TIP
您可以在 go.pkg.dev 参考上找到现有验证器的完整列表。
如果需要,您可以在规则集函数上向控制器添加接收器。这将允许您从这些函数访问控制器依赖的服务。当您使用应理想情况下在仓库中定义的具有数据库范围的验证器时,这特别有用。
// http/controller/product/validation.go
func (ctrl *Controller) CreateRequest(_ *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Object()}},
{Path: "name", Rules: v.List{
v.Required(),
v.String(),
v.Unique(ctrl.UserService.UniqueScope("name")),
}},
//...
}
}
// service/product/product.go
func (s *Service) UniqueScope(column string) func(db *gorm.DB, val any) *gorm.DB {
return s.repository.UniqueScope(column)
}
// database/repository/product.go
func (r *Product) UniqueScope(column string) func(db *gorm.DB, val any) *gorm.DB {
return func(db *gorm.DB, val any) *gorm.DB {
return db.Table(model.Product{}.TableName()).Where(column, val)
}
}
DANGER
规则集及其验证器仅用于单次使用。它们不应被重用或并发使用。
最后,您可以将规则集函数应用于路由。规则集函数将为这些路由上的每个新传入请求调用,生成新规则集。
func (ctrl *Controller) RegisterRoutes(router *goyave.Router) {
subrouter := router.Subrouter("/products")
subrouter.Get("/", ctrl.Index).ValidateQuery(IndexRequest)
subrouter.Post("/", ctrl.Create).ValidateBody(CreateRequest)
// 或如果使用接收器:
// subrouter.Get("/", ctrl.Index).ValidateQuery(ctrl.IndexRequest)
// subrouter.Post("/", ctrl.Create).ValidateBody(ctrl.CreateRequest)
}
INFO
验证查询时,应始终期望根元素是一个对象。
验证过程
首先,validation.RuleSet
被转换为 validation.Rules
,这是一种更易于使用的格式,并以这样一种方式构造数组字段,使得可以递归验证它们。路径也使用 Goyave 的 walk
库进行解析。
验证时,每个路径将按定义顺序一个一个检查。验证过程将探索正在验证的原始数据以找到与给定路径对应的元素。然后,所有验证器将按注册顺序在此字段上执行。
在我们上面的 IndexRequest
示例中:
- 我们将首先检查
v.CurrentElement
,它对应于正在验证的数据中的根元素。然后我们检查它是否是一个对象。 - 然后,我们探索对象并查看是否可以找到名为 "page" 的字段。我们首先检查它是否为整数,并在检查其值是否大于或等于
1
(v.Min(1)
)之前确保其类型为int
。 - 该过程继续用于规则集中的所有剩余字段。
INFO
- 如果一个字段未通过验证,过程不会停止,其他字段也将被检查。
- 请求的
context.Context
自动注入将传递给验证器的数据库实例中。 - 如果正在验证的数据不是 JSON,则期望为数组但不是数组的字段将转换为具有单个值的数组。这对于查询和任何 url 编码的数据很有用。此行为由
ConvertSingleValueArrays
验证选项控制。 validation.Rules
被添加到request.Extra
中,键为goyave.ExtraQueryValidationRules
和goyave.ExtraBodyValidationRules
。- 如果验证不通过,错误将添加到
request.Extra
中,键为goyave.ExtraQueryValidationError
和goyave.ExtraBodyValidationRules
。响应状态设置为422 Unprocessable Entity
,相关的状态处理器格式化并将其写入响应。这意味着可以通过替换代码422
的状态处理器来自定义错误响应格式。 struct
被视为最终值,不会作为字段路径的一部分进行探索。
结构验证
WARNING
- 正在验证的数据的根元素可以是任何类型。因此,始终建议使用路径
validation.CurrentElement
验证字段。 - 大多数时候,您希望根元素是必需的。具有空正文且根元素非必需的请求将通过验证。在必填字段部分中了解更多信息。
对象
您可以使用点分隔表示法验证对象。例如,如果您想验证以下请求:
{
"user": {
"name": "Josh",
"email": "josh@example.org"
}
}
您将使用以下规则集:
func CreateRequest(_ *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Object()}},
{Path: "user", Rules: v.List{v.Required(), v.Object()}},
{Path: "user.name", Rules: v.List{v.Required(), v.String(), v.Between(3, 50)}},
{Path: "user.email", Rules: v.List{v.Required(), v.Email()}},
}
}
您可以使用通配符 *
来匹配对象的所有属性,而无需知道它们的名称。以下规则集将确保 "object" 的所有属性都是具有 "id" 属性的对象。
func WildcardValidation(r *goyave.Request) v.RuleSet {
return v.RuleSet{
"object.*": v.List{v.Object()},
"object.*.id": v.List{v.Required(), v.Int()},
}
}
数组
验证数组同样简单。您可以使用 []
语法来标识数组元素:
func ArrayRequest(_ *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Object()}},
{Path: "array", Rules: v.List{v.Required(), v.Array(), v.Between(1, 5)}},
{Path: "array[]", Rules: v.List{v.Email(), v.Max(255)}},
}
}
在此示例中,我们正在验证一个包含一到五个电子邮件地址的数组,这些地址不能超过 255 个字符。验证数组元素时,所有元素都必须通过验证。
如果数组的所有元素具有相同的类型,数组将被转换为正确的类型。如果数组为空,则不会转换。例如:
// 原始数组,其元素验证为 `v.Int()`
[]any{1, 2.0, uint(3)}
// 将首先转换为:
[]any{1, 2, 3}
// 然后转换为
[]int{1, 2, 3}
WARNING
字段验证顺序有一个例外。数组元素被递归验证。因此,数组元素总是在其父数组之前验证,无论您在规则集中在之前还是之后定义它们。
以这种方式验证数组很重要,以便如果所有元素通过验证,数组类型可以转换为期望的类型。
根元素可以是任何东西,不一定是对象。这意味着您可以将数组作为根元素:
func ArrayRequest(_ *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Array()}},
{Path: "[]", Rules: v.List{v.Email(), v.Max(255)}},
}
}
N 维数组
您可以验证 n 维数组。
func ArrayRequest(_ *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Object()}},
{Path: "values", Rules: v.List{v.Required(), v.Array()}},
{Path: "values[]", Rules: v.List{v.Array(), v.Max(3)}},
{Path: "values[][]", Rules: v.List{v.Array()}},
{Path: "values[][][]", Rules: v.List{v.Float64(), v.Max(4)}},
}
}
在此示例中,我们正在验证一个数值的三维数组。第一维必须由大小为 3 或更小的数组组成。第二维必须由包含数字的数组组成。第三维必须是小于或等于 4 的数字。以下 JSON 输入通过验证:
{
"array": [
[
[0.5, 1.42],
[0.6, 4, 3]
],
[[0.6, 1.43], [], [2]]
]
}
对象数组
您可以使用相同的点分隔语法验证数组内的对象:
func PeopleRequest(_ *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Object()}},
{Path: "people", Rules: v.List{v.Required(), v.Array()}},
{Path: "people[]", Rules: v.List{v.Object()}},
{Path: "people[].name", Rules: v.List{v.Required(), v.String(), v.Max(255)}},
{Path: "people[].email", Rules: v.List{v.Required(), v.Email(), v.Max(255)}},
}
}
在此示例中,我们正在验证一个人员数组。以下 JSON 输入通过验证:
{
"people": [
{
"name": "John",
"email": "john@example.org"
},
{
"name": "Zoe",
"email": "zoe@example.com"
}
]
}
转义特殊字符
您可以使用反斜杠转义路径的特殊字符(.
, [
, ]
, *
, \
)。
object.\*
example\.org
object.field\[text\]
abc\[\]def
path\\to\\element
必填字段、可空字段和未定义字段
- 如果字段是必需的(
validation.Required()
验证器),字段必须存在于请求中。 - 如果字段是可空的(
validation.Nullable()
验证器),字段可以具有nil
值。 - 可空字段可以是必需的。这意味着必须提供值并存在于请求中。
nil
是一个值,与字段未定义不同。如果字段的路径与请求数据中的任何内容都不匹配,则字段未定义。- 如果请求包含具有
nil
/null
值的字段,并且该字段不是可空的,则字段将从请求中完全移除。这意味着如果字段未标记为可空的,则具有nil
值的字段被视为未定义。nil
数组元素不会被移除,即使它们未标记为可空的。
- 如果必需的字段未定义,后续验证器将不会执行。
- 如果必需的字段具有未定义的父级,其验证将完全跳过。
- 确保所有父级都定义了验证器,以确保它们具有期望的类型。
- 您可以在对象内部要求字段,而不使对象本身成为必需的。这意味着子字段仅在其父级存在时才必需。
validation.Required()
在数组元素上无效,除非数组为空。如果数组为空且具有必需元素,则返回的验证错误将针对索引-1
。 prefer 在数组上使用大小验证器来检查其包含的元素数量。
WARNING
未在规则集中列出的字段不会被验证,但它仍然可以存在于正文中,请验证所有您期望的字段,无一例外。
条件要求
使用 validation.RequiredIf()
,您可以动态地使字段成为必需的。当且仅当指定的回调返回 true
时,字段将被设置为必需的。
func BookRequest(request *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Object()}},
{Path: "author_id", Rules: v.List{v.RequiredIf(func(ctx *v.Context) bool {
return request.RouteParams["authorName"] == "anonymous"
}), v.Int()}},
}
}
INFO
无论为正在验证的字段定义验证器的顺序如何,RequiredIf
的回调将始终首先执行,以便可以如上所述检查存在标准。
因此,回调执行两次。一次用于存在标准,一次用于实际验证。
组合
组合可以通过多次重用相同的规则集函数而不复制它们来帮助您减少冗余。要组合规则集,使用 validation.RuleSet
作为 Rules
而不是 validation.List
:
func CreateAuthorRequest(request *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Object()}},
{Path: "name", Rules: v.List{v.Required(), v.String()}},
{Path: "bio", Rules: v.List{v.Required(), v.String()}},
{Path: "books", Rules: v.List{v.Required(), v.Array()}},
{Path: "books[]", Rules: CreateBookRequest(request)},
}
}
func CreateBookRequest(_ *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Object()}},
{Path: "title", Rules: v.List{v.Required(), v.String()}},
{Path: "price", Rules: v.List{v.Required(), v.Float64()}},
}
}
// 结果为:
func CreateAuthorAndBooksRequest(request *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Object()}},
{Path: "name", Rules: v.List{v.Required(), v.String()}},
{Path: "bio", Rules: v.List{v.Required(), v.String()}},
{Path: "books", Rules: v.List{v.Required(), v.Array()}},
{Path: "books[]", Rules: v.List{v.Required(), v.Object()}},
{Path: "books[].title", Rules: v.List{v.Required(), v.String()}},
{Path: "books[].price", Rules: v.List{v.Required(), v.Float64()}},
}
}
TIP
- 您可以嵌套组合任意多次,只要不创建无限递归。
- 您可以在当前元素上组合。
相对性
根数据相对于规则集是相对的。在我们之前的示例中,这意味着对于 CreateBookRequest()
返回的规则集,看起来我们正在验证单个书籍,并且根元素是书籍对象。当规则需要比较其他字段时,这很有用,例如 validation.LowerThan("otherField")
。
如果我们像这样修改上面的示例:
func CreateBookRequest(_ *goyave.Request) v.RuleSet {
return v.RuleSet{
{Path: v.CurrentElement, Rules: v.List{v.Required(), v.Object()}},
{Path: "title", Rules: v.List{v.Required(), v.String()}},
{Path: "minPrice", Rules: v.List{v.Required(), v.Float64()}},
{Path: "price", Rules: v.List{v.Required(), v.Float64(), v.GreaterThanEqual("minPrice")}},
}
}
当使用 CreateAuthorRequest()
时,GreaterThanEqual()
验证器将比较 books[].price
的值与 books[].minPrice
,考虑数组索引。
当直接使用 CreateBookRequest()
时,GreaterThanEqual()
验证器将比较 price
的值与 minPrice
。
TIP
因此,组合也可以用于功能原因,而不仅仅是代码可重用性。
手动验证
您可能需要手动验证数据,或验证不是来自 Goyave 请求的数据。只要可以探索此数据,您就可以使用相同的验证系统。
func (ctrl *Controller) Handler(response *goyave.Response, request *goyave.Request) {
var data any = map[string]any{
//...
}
ruleSet := validation.RuleSet{
//...
}
opt := &validation.Options{
Context: request.Context(),
Data: data,
Rules: ruleSet,
Now: request.Now,
ConvertSingleValueArrays: false,
Language: request.Lang,
DB: ctrl.DB().WithContext(request.Context()),
Config: ctrl.Config(),
Logger: ctrl.Logger(),
Extra: map[any]any{},
}
validationErrors, errs := validation.Validate(opt)
if errs != nil {
response.Error(errs)
return
}
if validationErrors != nil {
// 存在验证错误
}
// 验证可能已将根元素转换为另一种类型。
data = opt.Data
//...
}
INFO
validation.Validate()
返回的第二个值是在验证期间发生的错误切片。这些错误不是验证错误,而是在验证器无法正确执行时引发的错误。例如,如果使用数据库的验证器生成了数据库错误。
自定义验证器
如果可用的验证规则都不满足您的需求,您可以实现自定义验证规则。为此,创建一个新文件 http/validation/<validator_name>.go
,在其中定义您的自定义规则。每个验证器应有自己的文件,其名称与验证器的名称相同。
TIP
导入自定义规则时,包名可能会让读者困惑,因为它与框架的包相同。它也很长。因此,建议也向导入添加短别名(例如 vv
)以缩短语法:
import vv "my-project/http/validation"
所有验证器必须实现 validation.Validator
接口。为此,它们必须与 validation.BaseValidator
组合,并至少实现 Validate()
和 Name()
方法。其他方法是可选的,由 validation.BaseValidator
定义默认值。
// http/validation/custom.go
package validation
import "goyave.dev/goyave/v5/validation"
type CustomValidator struct {
validation.BaseValidator
}
func (v *CustomValidator) Validate(ctx *validation.Context) bool {
// ...
return true
}
func (v *CustomValidator) Name() string {
return "custom"
}
func Custom() *CustomValidator {
return &CustomValidator{}
}
如果您的规则修改正在验证的字段的值,它必须重新分配 ctx.Value
。这对于转换规则(如日期,将输入数据转换为 time.Time
)很有用。转换数据的验证器大多数时候是类型验证器,意味着它应实现 IsType() bool
并返回 true
。
func (v *CustomValidator) Validate(ctx *validation.Context) bool {
//...
ctx.Value = "新值"
return true
}
func (v *CustomValidator) IsType() bool {
return true
}
如果您的验证器支持许多不同类型的原始数据(数字、字符串、数组、对象和/或文件),并且应根据值的类型具有不同的验证错误消息,那么您的验证器应实现 IsTypeDependent() bool
并返回 true
。在下面的本地化部分中了解更多信息。
func (v *CustomValidator) IsTypeDependent() bool {
return true
}
如果此字段的列表中存在类型验证器,它将用作字段期望类型的参考。否则,使用实际字段类型。
如果您的验证器使用数据库或任何其他可能生成 error
的操作,请使用 validation.Context.AddError()
并 return false
:
func (v *CustomValidator) Validate(ctx *validation.Context) bool {
if ctx.Invalid {
return true
}
count := int64(0)
err := v.DB().Table("table_name").Where("id", ctx.Value).Count(&count).Error
if err != nil {
ctx.AddError(errors.New(err))
return false
}
return count > 0
}
INFO
- 如果验证是从内置验证中间件开始的,请求对象将从
Extra
中可用,键为validation.ExtraRequest{}
。 validation.Context.Invalid
是一个只读字段,可用于跳过验证器,如果链中先前的验证器返回false
。
func (v *CustomValidator) Validate(ctx *validation.Context) bool {
if ctx.Invalid {
// 跳过而不返回验证错误消息
// 这是安全的,因为先前的验证器已经返回 false
return true
}
// ...
}
- 由于与
validation.BaseValidator
的组合,所有验证器都可以访问在validation.Options
中传递的数据库、配置、语言和记录器。如果验证是从内置验证中间件开始的,这些值将自动可用。
嵌套验证
验证器可以进行所谓的嵌套验证。这意味着它们生成规则集并使用它们通过手动验证来验证复杂字段。然后它们可以将验证错误与更高级别的错误合并。
示例:
func (v *CustomValidator) Validate(ctx *validation.Context) bool {
ruleSet := validation.RuleSet{
//...
}
opt := &validation.Options{
Data: ctx.Value,
Rules: ruleSet,
Now: ctx.Now,
ConvertSingleValueArrays: false,
Language: v.Lang(),
DB: v.DB(),
Config: v.Config(),
Logger: v.Logger(),
}
validationErrors, errs := validation.Validate(opt)
if errs != nil {
ctx.AddError(errs...)
return false
}
if validationErrors != nil {
ctx.AddValidationErrors(ctx.Path(), validationErrors)
return false
}
return true
}
这里,validationErrors
将与正在验证的字段路径处的父验证错误合并。
例如,如果字段是 book.author
并且 validationErrors
包含以下内容:
{
"fields": {
"name": {
"errors": ["名称必须是字符串。"]
}
},
"errors": ["作者包含无效信息。"]
}
然后合并后父验证错误将如下所示:
{
"fields": {
"book": {
"fields": {
"author": {
"fields": {
"name": {
"errors": ["名称必须是字符串。"]
}
},
"errors": ["作者包含无效信息。"]
}
}
}
}
}
缺失的路径段将自动添加(如果缺失)。结果验证错误中已存在的字段不会被覆盖,新值将合并到其中。
您还可以使用 validation.Context.AddValidationError()
按消息添加单个错误:
func (v *CustomValidator) Validate(ctx *validation.Context) bool {
//...
ctx.AddValidationError(ctx.Path(), v.Lang().Get("customErrorMessage"))
return false
}
TIP
可以使用 walk.Path
API 操作和更改路径。
数组元素批量验证
如果您的验证器旨在验证数组,并且在数组级别而不是元素级别执行更高效,您还可以在特定数组元素索引上添加错误。
这在需要针对数据库验证数组时很有用。执行单个 SQL 查询比每个元素执行一个更高效。
func (v *CustomValidator) Validate(ctx *validation.Context) bool {
//...
ctx.AddArrayElementValidationErrors(1, 4, 6)
return false
}
INFO
在此示例中,正在验证的数组字段中索引 1、4 和 6 处的元素将被标记为无效,并且验证器的关联错误消息将添加到结果验证错误中。
本地化
当验证器返回 false
时作为验证错误消息返回的字符串在 resources/lang/<language_name>/rules.json
中定义。条目名称是规则的名称(规则的 Name()
方法返回的值)。
{
"custom_format": ":field 格式无效。"
}
当相关字段是数组元素时,使用的条目将是:validator_name.element
。
{
"custom_format.element": ":field 元素格式无效。"
}
对于类型依赖验证器,值的类型也被定义,并允许您根据字段的类型返回不同的消息:
{
"size.string": ":field 必须正好是 :value 个字符长。",
"size.numeric": ":field 必须正好是 :value。",
"size.array": ":field 必须正好包含 :value 项。",
"size.file": ":field 必须正好是 :value KiB。",
"size.object": ":field 必须正好有 :value 个字段。",
"size.string.element": ":field 元素必须正好是 :value 个字符长。",
"size.numeric.element": ":field 元素必须正好是 :value。",
"size.array.element": ":field 元素必须正好包含 :value 项。",
"size.object.element": ":field 元素必须正好有 :value 个字段。"
}
占位符
验证消息可以使用占位符在验证错误消息中注入动态值。每个验证器通过 MessagePlaceholders()
方法定义自己的占位符:
// MessagePlaceholders 返回 ":min" 和 ":max" 占位符。
func (v *BetweenValidator) MessagePlaceholders(_ *Context) []string {
return []string{
":min", fmt.Sprintf("%v", v.Min),
":max", fmt.Sprintf("%v", v.Max),
}
}
使用这些占位符,消息 :field 必须在 :min 和 :max 个字符之间。
将更改为 名称必须在 1 和 255 个字符之间。
。
:field
占位符默认由翻译的字段名替换。字段名翻译在 resources/lang/<language_name>/fields.json
中定义。因此,如果您有一个字段 authorId
,您可以使其在消息中显示为 author ID
,如下所示:
{
"authorId": "author ID"
}
如果没有可用的字段名翻译,则使用原始字段名而不带路径前缀。例如,如果字段的路径是 book.author.name
,字段名将仅为 name
。对于数组元素,使用的名称是数组的名称。
覆盖消息
您可以使用 v.WithMessage()
覆盖任何验证器的验证错误消息。当验证器不通过时,将使用给定语言条目的翻译而不是默认的。
验证器返回的原始占位符仍用于渲染消息。例如,如果您覆盖 Min
验证器的消息,您将能够在自定义消息中使用 :min
占位符。
覆盖消息时,不添加类型依赖和 "element" 后缀。
func (ctrl *Controller) UpdateRequest(_ *goyave.Request) v.RuleSet {
return v.RuleSet{
//...
{Path: "contents", Rules: v.List{v.WithMessage(v.String(), "validation.rules.customMessage"), v.Min(10)}},
}
}