Overview
middlewares that wraps the extensive collection of validators and sanitizers offered by my other open source package for JSON field syntax, providing efficient querying and extraction of data from JSON objects.
It allows you to combine them in many ways so that you can validate and sanitize your Gin requests, and offers tools to determine if the request is valid or not, which data was matched according to your validators.
It is based on the popular js/express library 1.16+.
It's also verified to work with ):
There are other libraries out there, but they often feel too complex for what they do. They require more setup and work than you’d expect, especially when you just want a simple, straightforward solution for validation.
Installation
Make sure you have to salute John!
💡 Tip:
You can use will print"Hello, ".
That's where
ginvalidatorcomes in handy. It providesvalidators,sanitizersandmodifiersthat are used to validate your request.
Let's add a validator and a modifier that checks that the person query string cannot be empty, with the validator named Empty and modifier named Not:
CODEpackage main
import (
"net/http"
gv "github.com/bube054/ginvalidator"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/hello", gv.NewQuery("person", nil).
Chain().
Not().
Empty(nil).
Validate(), func(ctx *gin.Context) {
person := ctx.Query("person")
ctx.String(http.StatusOK, "Hello, %s!", person)
})
r.Run()
}
📝 Note:
For brevity,gvis used as an alias forginvalidatorin the code examples.
Now, restart your server, and go to again, you’ll see the following JSON content, formatted for clarity:
CODE{
"errors": [
{
"location": "queries",
"message": "Invalid value",
"field": "person",
"value": ""
}
]
}
Now, what this is telling us is that
- there's been exactly one error in this request;
- this field is called person;
- it's located in the query string (location:
"queries");
- the error message that was given was
"Invalid value".
This is a better scenario, but it can still be improved. Let's continue.
Creating better error messages
All request location validators accept an optional second argument, which is a function used to format the error message. If
nilis provided, a default, generic error message will be used, as shown in the example above.
CODEpackage main
import (
"net/http"
gv "github.com/bube054/ginvalidator"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/hello",
gv.NewQuery("person",
func(initialValue, sanitizedValue, validatorName string) string {
return "Please enter your name."
},
).Chain().
Not().
Empty(nil).
Validate(),
func(ctx *gin.Context) {
result, err := gv.ValidationResult(ctx)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": "The server encountered an unexpected error.",
})
return
}
if len(result) != 0 {
ctx.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
"errors": result,
})
return
}
person := ctx.Query("person")
ctx.String(http.StatusOK, "Hello, %s!", person)
})
r.Run()
}
Now if you access to salute John!
The available locations are
BodyLocation,CookieLocationQueryLocation,ParamLocationandHeaderLocation.
Each of these locations includes aStringmethod that returns the location where validated/sanitized data is being stored.
Sanitizing inputs
While the user can no longer send empty person names, it can still inject HTML into your page! This is known as the is one of the main concepts in ginvalidator, therefore it's useful to learn about it, so that you can use it effectively.
But don't worry: if you've read through the Getting Started guide, you have already used validation chains without even noticing!
What are validation chains?
Validation chains are created using the following functions, each targeting a specific location in the HTTP request:
NewBody: Validates data from thehttp.Requestbody. Its location isBodyLocation.
NewCookie: Validates data from thehttp.Requestcookies. Its location isCookieLocation.
NewHeader: Validates data from thehttp.Requestheaders. Its location isHeaderLocation.
NewParam: Validates data from the Gin route parameters. Its location isParamLocation.
NewQuery: Validates data from thehttp.Requestquery parameters. Its location isQueryLocation.
They have this name because they wrap the value of a field with validations (or sanitizations), and each of its methods returns itself.
This pattern is usually called
Sanitizerstransform the field value. They are useful to remove noise from the value and perhaps even to provide some basic line of defense against threats.
Sanitizers persist the updated fields value back into the Gin Contexts, so that it's usable by other ginvalidator functions, your own route handler code, and even other middlewares.
They are:
Modifiersdefine how validation chains behave when they are run.
They are:
- . If any details are unclear, you may also want to refer to related functions within the
validatorgopackage for additional context, which I’ll be explaining below.
Standard validators/sanitizers
All of the functionality exposed by the validation chain actually comes from for extracting values. Please refer to the linked documentation for details.
Example:
{
"user": {
"name": "John",
"email": "[email protected]"
}
}
With path user.name, the extracted value would be "John".
application/x-www-form-urlencoded: Typically used for HTML form submissions. Fields are submitted as key-value pairs in the body.
Example:
Content-Type: application/x-www-form-urlencoded
Body:
name=John&[email protected]
Field "name" would have the value "John", and "email" would have the value "[email protected]".
multipart/form-data: Commonly used for file uploads or when submitting form data with files.
Example:
Content-Type: multipart/form-data
Body:
--boundary
Content-Disposition: form-data; name="name"
John
--boundary
Content-Disposition: form-data; name="file"; filename="resume.pdf"
Content-Type: application/pdf
[binary data]
--boundary--
Field "name" would have the value "John", and "file" would be the uploaded file.
Query fields correspond to URL search parameters, and their values are automatically url unescaped by Gin.
Examples:
- Field:
"name", Value:"John"
/hello?name=John
- Field:
"full_name", Value:"John Doe"
/hello?full_name=John%20Doe
Param fields represent URL path parameters, and their values are automatically unescaped by ginvalidator.
Example:
- Field:
"id", Value:"123"
/users/:id
Header fields are HTTP request headers, and their values are not unescaped. A log warning will appear if you provide a non-canonical header key.
Example:
- Field:
"User-Agent", Value:"Mozilla/5.0"
Header: "User-Agent", Value: "Mozilla/5.0"
Cookies fields are HTTP cookies, and their values are automatically url unescaped by Gin.
Example:
- Field:
"session_id", Value:"abc 123"
Cookie: "session_id=abc%20123"
Customizing express-validator
If the server you're building is anything but a very simple one, you'll need validators, sanitizers and error messages beyond the ones built into ginvalidator sooner or later.
Custom Validators and Sanitizers
A classic need that ginvalidator can't fulfill for you, and that you might run into, is validating whether an e-mail address is in use or not when a user signing up.
It's possible to do this in ginvalidator by implementing a custom validator.
A CustomValidator is a method available on the validation chain, that receives a special function , and have to returns the new sanitized value.
Implementing a custom validator
A CustomValidator can be asynchronous by using goroutines and a sync.WaitGroup to handle concurrent operations. Within the validator, you can spin up goroutines for each asynchronous task, adding each task to the WaitGroup. Once all tasks complete, the validator should return a boolean.
For example, in order to check that an e-mail is not in use:
func isUserPresent(email string) bool {
return email == "[email protected]"
}
r.POST("/create-user",
gv.
NewBody("email", nil).
Chain().
CustomValidator(
func(req *http.Request, initialValue, sanitizedValue string) bool {
var exists bool
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
exists = isUserPresent(sanitizedValue)
}()
wg.Wait()
return !exists
},
).
Validate(),
func(ctx *gin.Context) {
// Handle the request
},
)
Or maybe you could also verify that the password matches the repeat:
type createUser struct {
Password string `json:"password"`
PasswordConfirmation string `json:"passwordConfirmation"`
}
r.POST("/create-user",
gv.NewBody("password", nil).
Chain().
Matches(regexp.MustCompile(`^[A-Za-z\d]{8,}$`)).
Validate(),
gv.NewBody("passwordConfirmation", nil).
Chain().
CustomValidator(func(req *http.Request, initialValue, sanitizedValue string) bool {
data, err := io.ReadAll(req.Body)
if err != nil {
return false
}
// Refill the request body to allow further reads, if needed.
req.Body = io.NopCloser(bytes.NewBuffer(data))
var user createUser
json.Unmarshal(data, &user)
return sanitizedValue == user.PasswordConfirmation
}).
Validate(),
func(ctx *gin.Context) {
// Handle request
},
)
⚠️ Caution:
If the request body will be accessed multiple times—whether in the same validation chain, in another validation chain for the same request context, or in subsequent handlers—ensure you reset the request body after each read. Failing to do so can lead to errors or missing data when the body is read again.
Implementing a custom sanitizer
CustomSanitizer don't have many rules. Whatever the value that they return, is the new value that the field will acquire.
Custom sanitizers can also be asynchronous by using goroutines and a sync.WaitGroup to handle concurrent operations.
r.POST("/user/:id",
gv.NewParam("id", nil).
Chain().
CustomSanitizer(
func(req *http.Request, initialValue, sanitizedValue string) string {
return strings.Repeat(sanitizedValue, 3) // some string manipulation
},
).
Validate(),
func(ctx *gin.Context) {
// Handle request
},
)
Error Messages
Whenever a field value is invalid, an error message is recorded for it.
The default error message is "Invalid value", which is not descriptive at all of what the error is, so you might need to customize it. You can customize by
gv.NewBody("email",
func(initialValue, sanitizedValue, validatorName string) string {
switch validatorName {
case gv.EmailValidatorName:
return "Email is not valid."
case gv.EmptyValidatorName:
return "Email is empty."
default:
return gv.DefaultValChainErrMsg
}
},
).
Chain().
Not().Empty(nil).
Email(nil).
Validate()
initialValueis the original value extracted from the request (before any sanitization).
sanitizedValueis the value after it has been sanitized (if applicable).
validatorNameis the name of the validator that failed, which helps identify the validation rule that did not pass.
For a complete list of validator names, refer to the - Attah Gbubemi David (author)
License
This project is licensed under the file for details.
SOCIAL SHARE CARD GENERATOR