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
Validfield (check this type at std library)
If you happen to need the use of nullability and you see this:
go-validatorREADME
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-validatortreatsNamefield as a normal struct. So, with arequiredtag 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 = falsemeans nothing.- Have a read about
requiredtag 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.