Simple, declarative validation for Go maps and HTTP payloads with nested and list object support.
- Validate
map[string]interface{}andhttp.Request(JSON or multipart). - Compose rules fluently. Support nested object and list of object.
- Transform values with manipulators and plug custom extensions.
- Bind validated data back to your struct.
Examples: see the test suite: https://github.com/Rhyanz46/go-map-validator/tree/main/test
go get github.com/Rhyanz46/go-map-validator/map_validatorImport: import "github.com/Rhyanz46/go-map-validator/map_validator"
import "github.com/Rhyanz46/go-map-validator/map_validator"
type Login struct {
Email string `json:"email"`
Password string `json:"password"`
}
// Build rules once — safe to reuse across handlers and goroutines.
rules := map_validator.BuildRoles().
SetRule("email", map_validator.Email().WithMax(100)).
SetRule("password", map_validator.Str().Between(6, 30)).
Done()
// HTTP handler: one-liner validate-and-bind
dto, err := map_validator.ValidateJSON[Login](httpReq, rules)
if err != nil { /* handle — ErrNoRules, ErrInvalidJsonFormat, or validation error */ }
// Or validate a plain map (no HTTP):
payload := map[string]interface{}{"email": "dev@example.com", "password": "secret123"}
extra, err := map_validator.NewValidateBuilder().SetRules(rules).Load(payload)
if err != nil { /* handle */ }
result, err := extra.RunValidate()
if err != nil { /* handle */ }
var dto2 Login
_ = result.Bind(&dto2)- Validate map by keys and HTTP JSON/multipart (file upload supported).
- Types via
reflect.Kindwith nullable fields (Null) and default (IfNull). Enum,RegexString,Email,UUID/UUIDToString.- IPv4 validators:
IPV4,IPV4Network(.0 network),IPv4OptionalPrefix(CIDR optional). Min/Maxfor strings, numeric, and slices.- Nested object (
Object) and list of object (ListObject). - Uniqueness across sibling fields (
Unique). - Conditional required:
RequiredWithoutandRequiredIf. - Strict mode to reject unknown keys (
Setting{Strict:true}). - Custom messages for type/regex/min/max/unique/enum value.
- Manipulators to post-process values.
- Extensions lifecycle hooks.
- Short constructors (
Str(),Int(),Email(),UUID(),IPv4(),StrEnum(), …) and chain helpers (.WithMax(),.Nullable(),.Between(),.Regex(), …) for concise rule definitions. ValidateJSON[T]— generic one-liner that collapses the Load → Validate → Bind pipeline for HTTP handlers.- Safe for shared & concurrent use — rules no longer hold per-call state, so a single
rulesvalue can be declared as a package-level var and reused across handlers and goroutines.
All changes are additive on the public API — existing usage patterns keep working.
New APIs
ValidateJSON[T any](r *http.Request, rules RulesWrapper) (T, error)— collapses Load → Validate → Bind into a single call. See One-liner for JSON handlers.- Short constructors:
Str(),Int(),Int64(),Float64(),Bool(),Email(),UUID(),IPv4(),StrEnum(items…),IntEnum(items…),NestedObject(w),ListOfObject(w). - Chain helpers on
Rules:.Nullable(),.Default(v),.WithMin(n),.WithMax(n),.Between(min, max),.Regex(p),.WithMsg(cm),.UniqueFrom(…),.WithRequiredIf(…),.WithRequiredWithout(…). Value-receiver, so each call returns a copy — safe to chain. Done()onRulesWrapper— optional chain terminator soBuildRoles().SetRule(…).Done()reads cleanly.OnEnumValueNotMatchfield inCustomMsgfor custom enum-mismatch messages, plus two new template variables:${actual_value}and${enum_values}.- New error sentinel
ErrNoRulesreturned byLoad/LoadJsonHttp/LoadFormHttpwhen no rules are set.
Behavior changes
SetRules(empty)no longer panics. The error surfaces asErrNoRulesfrom the subsequentLoad*call — easier to handle uniformly.- Enum with an unsupported element kind no longer panics. It returns a normal validation error instead.
- Rules are now safe to share across handlers and goroutines. The previous mutable-state bug inside
rulesWrapperis gone — you can declarerulesas a package-level variable and reuse it freely.
Bug fixes
ListObjectitems now bind into the target struct with their actual field values. Before, items often bound as zero values because of a key-shadowing bug insidevalidateRecursive.
op, err := map_validator.NewValidateBuilder().SetRules(rules).LoadJsonHttp(req)
if err != nil { /* handle */ }
extra, err := op.RunValidate()
if err != nil { /* handle */ }
// Bind to your DTO
var dto MyDTO
_ = extra.Bind(&dto)Note: JSON numbers decode as float64. The validator tolerates integer-family comparisons when rules expect an int kind.
ValidateJSON[T] collapses the Load → Validate → Bind pipeline into a single generic call. Ideal for HTTP handlers where you rarely need the intermediate state.
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
rules := map_validator.BuildRoles().
SetRule("email", map_validator.Email().WithMax(255)).
SetRule("password", map_validator.Str().Between(6, 64)).
Done()
req, err := map_validator.ValidateJSON[LoginRequest](httpReq, rules)
if err != nil {
// forward as-is: ErrNoRules, ErrInvalidJsonFormat, or validation error
return err
}
// req is already validated and boundBefore vs after — same validation, less ceremony:
// before: 4 error checks, intermediate vars
op, err := map_validator.NewValidateBuilder().SetRules(rules).LoadJsonHttp(r)
if err != nil { return err }
extra, err := op.RunValidate()
if err != nil { return err }
var dto LoginRequest
if err := extra.Bind(&dto); err != nil { return err }
// after: one call, one error check
dto, err := map_validator.ValidateJSON[LoginRequest](r, rules)
if err != nil { return err }The 5-step pipeline remains the right choice when you need access to the ExtraOperationData returned by RunValidate() before bind — e.g. reading GetFilledField() / GetNullField() / GetData(), or running custom logic against the validated map before committing it to your struct. Extension lifecycle hooks (BeforeLoad, AfterLoad, BeforeValidation, AfterValidation) still run under ValidateJSON because it internally composes the same LoadJsonHttp + RunValidate calls.
Since rules no longer hold per-call mutable state, the same rules value can be declared as a package-level variable and shared across handlers safely — including concurrent requests.
filter := map_validator.BuildRoles().
SetRule("search", map_validator.Rules{Type: reflect.String, Null: true}).
SetRule("organization_id", map_validator.Rules{UUID: true, Null: true}).
Done()
parent := map_validator.BuildRoles().
SetRule("filter", map_validator.Rules{Object: filter, Null: true}).
SetRule("rows_per_page", map_validator.Rules{Type: reflect.Int64, Null: true}).
SetRule("page_index", map_validator.Rules{Type: reflect.Int64, Null: true}).
SetRule("sort", map_validator.Rules{
Null: true,
IfNull: "FULL_NAME:DESC",
Type: reflect.String,
Enum: &map_validator.EnumField[any]{Items: []string{"FULL_NAME:DESC","FULL_NAME:ASC","EMAIL:ASC","EMAIL:DESC"}},
}).
SetSetting(map_validator.Setting{Strict: true}).
Done()
extra, err := map_validator.NewValidateBuilder().SetRules(parent).Load(map[string]interface{}{}).RunValidate()
_ = extra; _ = erritem := map_validator.BuildRoles().
SetRule("name", map_validator.Rules{Type: reflect.String}).
SetRule("quantity", map_validator.Rules{Type: reflect.Int, Min: map_validator.SetTotal(1)}).
Done()
rules := map_validator.BuildRoles().
SetRule("goods", map_validator.Rules{ListObject: item}).
Done()
payload := map[string]interface{}{
"goods": []interface{}{
map[string]interface{}{"name": "Apple", "quantity": 2},
},
}
_, err := map_validator.NewValidateBuilder().SetRules(rules).Load(payload).RunValidate()
if err != nil { panic(err) }When using ListObject, the input must be an array of objects. Sending a single object yields: "field 'goods' is not valid list object".
rules := map_validator.BuildRoles().
SetRule("password", map_validator.Rules{Type: reflect.String, Null: true}).
SetRule("new_password", map_validator.Rules{Type: reflect.String, Unique: []string{"password"}, Null: true}).
SetRule("flavor", map_validator.Rules{Type: reflect.String, RequiredWithout: []string{"custom_flavor"}}).
SetRule("custom_flavor", map_validator.Rules{Type: reflect.String, RequiredIf: []string{"flavor"}}).
Done()Supported fields in CustomMsg:
OnTypeNotMatch,OnRegexString,OnMin,OnMax,OnUnique,OnEnumValueNotMatch.
Message variables:
${field}: nama field yang divalidasi (key pada rules).${expected_type}: tipe yang diharapkan (hasilreflect.Kind.String()dari rules).${actual_type}: tipe aktual dari nilai yang diterima.${actual_length}: panjang aktual (string: jumlah rune; angka: nilai numerik yang dibandingkan; slice: jumlah elemen).${expected_min_length}: nilai/ukuran minimum yang diharapkan (Min).${expected_max_length}: nilai/ukuran maksimum yang diharapkan (Max).${unique_origin}: nama field asal pada pengecekan unik.${unique_target}: nama field target yang dibandingkan pada pengecekan unik.${actual_value}: nilai aktual yang dikirim (tersedia diOnEnumValueNotMatch).${enum_values}: daftar nilai enum yang diperbolehkan (tersedia diOnEnumValueNotMatch).
rules := map_validator.BuildRoles().
SetRule("total", map_validator.Rules{
Type: reflect.Int,
Min: map_validator.SetTotal(2),
Max: map_validator.SetTotal(3),
CustomMsg: map_validator.CustomMsg{
OnMin: map_validator.SetMessage("Min is ${expected_min_length}, got ${actual_length}"),
OnMax: map_validator.SetMessage("Max is ${expected_max_length}, got ${actual_length}"),
},
}).
Done()Examples:
- Type mismatch
rules := map_validator.BuildRoles().
SetRule("qty", map_validator.Rules{
Type: reflect.Int64,
CustomMsg: map_validator.CustomMsg{
OnTypeNotMatch: map_validator.SetMessage("Field ${field} must be ${expected_type}, but got ${actual_type}"),
},
}).
Done()- Regex validation
rules := map_validator.BuildRoles().
SetRule("email", map_validator.Rules{
RegexString: `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`,
CustomMsg: map_validator.CustomMsg{OnRegexString: map_validator.SetMessage("Your ${field} is not a valid email")},
}).
Done()- Unique across fields
rules := map_validator.BuildRoles().
SetRule("password", map_validator.Rules{Type: reflect.String}).
SetRule("new_password", map_validator.Rules{
Type: reflect.String, Unique: []string{"password"},
CustomMsg: map_validator.CustomMsg{OnUnique: map_validator.SetMessage("The value of '${unique_origin}' must be different from '${unique_target}'")},
}).
Done()- Enum value not in list
rules := map_validator.BuildRoles().
SetRule("status", map_validator.StrEnum("active", "inactive", "pending").
WithMsg(map_validator.CustomMsg{
OnEnumValueNotMatch: map_validator.SetMessage("'${field}' value '${actual_value}' is not allowed; expected one of ${enum_values}"),
})).
Done()
// → "'status' value 'banned' is not allowed; expected one of [active inactive pending]"Notes:
- If a corresponding
CustomMsgis not set, the default error message is used. - Variables are replaced contextually at error time (e.g.,
${field}is the rule’s key). - Currently not customizable: null errors, and specific
RequiredWithout/RequiredIfmessages.
rules := map_validator.BuildRoles().
SetRule("name", map_validator.Rules{Type: reflect.String}).
SetManipulator("name", func(v interface{}) (interface{}, error) {
s := strings.TrimSpace(v.(string))
return strings.ToUpper(s), nil
}).
Done()Manipulators run after validation and before Bind() on the built value tree (including nested/list fields with matching keys).
Implement ExtensionType to hook into load/validate lifecycle. Example scaffold: example_extensions/example.go.
Use-cases:
- Normalize/transform input across many fields.
- Convert string→number for multipart forms.
- Enrich data or apply cross-cutting rules.
Set Setting{Strict:true} in a rules group to reject any unknown keys at that object level. Apply again for nested rules where needed.
- JSON numbers decode as
float64. Integer-family kinds are tolerated on JSON input. LoadFormHttp: non-file values arrive as strings; there is no automatic string→int/float/bool parsing. Use a manipulator or extension to convert.- Email validation is simple (checks
@and.), not full RFC compliance. - Error reporting returns the first encountered error (no multi-error aggregation with field paths yet).
- Custom messages are not yet available for: null errors and specific
RequiredWithout/RequiredIfmessages. - Empty rules no longer panic.
SetRulesaccepts them silently; the subsequentLoad/LoadJsonHttp/LoadFormHttpreturnsErrNoRulesso callers can handle it uniformly.
- Detailed error reporting with field paths and multi-error aggregation.
- URL params extraction helpers.
- Base64 validation.
- Multipart file size limits and image resolution checks.
- OpenAPI spec generator extension.
- Multi-validator per field (e.g., IPv4 + UUID combined).
- Updates/Discussion: Telegram