validator icon indicating copy to clipboard operation
validator copied to clipboard

How to validate struct without struct tag?

Open chenyanchen opened this issue 3 years ago • 6 comments

  • [x] I have looked at the documentation here first?
  • [x] I have looked at the examples provided that may showcase my question here?

Package version v10:

import "github.com/go-playground/validator/v10"

Issue, Question or Enhancement:

I made some gRPC services with protobuf, the generated files has no struct tags. How to validate a struct without struct tags?

Maybe gogo/protobuf can add some struct tags into the generation files, but this is no matter to use a new package for the complex method.

I find the same problem #722 not same pain spot and closed.

There is some simply method to solove this problem?

Code sample, to showcase or reproduce:

.proto file

message SearchRequest {
  string RequestID = 1;
  uint64 UserID = 2;
  string Email = 3;
  repeated string Keywords = 4;
}

Generated structure:

type SearchRequest struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	RequestID string   `protobuf:"bytes,1,opt,name=RequestID,proto3" json:"RequestID,omitempty"`
	UserID    uint64   `protobuf:"varint,2,opt,name=UserID,proto3" json:"UserID,omitempty"`
	Email     string   `protobuf:"bytes,3,opt,name=Email,proto3" json:"Email,omitempty"`
	Keywords  []string `protobuf:"bytes,4,rep,name=Keywords,proto3" json:"Keywords,omitempty"`
}

I want validate this structure equal this:

type SearchRequest struct {
	RequestID string `validate:"required"`
	UserID    uint64
	Email     string   `validate:"omitempty,email"`
	Keywords  []string `validate:"dive,required"`
}

In the proto generated files, there is no tags, so I expect a method like RegisterStructMap, the map[string]string is map[FieldName]tag.

func main() {
	v := validator.New()
	v.RegisterStructMap(
		proto.SearchRequest{}, map[string]string{
			"RequestID": "required",
			"Email":     "omitempty,email",
			"Keywords":  "dive,required",
		},
	)
	req := &proto.SearchRequest{
		Keywords: []string{""},
	}
	if err := v.Struct(req); err != nil {
		fmt.Println(err)
	}
}
// Output:
//     Key: 'SearchRequest.RequestID' Error:Field validation for 'RequestID' failed on the 'required' tag
//     Key: 'SearchRequest.Keywords[0]' Error:Field validation for 'Keywords[0]' failed on the 'required' tag

chenyanchen avatar Nov 11 '21 07:11 chenyanchen

I find a way to validate structure without struct tag, is this the current best way to validate no tag structure?

This not my expect way to validate no tag structure.

package main

import (
	"fmt"
	"testing"

	"github.com/go-playground/validator/v10"
)

var (
	v       = validator.New()
	withTag = WithTag{
		RequestID: "",
		Email:     "email",
		Phone:     "123456",
		Keywords:  []string{""},
	}
	noTag = NoTag(withTag)
)

func main() {
	v.RegisterStructValidation(noTagValidateFunc, NoTag{})

	fmt.Println(v.Struct(withTag))
	fmt.Println(v.Struct(noTag))
}

type WithTag struct {
	RequestID string   `validate:"required"`
	Email     string   `validate:"omitempty,email"`
	Phone     string   `validate:"omitempty,e164"`
	Keywords  []string `validate:"dive,required"`
}

type NoTag struct {
	RequestID string
	Email     string
	Phone     string
	Keywords  []string
}

func noTagValidateFunc(sl validator.StructLevel) {
	s := sl.Current().Interface().(NoTag)
	v := sl.Validator()
	if err := v.Var(s.RequestID, "required"); err != nil {
		sl.ReportValidationErrors("RequiredID", "RequiredID", err.(validator.ValidationErrors))
	}
	if err := v.Var(s.Email, "omitempty,email"); err != nil {
		sl.ReportValidationErrors("Email", "Email", err.(validator.ValidationErrors))
	}
	if err := v.Var(s.Phone, "omitempty,e164"); err != nil {
		sl.ReportValidationErrors("Phone", "Phone", err.(validator.ValidationErrors))
	}
	if err := v.Var(s.Keywords, "dive,required"); err != nil {
		sl.ReportValidationErrors("Keywords", "Keywords", err.(validator.ValidationErrors))
	}
}

func BenchmarkWithTag(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = v.Struct(withTag)
	}
}

func BenchmarkNoTag(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = v.Struct(noTag)
	}
}

And benchmark result:

$ go test -bench=. -benchmem .
goos: darwin
goarch: amd64
pkg: github.com/chenyanchen/test/validator
cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
BenchmarkWithTag-8        515580              2031 ns/op             998 B/op         17 allocs/op
BenchmarkNoTag-8         6123906               200.5 ns/op            80 B/op          1 allocs/op
PASS
ok      github.com/chenyanchen/test/validator   3.147s

chenyanchen avatar Dec 08 '21 08:12 chenyanchen

Currently, there is nothing that would do this, except the painful way of using the Var function.

Maybe it's worth exploring a path, where we can pass in validation rules as an argument to the validation function as we do with map validation https://pkg.go.dev/github.com/go-playground/validator/v10#Validate.ValidateMap

Would look something like

func (v *Validate) StructWithRules(s interface{}, rules map[string]interface{}) error 

zemzale avatar Jan 15 '22 17:01 zemzale

Currently, there is nothing that would do this, except the painful way of using the Var function.

Maybe it's worth exploring a path, where we can pass in validation rules as an argument to the validation function as we do with map validation https://pkg.go.dev/github.com/go-playground/validator/v10#Validate.ValidateMap

Would look something like

func (v *Validate) StructWithRules(s interface{}, rules map[string]interface{}) error 

I suggest RegisterStructValidationInMap.

// rules is map[FieldName]Tag
func (v *Validate) RegisterStructValidationInMap(rules map[string]string, types ...interface{})

chenyanchen avatar Jan 26 '22 03:01 chenyanchen

The way I would approach this problem would be to use two different structs for the different purposes. I would use the structs generated by protoc for my gRPC and Protobuf use cases (receiving data over the wire), map the data from the Protobuf struct to a new struct I create with the validation tags, perform the validation on this new struct with the data filled in, and use the result of that validation to decide what to do with the data in the Protobuf struct received over the wire.

mattwelke avatar Mar 22 '22 17:03 mattwelke

Currently, there is nothing that would do this, except the painful way of using the Var function.

Maybe it's worth exploring a path, where we can pass in validation rules as an argument to the validation function as we do with map validation https://pkg.go.dev/github.com/go-playground/validator/v10#Validate.ValidateMap

Would look something like

func (v *Validate) StructWithRules(s interface{}, rules map[string]interface{}) error 

Currently, there is nothing that would do this, except the painful way of using the Var function. Maybe it's worth exploring a path, where we can pass in validation rules as an argument to the validation function as we do with map validation https://pkg.go.dev/github.com/go-playground/validator/v10#Validate.ValidateMap Would look something like

func (v *Validate) StructWithRules(s interface{}, rules map[string]interface{}) error 

I suggest RegisterStructValidationInMap.

// rules is map[FieldName]Tag
func (v *Validate) RegisterStructValidationInMap(rules map[string]string, types ...interface{})

I archieved this function,PR:#934

leoliang1997 avatar Apr 21 '22 03:04 leoliang1997

The way I would approach this problem would be to use two different structs for the different purposes. I would use the structs generated by protoc for my gRPC and Protobuf use cases (receiving data over the wire), map the data from the Protobuf struct to a new struct I create with the validation tags, perform the validation on this new struct with the data filled in, and use the result of that validation to decide what to do with the data in the Protobuf struct received over the wire.

^ boiler plate approach, not recommended

paulmil1 avatar Jul 19 '22 08:07 paulmil1