All the codes that you will see in this post you can check here
In the last post we did the POST method to create books in our application and in this post we will put a validation in our presenter layer (or data transfer object) that represent json input in our REST API.
In this case we will use a validator lib that will help us in our process.
So let's install the lib
go get github.com/go-playground/validator/v10
In our case we need to add some tags in our presenter layer to explain what we will validate.
//cmd/http/presenter/book.go
type BookPersist struct {
Title string `json:"title" validate:"required"`
Author string `json:"author" validate:"required"`
NumberPages int `json:"numberPages" validate:"required"`
}
In our BookPersist struct we added a validate tag, in our example we just add a required value to turn required values in ours fields but you can look into validator documentation to check every value that you can use.
But we need to change some places in our code to not impact our handler or usecase.
In this moment we have a place to decode the json to struct in our request.go file in the BindJson method. Let's update our BindJson to check this validations for us.
//cmd/http/helper/request.go
func BindJson(r *http.Request, destination interface{}) error {
e := json.NewDecoder(r.Body).Decode(destination)
if e != nil {
return ErrDecodeJson
}
return validateJsonFields(destination)
}
func validateJsonFields(input interface{}) error {
validator := validator.New()
if err := validator.Struct(input); err != nil {
return err
}
return nil
}
The validateJsonFields will help us to centralize the validation for every presenter/DTO that pass here.
So the validator.New will instantiate the lib and validator.Struct(...) will do the magic for us and if our json not respect the validations fields that we put in the validate tag the error will be generated.
BindJson will call this method for every decode and it's very important to tell you, if your struct don't use the validate tag BindJson will continue work without any problem, good right?
But our application don't know how to respond the validations errors to our user with friendly messages. To solve that we need to change one more place. Yes, our DealWith(...) method that handle our errors in our handlers. So let's improve our DealWith method.
//cmd/http/helper/error.go
func DealWith(err error) HttpError {
if ok, httpError := isValidationError(err); ok {
return *httpError
} else if errCode, ok := errorHandlerMap[err]; ok {
return HttpError{Status: errCode, Description: err.Error()}
} else {
return HttpError{
Status: http.StatusInternalServerError,
Description: "Internal error, please report to admin",
}
}
}
func isValidationError(err error) (bool, *HttpError) {
v := &validator.ValidationErrors{}
if errors.As(err, v) {
validationErrors := HttpError{Status: http.StatusBadRequest, Description: ErrJsonValidation.Error()}
for _, err := range err.(validator.ValidationErrors) {
message := generateErrorMessage(err)
validationErrors.Messages = append(validationErrors.Messages, message.Error())
}
return true, &validationErrors
}
return false, nil
}
func generateErrorMessage(err validator.FieldError) error {
return fmt.Errorf("error: field validation for '%s' failed on the '%s' tag", err.Field(), err.Tag())
}
First we will create the isValidationError(...) where we will check if the error that come from handler error is generated from our new validator lib.
We iterate the range err.(validator.ValidationErrors) because our struct could throw more than one error per field and in this range we append the errors to result in one message with all validations.
The generateErrorMessage(...) it's just a method to result in a clear message.
And in the DealWith method we have a new checkup the
if ok, httpError := isValidationError(err); ok {...}
It's very important this condition come first because the validation error will not be considered a domain error, for this reason we will not need to register the ErrJsonValidation in the errorHandlerMap.
Right now our application know how to deal with validation messages from our validator lib without change any handler or usecase.
And now we will dispatch a curl to check the response
curl --location 'http://localhost:8080/v1/books' \
--header 'Content-Type: application/json' \
--data '{
"title": "title",
"numberPages": 200
}'
{"description":"error validating json","messages":["error: field validation for 'Author' failed on the 'required' tag"]}
Right now our application get the error messages from presenter layer and delivery with a friendly message to our user.
In the next post we will start to configure our infrastructure to add docker and docker-compose to change our in-memory repository to a Postgresql.
Top comments (4)
you don't need to instantiate a new validator in each function call; you can have a global validator
That's correct, we can instantiate in a init() function and improve the algorithm.
I usually create a middleware passing a single instance of the validator and use the middleware for every endpoint
It's a valid approach too, i don't like to use middleware because not every handler need to validate inputs, so i like to delegate to the handler i think it's more readable to maintain.
But i would like to see this approach, you have any gist/github project to share?