Skip to content

decimal.Decimal field can not be parsed correctly. #102

@vx416

Description

@vx416

problem description
Before validating a struct, the validator will parse this struct into a map struct. During the parsing process, if the kind of struct's field is struct type, the parsing function will parse this field recursively. However, some of the struct fields should not be parsed recursively e.g decimal.Decimal, time.Time.

demo code

func init() {
	govalidator.AddCustomRule("gtzero_decimal", func(field string, rule string, message string, value interface{}) error {
		d := value.(decimal.Decimal)
		if d.IsZero() || d.IsNegative() {
			return fmt.Errorf("The %s field must be greater than zero", field)
		}
		return nil
	})
}

var CreateProductRules = govalidator.MapData{
	"name":      []string{"required", "max:20"},
	"price":     []string{"gtzero_decimal"},
}


type Product struct {
	Name      string          `json:"name"`
	Price     decimal.Decimal `json:"price"`
}

func TestValidateProduct(t *testing.T) {
  	opts := govalidator.Options{
		Data:  &Product{Name:"test", Price: decimal.Zero},
		Rules: CreateProductRules,
	}
	v := govalidator.New(opts)
	v.SetTagIdentifier("json")
	e := v.ValidateStruct()
        assert.Len(e, 1)
}


type CustomValuer struct {
	value string
}

func (v CustomValuer) Value() (driver.Value, error) {
	return v.value, nil
}

type structWithValuerAndTime struct {
	Name    string        `json:"name"`
	Number  int64         `json:"number"`
	Time    time.Time     `json:"time"`
	NullInt sql.NullInt64 `json:"nullInt"`
	Value   CustomValuer  `json:"value"`
}

func TestRoller_StartWithValuerAndTimeField(t *testing.T) {
	r := roller{}
	s := structWithValuerAndTime{
		Name:    "John Doe",
		Number:  1,
		Time:    time.Now(),
		NullInt: sql.NullInt64{Valid: true, Int64: 1},
		Value:   CustomValuer{value: "test"},
	}
	r.setTagIdentifier("json")
	r.setTagSeparator("|")
	r.start(&s)
	if len(r.getFlatMap()) != 5 {
		t.Error("failed to push valuer and time type")
	}
}

solution

// isValuer check the filed implement driver.Valuer or not
func isValuer(field reflect.Value) (driver.Valuer, bool) {
	var fieldRaw interface{}
	fieldRaw = field.Interface()
	if scanner, ok := fieldRaw.(driver.Valuer); ok {
		return scanner, ok
	}
	if field.CanAddr() {
		fieldRaw = field.Addr().Interface()
	}
	if scanner, ok := fieldRaw.(driver.Valuer); ok {
		return scanner, ok
	}
	return nil, false
}

// traverseStruct through all structs and add it to root
func (r *roller) traverseStruct(iface interface{}) {
	ifv := reflect.ValueOf(iface)
	ift := reflect.TypeOf(iface)

	if ift.Kind() == reflect.Ptr {
		ifv = ifv.Elem()
		ift = ift.Elem()
	}

	for i := 0; i < ift.NumField(); i++ {
		v := ifv.Field(i)
		rfv := ift.Field(i)

		switch v.Kind() {
		case reflect.Struct:
			var typeName string
			if len(rfv.Tag.Get(r.tagIdentifier)) > 0 {
				tags := strings.Split(rfv.Tag.Get(r.tagIdentifier), r.tagSeparator)
				if tags[0] != "-" {
					typeName = tags[0]
				}
			} else {
				typeName = rfv.Name
			}
			if v.CanInterface() {
				switch v.Type().String() {
				case "govalidator.Int":
					r.push(typeName, v.Interface())
				case "govalidator.Int64":
					r.push(typeName, v.Interface())
				case "govalidator.Float32":
					r.push(typeName, v.Interface())
				case "govalidator.Float64":
					r.push(typeName, v.Interface())
				case "govalidator.Bool":
					r.push(typeName, v.Interface())
				case "time.Time":
					r.push(typeName, v.Interface())
				default:
					if _, ok := isValuer(v); ok {
						r.push(typeName, v.Interface())
					} else {
						r.typeName = ift.Name()
						r.traverseStruct(v.Interface())
					}
				}
			}
....

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions