Documentation
¶
Overview ¶
Package validator provides lightweight, fluent input validation for express handlers, in the spirit of the npm "express-validator" package. Instead of mirroring express-validator's global check()/body()/query() functions and its validationResult() accumulator, this port models validation as a Schema value: an ordered list of per-field rule chains that you build once and reuse as a plain function or as request middleware. Build a Schema of field rules and either validate a map directly with Schema.Validate or mount it with Schema.Body / Schema.Query so that invalid requests are rejected with a 400 JSON response before your handler runs.
schema := validator.Schema{
validator.Field("email").Required().Email(),
validator.Field("age").Optional().IsInt().Min(0).Max(120),
validator.Field("name").Required().MinLen(2).MaxLen(50),
}
app.Post("/users", schema.Body(), createUser)
You reach for this package when an express route needs to guard its body or query string against missing, malformed, or out-of-range fields without pulling in a large dependency. Each Field starts a chain, and the chained methods (Required, Optional, Email, MinLen, MaxLen, Min, Max, IsInt, IsNumber, In, Matches, Custom) append rules that are evaluated in the order they were declared. Rule chains are fluent: every method returns the same *FieldRules pointer, so a whole field can be described in a single readable expression that reads much like the express-validator DSL it emulates.
Mechanically, a rule is a func(value any, present bool) string. It receives the raw field value together with a flag indicating whether the key was present in the incoming map, and returns an empty string when the value is acceptable or a human-readable message when it is not. Schema.Validate walks each field, looks the value up in the supplied map[string]any, and runs the field's rules in sequence, collecting at most one message per field (the first that fails) into an Errors slice. Numeric rules coerce strings via strconv, string-length rules count bytes with len, Email matches a pragmatic regular expression (a local part, an "@", and a dotted domain), and Matches compiles an arbitrary caller-supplied pattern with regexp.MustCompile.
Presence and emptiness have deliberate semantics. A value is "empty" when it is nil or a string that is blank after trimming whitespace. Required fails on a missing or empty value; Optional causes the entire remaining chain to be skipped when the field is absent or empty, so optional fields validate only when the caller actually supplied something. Every other rule treats an absent value as passing, which means order matters: pair a value constraint with Required (or Optional) to decide what happens when the field is not sent. The middleware forms normalize their inputs first, flattening url.Values (from a query string or urlencoded body) so that only the first value of each key is validated as a string.
Compared with the Node original, the surface here is intentionally smaller and synchronous: there are no sanitizers, no async/custom-promise validators, no wildcard or nested-path selectors, and no per-rule custom messages beyond what Custom returns. What does match is the spirit of a declarative, chainable, per-field rule set and the 400-with-errors behavior of the middleware, whose JSON payload is {"errors": [{"field", "message"}, ...]}. For raw single-value string checks (IsEmail, IsURL, IsUUID, and friends) that correspond to validator.js, use the sibling validatorjs package instead.
Index ¶
- type Errors
- type FieldError
- type FieldRules
- func (f *FieldRules) Custom(fn func(value any) string) *FieldRules
- func (f *FieldRules) Email() *FieldRules
- func (f *FieldRules) In(allowed ...string) *FieldRules
- func (f *FieldRules) IsInt() *FieldRules
- func (f *FieldRules) IsNumber() *FieldRules
- func (f *FieldRules) Matches(pattern string) *FieldRules
- func (f *FieldRules) Max(n float64) *FieldRules
- func (f *FieldRules) MaxLen(n int) *FieldRules
- func (f *FieldRules) Min(n float64) *FieldRules
- func (f *FieldRules) MinLen(n int) *FieldRules
- func (f *FieldRules) Optional() *FieldRules
- func (f *FieldRules) Required() *FieldRules
- type Schema
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Errors ¶
type Errors []FieldError
Errors is a collection of validation failures.
Example ¶
ExampleErrors shows how the Errors slice satisfies the error interface. When a schema produces one or more FieldError values, calling Error on the slice joins them into a single summary string prefixed with "validation failed:". Here both required fields are missing, so two failures are collected. The combined message lists each field and its reason separated by semicolons. This is convenient when you want to log or return one flat string rather than iterating the individual failures.
package main
import (
"fmt"
"github.com/malcolmston/express/validator"
)
func main() {
schema := validator.Schema{
validator.Field("email").Required(),
validator.Field("name").Required(),
}
errs := schema.Validate(map[string]any{})
fmt.Println(errs.Error())
}
Output: validation failed: email is required; name is required
type FieldError ¶
type FieldError struct {
// Field is the name of the field that failed validation.
Field string `json:"field"`
// Message is the human-readable reason the field was rejected.
Message string `json:"message"`
}
FieldError is a single validation failure.
type FieldRules ¶
type FieldRules struct {
// contains filtered or unexported fields
}
FieldRules accumulates the rules applied to a single named field.
func Field ¶
func Field(name string) *FieldRules
Field starts a rule chain for the named field.
Example ¶
ExampleField demonstrates that a valid input map produces no errors. The chain marks name as required with a minimum length and treats age as an optional integer, so omitting age entirely is acceptable because Optional skips the rest of that field's chain when the value is absent. Every supplied value satisfies its rule, so Schema.Validate returns a nil Errors slice whose length is zero. This mirrors how you would gate a handler: an empty result means the data is safe to use. The example prints the error count to show the happy path.
package main
import (
"fmt"
"github.com/malcolmston/express/validator"
)
func main() {
schema := validator.Schema{
validator.Field("name").Required().MinLen(2).MaxLen(50),
validator.Field("age").Optional().IsInt().Min(0),
}
errs := schema.Validate(map[string]any{
"name": "Ada",
})
fmt.Println("errors:", len(errs))
}
Output: errors: 0
func (*FieldRules) Custom ¶
func (f *FieldRules) Custom(fn func(value any) string) *FieldRules
Custom applies a user-supplied validation function. Returning a non-empty string records that message as an error.
func (*FieldRules) Email ¶
func (f *FieldRules) Email() *FieldRules
Email validates that the value is a syntactically valid email address.
func (*FieldRules) In ¶
func (f *FieldRules) In(allowed ...string) *FieldRules
In requires the value to be one of the allowed strings.
func (*FieldRules) IsInt ¶
func (f *FieldRules) IsInt() *FieldRules
IsInt requires the value to be an integer.
func (*FieldRules) IsNumber ¶
func (f *FieldRules) IsNumber() *FieldRules
IsNumber requires the value to be numeric.
func (*FieldRules) Matches ¶
func (f *FieldRules) Matches(pattern string) *FieldRules
Matches requires the string value to match the given regular expression.
func (*FieldRules) Max ¶
func (f *FieldRules) Max(n float64) *FieldRules
Max requires a numeric value <= n.
func (*FieldRules) MaxLen ¶
func (f *FieldRules) MaxLen(n int) *FieldRules
MaxLen requires the string value to be at most n characters.
func (*FieldRules) Min ¶
func (f *FieldRules) Min(n float64) *FieldRules
Min requires a numeric value >= n.
func (*FieldRules) MinLen ¶
func (f *FieldRules) MinLen(n int) *FieldRules
MinLen requires the string value to be at least n characters.
func (*FieldRules) Optional ¶
func (f *FieldRules) Optional() *FieldRules
Optional marks the field as optional: when absent, remaining rules are skipped instead of failing.
func (*FieldRules) Required ¶
func (f *FieldRules) Required() *FieldRules
Required fails when the field is missing or an empty string.
type Schema ¶
type Schema []*FieldRules
Schema is an ordered set of field rules.
func (Schema) Body ¶
Body returns express middleware that validates the parsed request body. It expects a preceding body-parser (express.JSON or express.URLEncoded). On failure it responds 400 with {"errors": [...]}; on success it calls next.
Example ¶
ExampleSchema_Body mounts a Schema as express request middleware and drives it with httptest to show the rejection path. Schema.Body validates the parsed request body and, on failure, responds with HTTP 400 and a JSON object of the form {"errors": [...]} before the downstream handler ever runs. Here the body is registered directly as a map so the validator sees a missing required field. The recorder captures the 400 status and the JSON payload naming the field. Because the response is deterministic, the Output block asserts both.
package main
import (
"fmt"
"net/http/httptest"
"strings"
"github.com/malcolmston/express"
"github.com/malcolmston/express/validator"
)
func main() {
schema := validator.Schema{
validator.Field("email").Required().Email(),
}
app := express.New()
app.Use(func(req *express.Request, res *express.Response, next express.Next) {
req.SetBody(map[string]any{"email": ""})
next()
})
app.Post("/users", schema.Body(), func(req *express.Request, res *express.Response, next express.Next) {
res.Send("created")
})
rec := httptest.NewRecorder()
app.ServeHTTP(rec, httptest.NewRequest("POST", "/users", strings.NewReader("")))
fmt.Println("status:", rec.Code)
fmt.Println("body:", strings.TrimSpace(rec.Body.String()))
}
Output: status: 400 body: {"errors":[{"field":"email","message":"is required"}]}
func (Schema) Validate ¶
Validate runs the schema against a data map, returning all failures (nil when valid).
Example ¶
ExampleSchema_Validate builds a Schema with a required e-mail field and an optional, bounded integer field, then runs it against a map that violates both constraints. Schema.Validate walks the fields in declaration order and records at most one message per field, so the resulting Errors slice is fully deterministic. The e-mail value fails the Email rule and the age value, once coerced from its string form, fails the Max rule. Each FieldError pairs the offending field name with a human-readable message. The Output block below confirms both failures are reported in field order.
package main
import (
"fmt"
"github.com/malcolmston/express/validator"
)
func main() {
schema := validator.Schema{
validator.Field("email").Required().Email(),
validator.Field("age").Optional().IsInt().Min(0).Max(120),
}
errs := schema.Validate(map[string]any{
"email": "not-an-email",
"age": "200",
})
for _, e := range errs {
fmt.Printf("%s: %s\n", e.Field, e.Message)
}
}
Output: email: must be a valid email address age: must be <= 120