go-playground / validator

:100:Go Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving
MIT License
16.53k stars 1.31k forks source link

Use generics with the RegisterValidation ? #1065

Open goriunov opened 1 year ago

goriunov commented 1 year ago

Package version eg. v9, v10:

v10

Issue, Question or Enhancement:

I am trying to implement an optional type for the API and integrate the validator with generics unfortunatly i do not see any way to use just generics on the validator. So far using RegisterCustomTypeFunc I was able to make it work by passing all the possible types of the generic.

Code sample, to showcase or reproduce:


import (
    "encoding/json"
)

type Optional[T any] struct {
    Value T
    Null  bool
    Set   bool
}

func (i *Optional[T]) UnmarshalJSON(data []byte) error {
    // If this method was called, the value was set.
    i.Set = true

    if string(data) == "null" {
        // The key was set to null
        i.Null = true
        return nil
    }

    // The key isn't set to null
    var temp T
    if err := json.Unmarshal(data, &temp); err != nil {
        return err
    }
    i.Value = temp
    i.Null = false
    return nil
}

type Client struct {
    Id       Optional[string]         `json:"id" validate:"required,min=10"`
    Location Optional[Location] `json:"location" validate:"required,dive"`
}

type Location struct {
    Type        string `json:"type,omitempty" validate:"omitempty,enum"`
    Coordinates string `json:"coordinates,omitempty" validate:"omitempty,enum"`
    Address     string      `json:"address,omitempty" validate:"omitempty"`
}

Ideally I would love to be able to register in in the way like:

validate.RegisterCustomTypeFunc(customValuer, opt.Optional[any]{})

func customValuer(field reflect.Value) interface{} {
    if valuer, ok := field.Interface().(Optional[any]); ok {
        return valuer.Value
    }
    return nil
}

Or at least would be great to do something similar to RegisterValidation where I could just specify custom tag for the optional fields and return the value that would be validated instead of Boolean eg:

validate.RegisterSomethingCool("customOptional", func(f validator.FieldLevel) interface{} {
        value := f.Field().Interface().(Optional[any])
        return value.Value
})
arobert93 commented 10 months ago

Any solution for this?

dropwhile commented 8 months ago

I ran into the same issue.

Here is an example diff of a possible solution:

diff --git a/util.go b/util.go
index 1685159..51f3000 100644
--- a/util.go
+++ b/util.go
@@ -51,6 +51,13 @@ BEGIN:
            }
        }

+       if v.v.hasCustomMatcherFunc {
+           if nc := v.v.customMatcherFunc(current); nc != nil {
+               current = reflect.ValueOf(nc)
+               goto BEGIN
+           }
+       }
+
        return current, current.Kind(), nullable
    }
 }
diff --git a/validator_instance.go b/validator_instance.go
index d5a7be1..8e7f5c1 100644
--- a/validator_instance.go
+++ b/validator_instance.go
@@ -85,6 +85,7 @@ type Validate struct {
    tagNameFunc           TagNameFunc
    structLevelFuncs      map[reflect.Type]StructLevelFuncCtx
    customFuncs           map[reflect.Type]CustomTypeFunc
+   customMatcherFunc     CustomTypeFunc
    aliases               map[string]string
    validations           map[string]internalValidationFuncWrapper
    transTagFunc          map[ut.Translator]map[string]TranslationFunc // map[<locale>]map[<tag>]TranslationFunc
@@ -92,6 +93,7 @@ type Validate struct {
    tagCache              *tagCache
    structCache           *structCache
    hasCustomFuncs        bool
+   hasCustomMatcherFunc  bool
    hasTagNameFunc        bool
    requiredStructEnabled bool
 }
@@ -337,6 +339,14 @@ func (v *Validate) RegisterCustomTypeFunc(fn CustomTypeFunc, types ...interface{
    v.hasCustomFuncs = true
 }

+// RegisterCustomMatcherFunc registers a CustomTypeFunc for manual conversion
+//
+// NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation
+func (v *Validate) RegisterCustomMatcherFunc(fn CustomTypeFunc) {
+   v.customMatcherFunc = fn
+   v.hasCustomMatcherFunc = true
+}
+
 // RegisterTranslation registers translations against the provided tag.
 func (v *Validate) RegisterTranslation(tag string, trans ut.Translator, registerFn RegisterTranslationsFunc, translationFn TranslationFunc) (err error) {

Example usage:

validate.RegisterCustomMatcherFunc(DriverValuer)

func DriverValuer(field reflect.Value) interface{} {
    if valuer, ok := field.Interface().(driver.Valuer); ok {
        if val, err := valuer.Value(); err == nil {
            return val
        }
    }
    return nil
}
stonymahony commented 6 months ago

I have exactly the same problem as many I think who try to implement a PATCH API where you have to distinguish between null and undefined and therefore use such a generic based optional type.

Is there an ideomatic "official" solution for using it with validator?

deankarn commented 6 months ago

There is a discussion here about adding the ability, haven’t had time to play around with potential solutions yet https://github.com/go-playground/validator/discussions/1232

renom commented 1 month ago

Any solution?