Gotcha of custom type in go-validator
Table of Contents
Source code of this article can be found here: Github repo
What’s go-validator? #
From it’s repo:
100+ Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving
One common use-case you may think of is validating request body
of an API
Endpoint
func makeValidator() *validator.Validate {
validate := validator.New(validator.WithRequiredStructEnabled())
return validate
}
func TestSimpleStruct(t *testing.T) {
// check `Name` length is greater than 10
type requestBody struct {
Name string `validate:"required,gt=10"`
}
validate := makeValidator()
body := &requestBody{
Name: "short",
}
err := validate.Struct(body)
assert.Error(t, err, "should raise an error because name is less than 10")
}
Gotcha of custom type #
Go’s lack of nullability
makes us rely on some alternatives to represent
nullability
in Golang
. Two methods I know of are:
- Pointer
- Custom struct with a
Valid
field (check this type at std library)
If you happen to need the use of nullability
and you see this:
go-validator
README
Handles custom field types such as sql driver Valuer see Valuer
Then you might think it has support for anything that implements interface
driver.Valuer
out of the box, like sql.NullString
which I mentioned earlier
But those simple unit-tests say otherwise:
1st test #
func TestNullField(t *testing.T) {
validate := makeValidator()
type testCase struct {
Name sql.NullString `validate:"required"`
}
test := &testCase{
Name: sql.NullString{
String: "Hello", // This is not empty on purpose,
// I will explain it later
Valid: false, // Valid = false represents null
},
}
err := validate.Struct(test)
// check err is not nil
assert.Error(t, err, "should return an error because name is null")
}
Test result:
go test ./... -v
=== RUN TestNullField
main_test.go:31:
Error Trace: /home/remvn/personal/go-validator-custom-type/main_test.go:31
Error: An error is expected but got nil.
Test: TestNullField
Messages: should return an error because Valid is false
--- FAIL: TestNullField (0.00s)
=== RUN TestSimpleStruct
--- PASS: TestSimpleStruct (0.00s)
FAIL
FAIL github.com/remvn/go-validator-custom-type 0.002s
FAIL
The test failed, err
returned from validate function is nil even though Valid
= false
2nd test #
In this test. We created a non-null string with Valid
being true, we also
added another validate tag: gt=10
(check if the length is greater than 10)
func TestFieldLength(t *testing.T) {
validate := makeValidator()
type testCase struct {
Name sql.NullString `validate:"required,gt=10"`
}
test := &testCase{
// This is a non-null string
Name: sql.NullString{
String: "hello", // note that string length is less than 10
Valid: true,
},
}
err := validate.Struct(test)
// check err is not nil
assert.Error(t, err, "should return an error because length is less than 10")
}
The test even result with panic
:
go test -run TestFieldLength -v ./...
=== RUN TestFieldLength
--- FAIL: TestFieldLength (0.00s)
panic: Bad field type sql.NullString [recovered]
panic: Bad field type sql.NullString
goroutine 7 [running]:
testing.tRunner.func1.2({0x63e2a0, 0xc00002b200})
What’s going on? Can we fix this? #
After some digging. Turns out this library didn’t handle driver.Valuer
out of
the box
. It also explains why:
The first test is failing because by default
go-validator
treatsName
field as a normal struct. So, with arequired
tag on that struct, it tried to check every field of that struct, the check will pass if one of them is not zero-value. This is also why I put a non-empty string on purpose to make the test failing, proving thatValid = false
means nothing.- Have a read about
required
tag here: docs
- Have a read about
With the knowledge above, it’s easier to understand why second test is failing, since now it tried to check “the length of the struct is greater than 10”, which ultimately panic by default
To use custom types for go-validator
you need to register a custom function
to pull out the “real” value of the struct manually.
Here’s how we can do it:
func makeValidator() *validator.Validate {
validate := validator.New(validator.WithRequiredStructEnabled())
// register func for custom struct, do this for every custom struct
// you're going to need
validate.RegisterCustomTypeFunc(validateValuer, sql.NullString{}, sql.NullInt64{})
return validate
}
func validateValuer(field reflect.Value) interface{} {
if valuer, ok := field.Interface().(driver.Valuer); ok {
val, err := valuer.Value()
if err == nil {
// return the "real" value of custom struct
// for example: if concrete type of driver.Valuer interface
// is sql.NullString then val will be a string
return val
}
}
// return nil means this field is indeed "null".
// field with tag `required` will fail the check
return nil
}
Now the tests are passing:
go test -v ./...
=== RUN TestNullField
--- PASS: TestNullField (0.00s)
=== RUN TestFieldLength
--- PASS: TestFieldLength (0.00s)
=== RUN TestSimpleStruct
--- PASS: TestSimpleStruct (0.00s)
PASS
ok github.com/remvn/go-validator-custom-type 0.002s
I think this is a small lack of documentation of go-validator
, which can be
hard to figure out. But overall this is a good opportunity for me to understand
the language specs a little bit deeper.