go-playground / validator

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

Validator not enforced on embedded type #1178

Closed eugennicoara closed 11 months ago

eugennicoara commented 12 months ago

Package version eg. v9, v10:

v10

Issue, Question or Enhancement:

With reference to Exhibit A, the unit test should not pass because the Retry attribute is not present (line A - set to zero value) under Policies. Commenting out the Retry attribute under DefaultPolicy (line B) will cause the test to fail as expected.

With reference to Exhibit B, the only output after running the unit test is something like time="2023-10-11T16:33:44-06:00" level=error msg="retry, 53s" which I think proves it's not even trying to run the validator for PolicyParameters.

Why isn't the validator enforced on the embedded type?

Code sample, to showcase or reproduce:

Exhibit A:

package main

import (
    "testing"
    "time"

    vld "github.com/go-playground/validator/v10"
    "github.com/stretchr/testify/assert"
)

type FileConfiguration struct {
    Policies      []PolicyConfig   `validate:"omitempty"`
    DefaultPolicy PolicyParameters `validate:"required"`
}

type PolicyConfig struct {
    Name string `validate:"required"`

    PolicyParameters `validate:"required"`
}

type PolicyParameters struct {
    FailureAction string        `validate:"required,oneof=retry ignore"`
    Retry         time.Duration `validate:"required_if=FailureAction retry"`
}

func TestConfig(t *testing.T) {
    tests := []struct {
        name   string
        config FileConfiguration
    }{
        {
            name: "Default policy w/ retry action and named policy w/ retry action",
            config: FileConfiguration{
                Policies: []PolicyConfig{
                    {
                        Name: "A policy",
                        PolicyParameters: PolicyParameters{
                            FailureAction: "retry",
                            // Retry:         33 * time.Second, // line A
                        },
                    },
                },
                DefaultPolicy: PolicyParameters{
                    FailureAction: "retry",
                    Retry:         63 * time.Second, // line B
                },
            },
        },
    }

    validator := vld.New(vld.WithRequiredStructEnabled())

    for _, d := range tests {
        t.Run(d.name, func(t *testing.T) {
            assert.NoError(t, validator.Struct(d.config))
        })
    }
}

Exhibit B:

package main

import (
    "testing"
    "time"

    vld "github.com/go-playground/validator/v10"
    log "github.com/sirupsen/logrus"
    "github.com/stretchr/testify/assert"
)

type FileConfiguration struct {
    Policies      []PolicyConfig   `validate:"omitempty"`
    DefaultPolicy PolicyParameters `validate:"required"`
}

type PolicyConfig struct {
    Name string `validate:"required"`

    PolicyParameters `validate:"required"`
}

type PolicyParameters struct {
    FailureAction string        `validate:"required,oneof=retry ignore"`
    Retry         time.Duration `validate:"durationOnRetry"`
}

func TestConfig(t *testing.T) {
    tests := []struct {
        name   string
        config FileConfiguration
    }{
        {
            name: "Default policy w/ retry action and named policy w/ retry action",
            config: FileConfiguration{
                Policies: []PolicyConfig{
                    {
                        Name: "A policy",
                        PolicyParameters: PolicyParameters{
                            FailureAction: "retry",
                            Retry:         35 * time.Second, // line A
                        },
                    },
                },
                DefaultPolicy: PolicyParameters{
                    FailureAction: "retry",
                    Retry:         53 * time.Second, // line B
                },
            },
        },
    }

    validator := vld.New(vld.WithRequiredStructEnabled())
    err := validator.RegisterValidation("durationOnRetry", func(fl vld.FieldLevel) bool {
        p := fl.Parent()

        fa, ok := p.FieldByName("FailureAction").Interface().(string)
        if !ok {
            return false
        }
        if fa != "retry" {
            return true
        }

        r, ok := p.FieldByName("Retry").Interface().(time.Duration)
        if !ok {
            return false
        }
        if r < 30*time.Second {
            return false
        }

        log.Errorf("%s, %s", fa, r)

        return true
    })

    assert.NoError(t, err)

    for _, d := range tests {
        t.Run(d.name, func(t *testing.T) {
            assert.NoError(t, validator.Struct(d.config))
        })
    }
}
deankarn commented 12 months ago

Exhibit A - you are missing the dive tag on your Policies field, see the documentation, validator does not automatically dive into arrays, slices or maps to allow validation at the top level and then on internal contents.

type FileConfiguration struct {
    Policies      []PolicyConfig   `validate:"omitempty,dive"`
    DefaultPolicy PolicyParameters `validate:"required"`
}

Exhibit B - If I understand the example and you mean the PolicyParameters within the []PolicyConfig then same as Exibit A, missing dive.

eugennicoara commented 11 months ago

Excellent! I can confirm adding dive solved the problem. Thank you @deankarn !