Categorygithub.com/SharkFourSix/go-struct-validator
modulepackage
0.0.5
Repository: https://github.com/sharkfoursix/go-struct-validator.git
Documentation: pkg.go.dev

# README

go-struct-validator

GitHub tag (latest SemVer) golangci-lint Coverage Status Go Report Card GoDoc

Golang Struct validator.

Features

  • Nested struct validation
  • Activation triggers: Allows selective validation and struct re-use.

Getting Started

A guide on how to quickly get started.

  1. Customize validation options (optional)
  2. Add custom filter/validation functions (optional)
  3. Validate away!
go get github.com/SharkFourSix/go-strutct-validator
package main
import "github.com/SharkFourSix/go-strutct-validator"

type Person {
    Id int `validator:"min(1000)" trigger:"update,delete"` // evaluate only when 'update' or 'delete' triggers have been specified
    Age int `validator:"min(18)|max(65)"` // n >= 18 and n <= 65
    Name string `validator:"max(20)" filter:"upper"` // len(s) <= 20
}

func main(){

    // 1. Customize validation options
    validator.SetupOptions(func(opts *validator.ValidationOptions){
        // override options here. See available options
    })

    // 2.Add custom validation and filter functions
    validator.AddFilter("upper", func(ctx *validator.ValidationContext) reflect.Value{
        ctx.ValueMustBeOfKind(reflect.String)

        if !ctx.IsNull {
            stringValue := strings.ToUpper(ctx.GetValue().String())
            if ctx.IsPointer {
                ctx.GetValue().Set(&stringValue)
            }else{
                ctx.GetValue().SetString(stringValue)
            }
        }

        return ctx.GetValue()
    })

    person := Person{Age: 20, Name: "Bames Jond"}

    // validate
    result := validator.Validate(&person)
    if result.IsValid() {
        fmt.Println("validation passed")
    }else{
        fmt.Println(result.Error)
        fmt.Println(result.FieldErrors)
    }
}

Design Philosophy

This librabry provides validation functionality for structs only. It does not support data binding.

Each validation rule correspods to a function, which accepts a validator.ValidationContext, containing the value to validate as well as any arguments that the function may require.

Validating required vs optional values

The contract for validating pointer values is to first inspect whether the pointer is null (validationContext.IsNull), returning true if that is the case, implying an optional value, or, continuing with the validation in case the pointer is not null.

The contract for validating literal values is to inspect the values and perform validation logic accordingly.

Validation functions and filters

Both validation and filter functions accept the same input parameter validator.ValidationContext.

Validation functions return true or false (bool) to indicate whether the validation test passed or failed. If the validation function wishes to provide an error message, it may do so through validator.ValidationContext.ErrorMessage.

The goal of filter functions is to allow transforming data into desired and suitable formats.

NOTE: Because this is not a data binding library, filters may not change the data type since the type of the input value cannot be changed.

Filters return reflect.Value, which may be a newly allocated value or simply the same value found stored in validator.ValidationContext.value.

To access the input value within a filter or validator, call ValidationContext.GetValue(), which will return the underlying value (reflect.Value), resolving pointers (1 level deep) if necessary.

To check the type of the input value, you can use ValidationContext.IsValueOfKind(...reflect.Kind) or ValidationContext.IsValueOfType(inteface{}).

Sample validator

func MyValidator(ctx *validator.ValidationContext) bool {
    // First always check if the value is (a pointer and) null.
    if ctx.IsNull {
        // treat this as an optional field. if the caller decides otherwise, the first validtor in the list will be "requried"
        return true
    }

    // ..check supported type (will panic)
    ctx.MustBeOfKind(reflect.String)

    // or only check for supported types only without needing to panic
    if ctx.IsValueOfKind(reflect.String) {
        myString := ctx.GetValue().String()

        // apply validation logic
        if !strings.HasSuffix(myString, ".com") {

            // provide an optional error message. The validation orchestrator will set one for you if you do not specify one
            ctx.ErrorMessage = "only .com domains are allowed"

            return false
        }
    }else{
        // panic because this is not a validation error, rather a type/low level error that needs to be fixed
        panic("unsupported type " + ctx.ValueKind.String())
    }

    return true
}

Sample filter

func InPlaceMutationFilter(ctx *validator.ValidationContext) reflect.Value {

    // Check supported type (will panic)
    ctx.MustBeOfKind(reflect.Int)

    // always check if the value is (a pointer and) null.
    if !ctx.IsNull {
        myNumber := ctx.GetValue().Int()
        myNumber = myNumber * myNumber

        // update the value in place
        if ctx.IsPointer {
            ctx.GetValue().Set(&myNumber)
        }else{
            ctx.GetValue().Set(myNumber)
        }
    }

    return ctx.GetValue()
}
func NewValueFilter(ctx *validator.ValidationContext) reflect.Value {

    // Check supported type (will panic)
    ctx.MustBeOfKind(reflect.Int)

    value := ctx.GetValue()

    // always check if the value is (a pointer and) null.
    if !ctx.IsNull {
        myNumber := value.Int()
        myNumber = myNumber * myNumber

        // return a new value
        if ctx.IsPointer {
            value = reflect.ValueOf(&myNumber)
        }else{
            value = reflect.ValueOf(myNumber)
        }
    }

    return value
}

Validation flags

Validation flags control the validation behavior per input value.

type MyStruct struct {
    Foo *string `validate:"min(5)|max(50)" flags:"allow_zero"`
}

myStruct := MyStruct {}
result := validator.Validate(&myStruct)
fmt.Println(result.IsValid()) // --> true

Refer to Packaged Flags

Execution order and activation

Selective Validation

Sometimes you may wish to use the same struct but only work with specific fields in specific cases. Instead of creating a struct for each use case, you can use activation triggers to selectively evaluate those specific fields.

To specify an activation trigger, include the name of the trigger in the trigger tag.

NOTE Trigger names can be anything.

A special activation trigger 'all' exists which causes a field to be evaluated in all use cases. Omitting the trigger tag is equivalent to explicitly specifying ONLY this special value.

type ResourceRequest struct {
    ResourceId string `validator:"uuidv4" trigger:"update,delete"`
    ResourceName string `validator:"min(3)|max(50)"`
}

myResource := ResourceRequest{}

// get from some http request
httpRequestDataBinder.BindData(&myResource)

// making the following call validates .ResourceName
validator.Validate(&myResource, "create")

// ... later on when updating the resource name, both .ResourceId and .ResourceName
// will get evaludated
validator.Validate(&myResource, "update")

Execution Order

Validators are evaluated first and filters last.

Accessing validation errors

validator.ValidationResults.IsValid() indicates whether validation succeeded or not. If validation did not exceed, you are guaranteed to have at least one validation error in validator.ValidationResults.FieldErrors.

Each field error contains the label take from the field name or label tag, and error message returned by the failing validation function, or taken from the message tag or default generic error message if none of the former options were specified.

func main(){
    type Person {
        Age int `validator:"min(18)|max(65)" message:"You're too young or too old for this"`
        Name string `validator:"min(20)" filter:"upper" message:"Your name is too long" label:"Candidate name"`
    }

    person := Person{Age: 16, Name: "Bames Jond"}

    // validate
    result := validator.Validate(&person)
    if result.IsValid() {
        fmt.Println("validation passed")
    }else{
        fmt.Println(result.Error)
        fmt.Println(result.FieldErrors)
    }
}

Packaged validators

NameFunctionParameters
requiredIsRequired
alphanumIsAlphaNumeric
uuid1IsUuid1
uuid2IsUuid2
uuid3IsUuid3
uuid4IsUuid4
minIsMin(number)
maxIsMax(number)
enumIsEnum(...string)
emailIsEmail
at_least_todayIsOrBeforeToday(dateLayout) - optional
at_most_todayIsOrAfterToday(dateLayout) - optional
todayIsToday(dateLayout) - optional
before_todayIsBeforeToday(dateLayout) - optional
after_todayIsAfterToday(dateLayout) - optional

Packaged filters

NameFunctionParametersDescription
trimTrimTrim string space

Packaged flags

NameDescription
allow_zeroskips validation of values that match zero values

Validation options

Refer to validator.ValidationOptions to see list of options in validator.go

Documentation

https://pkg.go.dev/github.com/SharkFourSix/go-struct-validator#section-documentation

Contribution

Contributions are welcome

Inspiration taken from https://github.com/gookit/validate

# Functions

AddFilter adds the given filter function to the list of filters The backed storage containing the list of filters is not thread safe and so this function must be called once during package or application initialization.
AddValidator adds the given validator function to the list of validators The backed storage containing the list of validators is not thread safe and so this function must be called once during package or application initialization.
CopyOptions CopyOptions Copies the default global options into the specified destination.
IsAfterToday tests whether the given date is after today.
IsAlphaNumeric verifies that the given string is alphanumeric.
IsBeforeToday tests whether the given date is before today.
IsEmail tests if the input value matches an email format.
IsEnum tests if the input value matches any of the values passed in the arguments.
IsMax tests if the given input (string, integer, list) contains at least the given number of elements.
IsMin tests if the given input (string, integer, list) contains at least the given number of elements.
IsNotToday tests whether the given date is not today.
IsOrAfterToday tests whether the given date is today or after today.
IsBeforeToday tests whether the given date is today or before today.
IsRequired check if the required field has values.
IsToday tests whether the given date is today.
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
NullIfEmpty Sets the given string pointer's value to null if the string is empty.
SetupOptions SetupOptions allows you to configure the global validation options.
No description provided by the author
Validate validates the given struct # Parameters structPtr : Pointer to a struct trigger : Activation trigger - Specifies a unique value that will trigger activation of fields that have been taggeed with the same value.

# Constants

If a value contains zero value, allow the value to pass through by skipping validation since there's nothing to validate or filter.
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Structs

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
ValidationResult contains validation status, a general error, and field errors.

# Type aliases

No description provided by the author
No description provided by the author
FilterFunction FilterFunction is used to manipulate input values.
No description provided by the author
Flags used by the validation engine before calling validations and filters.
ValidationFunction ValidationFunction is used to validate input.