#4733·gin

Allow configuring the binding validator with go-playground/validator Options (e.g. WithRequiredStructEnabled)

Author: HoffsCreated Jul 5, 2026Updated Aug 4, 2026

Feature Description

There is currently no first-class way to construct gin's default binding validator with go-playground/validator Options — most importantly WithRequiredStructEnabled().

Why this matters

WithRequiredStructEnabled() is what makes the required tag apply to non-pointer struct fields. Without it, required is silently a no-op on any value-typed struct field — wrapper types such as a UUID or Decimal that are defined as type X struct{...}. So a request that omits such a field binds its zero value and passes validation, instead of failing with a 400:

type CreateReq struct {
    ID  MyUUID `json:"id" binding:"required"` // MyUUID is a struct
}
// {} binds successfully — `required` never fires.

Why it can't be opted into today

The option is constructor-only. In go-playground/validator it sets a private field with no setter, so it can only be enabled via validator.New(validator.WithRequiredStructEnabled()):

// validator_instance.go
requiredStructEnabled  bool
// options.go
func WithRequiredStructEnabled() Option { return func(v *Validate) { v.requiredStructEnabled = true } }

But gin's defaultValidator.lazyinit() hardcodes validator.New() with no options:

// binding/default_validator.go
func (v *defaultValidator) lazyinit() {
    v.once.Do(func() {
        v.validate = validator.New()
        v.validate.SetTagName("binding")
    })
}

binding.Validator.Engine() returns the already-constructed *validator.Validate, which is too late — the option must be passed at construction, and there is no setter to flip it afterwards. Because defaultValidator (and its validate field) are unexported, the only way to enable the option is to replace binding.Validator entirely with a custom StructValidator, re-implementing ValidateStruct's pointer/slice/array dispatch just to pass one option:

type myValidator struct{ v *validator.Validate }
func (m *myValidator) ValidateStruct(obj any) error { /* copy of gin's ptr/slice/struct dispatch */ }
func (m *myValidator) Engine() any { return m.v }
// binding.Validator = &myValidator{v: validator.New(validator.WithRequiredStructEnabled())}

That's a lot of copied boilerplate for a common need.

Proposal

Provide a first-class way to construct the default validator with options. Any of:

  1. An exported constructor for the default validator, e.g. binding.NewStructValidator(opts ...validator.Option) StructValidator, so users can do binding.Validator = binding.NewStructValidator(validator.WithRequiredStructEnabled()) without re-implementing ValidateStruct.
  2. A package-level setter such as binding.SetValidatorOptions(opts ...validator.Option) applied on lazyinit.
  3. Exporting defaultValidator (or accepting options on it) so the ptr/slice dispatch can be reused.

Option 1 seems the least invasive and keeps gin's existing ValidateStruct behavior intact.

Tested with gin v1.12.0 and go-playground/validator v10.30.2.