Introduction
Hey-hey, awesome DEV people! π
How about a little time in the company of an article that teaches you how to better communicate with the frontend developers on your team? Intrigued, but don't know what it's about? Don't worry, I'll explain it all now!
π I often notice an interesting trend in my work: the backend developer sends error statuses and explanations to the frontend, which are not always clear how to handle and output to the user in the application. As a result, the frontend developer spends precious time understanding what's going on and implementing borderline cases in the code.
π‘ But, what if I told you that errors from the backend can be not just readable to the developer, but understandable even to the user? Yes, that's accurately what this article will talk about!
We will take a look at package go-playground/validator v10
, which is almost the number one choice for such problems in Go.
π Table of contents
- Model to validate
- Vanilla representation of the error from package
- Recreate validator
- Override error message
Model to validate
Imagine we need to implement backend validation of incoming data from POST
request to REST API endpoint of new project creation. Can you imagine? Okay, now let's describe this structure in Go code.
Besides the standard attributes db:"..."
and json:"..."
, we need to add a new attribute validate:"..."
with the required validation tag from the go-playground/validator
package to each structure field that we need to validate.
It only sounds very complicated, in fact, everything is much simpler. Look:
// ./app/models/project_model.go
// Project struct to describe project.
type Project struct {
Title string `db:"title" json:"title" validate:"required,lte=25"`
// --> verify that the field exists and is less than or equal to 25 characters
Description string `db:"description" json:"description" validate:"required"`
// --> verify that the field exists
WebsiteURL string `db:"website_url" json:"website_url" validate:"uri"`
// --> verify that the field is correct URL string
Tags []string `db:"tags" json:"tags" validate:"len=3"`
// --> verify that the field contains exactly three elements
}
βοΈ Note: These are not all the parameters by which you can configure field validation for your structures! You can find the full list here.
An interesting thing is that if we specify a validator to check, for example, if the URL validated, then we don't need to specify the required
validation tag anymore. This happens because an empty string is not a valid URL.
In other words, almost any validation tag will also include a mandatory non-empty value (empty string, zero, nil, β¦) for the field.
Vanilla representation of the error from package
Input JSON body (here and below, we will work with these very input parameters for the JSON request body):
{
"title": "",
"description": "",
"website_url": "not-valid-uri",
"tags": [
"one", "two"
]
}
I will display the resulting error response as plain text so that you can appreciate why this presentation option for the frontend would not be good:
Key: 'Project.Title' Error:Field validation for 'Title' failed on the 'required' tag
Key: 'Project.Description' Error:Field validation for 'Description' failed on the 'required' tag
Key: 'Project.WebsiteURL' Error:Field validation for 'WebsiteURL' failed on the 'uri' tag
Key: 'Project.Tags' Error:Field validation for 'Tags' failed on the 'len' tag
And there are several important points that we want to improve right away.
First, the frontend knows nothing about the structures and models in our application. If the backend returns an error in this form (without specifying at least the field that failed validation), the frontend will not be able to make a visual output of the error for a particular field.
Second, it's better to specify the exact names of the fields which the frontend works with β not WebsiteURL
but website_url
, like in JSON.
Third, the error description itself will not tell the user (or even the frontend developer) anything useful, except that something went wrong somewhere.
Well, let's try to improve it!
βοΈ Note: I will show you the way I do it on my projects. By the way, I'd be happy to get feedback and examples of how you customize error output for frontend in your projects.
Recreate validator
Great, we get rid of the fields with names, like in the structure. We just override their output, so that the validator looks at the json:"..."
parameter in the structure, not at its actual name.
To complete this, we use the RegisterTagNameFunc
method built into the package with a little magic. I will put this in a different helper package (./pkg/utilities/validator.go
) so that there is more readable application code:
// ./pkg/utilities/validator.go
// NewValidator func for create a new validator for struct fields.
func NewValidator() *validator.Validate {
// Create a new validator.
validate := validator.New()
// Rename struct fields to JSON.
validate.RegisterTagNameFunc(func(fl reflect.StructField) string {
name := strings.SplitN(fl.Tag.Get("json"), ",", 2)
if name[1] == "-" {
return ""
}
return name[0]
})
return validate
}
// ...
If you want to avoid renaming any of the fields, add ,-
(comma + dash) to the end of its JSON name, like this:
WebsiteURL string `db:"website_url" json:"website_url,-" validate:"uri"`
Yes, you got me right, this method opens up great possibilities to customize the error output itself. You can rely not on json:"..."
attribute in the field, but on your one, for example, field_name:"..."
or any other one you wish.
βοΈ Note: To understand how it works, please follow this issue.
Function to check the validation error
Let's move on. It's time to make a nicer output of validation errors, so that the frontend developer on your team will thank you.
I always use this practice when implementing a REST API in JSON format for internal use (e.g., for single-page applications aka SPA):
- We return JSON in strictly consistent notation with the frontend, but relative to the interaction objects;
- The status code of the response from the backend is always
HTTP 200 OK
, unless it concerns server errors (status code5XX
); - The server response always contains the
status
field (typeint
) indicating the actual status code; - If an error occurred (status code not
2Π₯Π₯
), the server response always contains a fieldmsg
(typestring
) with a short indication of the cause of the error;
Furthermore, in the example below, I took code from my project written using the Fiber web framework. So, some elements from its libraries are present there. Don't be scared, the main thing here is to understand the principle of validation itself.
βοΈ Note: If you want to learn more about Fiber, I have a series of articles to help you do that. I'd be glad if you'd study it later, too.
Okay, my function to check for validation errors would look like this:
// ./pkg/utilities/validator.go
// ...
// CheckForValidationError func for checking validation errors in struct fields.
func CheckForValidationError(ctx *fiber.Ctx, errFunc error, statusCode int, object string) error {
if errFunc != nil {
return ctx.JSON(&fiber.Map{
"status": statusCode,
"msg": fmt.Sprintf("validation errors for the %s fields", object),
"fields": ValidatorErrors(errFunc),
})
}
return nil
}
The principle of this function is elementary:
- Accept the Fiber context to have all the possibilities to work with the context that came;
- Accept the object with the validation error defined above;
- Accept the status code, which should return if the error occurs;
- Accept the name of the object (or model) we're currently checking, so we can output a more readable error message;
- Return the generated JSON with all the necessary errors and explanations or
nil
;
I can now easily use the CheckForValidationError
function in the controller:
// ./app/controllers/project_controller.go
import (
// ...
"github.com/my-user/my-repo/pkg/utilities"
// --> add local package `utilities`
)
// CreateNewProject func for create a new project.
func CreateNewProject(c *fiber.Ctx) error {
// ...
// Create a new validator, using helper function.
validate := utilities.NewValidator()
// Validate all incomming fields for rules in Project struct.
if err := validate.Struct(project); err != nil {
// Returning error in JSON format with status code 400 (Bad Request).
return utilities.CheckForValidationError(
c, err, fiber.StatusBadRequest, "project",
)
}
// ...
}
Custom validation tag
Every so often, it happens that the built-in validation tags (or rather, the rules by which a particular field should be validated) are not always sufficient. To solve this problem, the authors of go-playground/validator
package provided a special method.
Let's consider its use on a simple example π
So, we have a field with type uuid.UUID
which we create with the package google/uuid, which we want to check with the built-in validator uuid.Parse()
of that package. All we need to do is add a new RegisterValidation
method to the NewValidator
function (described above) with simple logic code:
// ./pkg/utilities/validator.go
// NewValidator func for create a new validator for struct fields.
func NewValidator() *validator.Validate {
// Create a new validator.
validate := validator.New()
// ...
// Custom validation for uuid.UUID fields.
_ = validate.RegisterValidation("uuid", func(fl validator.FieldLevel) bool {
field := fl.Field().String() // convert to string
if _, err := uuid.Parse(field); err != nil {
return true // field has error
}
return false // field has no error
})
// ...
return validate
}
That's it! If the field passed validation, it will return false
logical value, and if there are any errors it will return true
.
βοΈ Note: The method
RegisterValidation
should be read and understood like this: βplease check if there is an error in the value of the field with the validation taguuid
?β.
Now we can validate fields of this type like this:
// ./app/models/something_model.go
// MyStruct struct to describe something.
type MyStruct struct {
ID uuid.UUID `db:"id" json:"id" validate:"uuid"`
// --> verify that the field is a valid UUID
}
Override error message
And now for the best part. Overriding the validation error message itself.
βοΈ Note: Follow comments in the code to better understand.
This helper function will map all validation errors to each field and then simply pass that map to the CheckForValidationError
function (which we described in the previous section):
// ./pkg/utilities/validator.go
// ...
// ValidatorErrors func for show validation errors for each invalid fields.
func ValidatorErrors(err error) map[string]string {
// Define variable for error fields.
errFields := map[string]string{}
// Make error message for each invalid field.
for _, err := range err.(validator.ValidationErrors) {
// Get name of the field's struct.
structName := strings.Split(err.Namespace(), ".")[0]
// --> first (0) element is the founded name
// Append error message to the map, where key is a field name,
// and value is an error description.
errFields[err.Field()] = fmt.Sprintf(
"failed '%s' tag check (value '%s' is not valid for %s struct)",
err.Tag(), err.Value(), structName,
)
}
return errFields
}
As you may have noticed, to override the field error message, we operate on special variables (err.Namespace()
, err.Field()
, err.Tag()
and err.Value()
) which the authors of the go-playground/validator
package offer us.
βοΈ Note: You can find a complete list of all available ones here.
Now, we will get this message when we make an invalid request:
{
"status": 400,
"msg": "validation errors for the project fields",
"fields": {
"category": "failed 'required' tag check (value '' is not valid for Project struct)",
"description": "failed 'required' tag check (value '' is not valid for Project struct)",
"tags": "failed 'len' tag check (value '[one two]' is not valid for Project struct)"
"title": "failed 'required' tag check (value '' is not valid for Project struct)"
"website_url": "failed 'uri' tag check (value 'not-valid-uri' is not valid for Project struct)"
}
}
βοΈ Note: After validation, all not valid fields are in alphabetical order, not in the order that was defined by the Go structure.
Hooray! π We got what we wanted and no one got hurt. On the contrary, everyone is happy, both on the frontend and the backend.
Photos and videos by
- Authors of the package
go-playground/validator
feat. Vic ShΓ³stak - Markus Spiske https://unsplash.com/photos/IiEFmIXZWSw
P.S.
If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! π»
βοΈ You can support me on Boosty, both on a permanent and on a one-time basis. All proceeds from this way will go to support my OSS projects and will energize me to create new products and articles for the community.
And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!
My main projects that need your help (and stars) π
- π₯ gowebly: A next-generation CLI tool that makes it easy to create amazing web applications with Go on the backend, using htmx, hyperscript or Alpine.js and the most popular CSS frameworks on the frontend.
- β¨ create-go-app: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
Top comments (1)
Great article.. Thanks