Skip to content

model validation

spaceweasel edited this page Apr 17, 2019 · 5 revisions

Model Validation

Model validation goes in hand with model binding. Once you have a populated model, you will normally want to validate the properties before persisting the object into a database. Although client-side checking probably exists, it can never be relied upon; you will need to check string lengths, missing or out of range values etc. Mango has two approaches to help with validation, but they both achieve the same goal of keeping your handlers clean and clear of clutter.

Struct Tag Validation

The simplest method of model validation is to use validate struct tags with suitable validators. The validator tags can contain multiple validators, but they must be compatible with the type of property being validated. All that is required in the handler is to call the context's Validate method and check the result:

type Person struct {
	FirstName string `json:"firstName" validate:"alpha,lenrange(2,30)"`
	LastName  string `json:"lastName" validate:"alpha,lenrange(2,30)"`
	Age       int    `json:"age" validate:"notzero"`
}

//...
	r.Post("/persons", func(c *mango.Context) {
		var p Person
		c.Bind(&p) 
		fails, ok := c.Validate(p)
		if !ok {
			c.RespondWith(fails).WithStatus(http.StatusBadRequest)
			return
		}		
		// TODO: Save p to a database...		
		c.RespondWith(p).WithStatus(http.StatusCreated)
	})

If the validation is unsuccessful, fails holds information about the nature of the failures, which can be passed straight back to the client, and serialized in the same way any other model data would be. For example, if the client tried to POST:

{
  "firstName": "J",
  "lastName": "9"
}

The FirstName is too short and the LastName is too short and not an alpha character. In addition, the Age has not been set. The response from the service would look like this:

{
    "Age": [
        {
            "Code": "notzero",
            "Message": "Must not be zero."
        }
    ],
    "FirstName": [
        {
            "Code": "lenrange(2,30)",
            "Message": "Must have a quantity of elements within the permitted range."
        }
    ],
    "LastName": [
        {
            "Code": "alpha",
            "Message": "Must contain only alpha characters."
        },
        {
            "Code": "lenrange(2,30)",
            "Message": "Must have a quantity of elements within the permitted range."
        }
    ]
}

This enables the client to have a good chance of rectifying the issues.

Nested Struct Tag Validation

Struct tag validation is recursive, so it can validate nested structs, maps and slices/arrays.

type Vehicle struct {
	Make  string `json:"make" validate:"alpha,lenrange(2,25)"`
	Model string `json:"model" validate:"lenrange(1,15)"`
}

type Person struct {
	FirstName string    `json:"firstName" validate:"alpha,lenrange(2,30)"`
	LastName  string    `json:"lastName" validate:"alpha,lenrange(2,30)"`
	Age       int       `json:"age" validate:"notzero"`
	Vehicles  []Vehicle `json:"vehicles" validate:"notempty"`
}

The notempty validator ensures that vehicles is not an empty slice, and that each vehicle has a Make which holds between 2 and 25 alpha characters. notempty can be used on String, Slice, Array and Map. However, sometimes you might not want to validate nested items, in which case you can use the validation instruction ignorecontents.

type Vehicle struct {
	Make  string `json:"make" validate:"alpha,lenrange(2,25)"`
	Model string `json:"model" validate:"lenrange(1,15)"`
}

type Person struct {
	FirstName string          `json:"firstName" validate:"alpha,lenrange(2,30)"`
	LastName  string          `json:"lastName" validate:"alpha,lenrange(2,30)"`
	Age       int             `json:"age" validate:"notzero"`
	FavCars   map[int]Vehicle `json:"vehicles" validate:"ignorecontents,notempty"`
}

The example above will prevent any validation of the Vehicles in the map (and any nested properties they might have), but it is still permitted to include validators for the FavCars container itself. So even though the properties of the individual vehicles are ignored, Mango will still validate that the map is not empty.

Struct Tag Validation With Pointer Types

Struct tag validation can also be used with pointer properties. The most basic validation is to prevent a nil pointer:

type Vehicle struct {
	Make  string `json:"make" validate:"alpha,lenrange(2,25)"`
	Model string `json:"model" validate:"lenrange(1,15)"`
}

type Person struct {
	FirstName string    `json:"firstName" validate:"alpha,lenrange(2,30)"`
	LastName  string    `json:"lastName" validate:"alpha,lenrange(2,30)"`
	Age       int       `json:"age" validate:"notzero"`
	Vehicles  *Vehicle  `json:"vehicle" validate:"notnil,ignorecontents"`
}

Most of the included validators for primitives can work with their pointer counterparts too, but you need to prefix the validators with a star:

type Vehicle struct {
	Make  *string `json:"make" validate:"*alpha,*lenrange(2,25)"`
	Model string  `json:"model" validate:"lenrange(1,15)"`
}

type Person struct {
	FirstName string    `json:"firstName" validate:"alpha,lenrange(2,30)"`
	LastName  string    `json:"lastName" validate:"alpha,lenrange(2,30)"`
	Age       int       `json:"age" validate:"notzero"`
	Vehicles  *Vehicle  `json:"vehicle" validate:"notnil"`
}

This example ensures that the vehicles property points to a Vehicle, and validates the vehicles properties too. Notice the Make string pointer validators are prefixed with a star. If you try to use a normal validator on a pointer type then you will get a panic. For example, omitting the star from the alpha validator of a string pointer property:

type Vehicle struct {
	Make  *string `json:"make" validate:"alpha,*lenrange(2,25)"`
	Model string  `json:"model" validate:"lenrange(1,15)"`
}

panic: alpha validator can only validate strings not, *string

If you see this type of error message ... validator can only validate <type> not, *<type> then you have probably forgotten the star prefix.

Custom Model Validation

Struct tag validation is simple to add and Mango validation makes it easy to give clients detailed information about validation failures without any great effort on your part. Validation tags have their limitations though, for one, they can only operate on a single property so if your validation is based on two properties (e.g. property A or property B must have a value), then this cannot be performed with the standard validation tags (although you can create your own). Another approach is to create a custom model validator by creating a function based on the ValidateFunc interface:

type ValidateFunc func(m interface{}) (map[string][]ValidationFailure, bool)

Once you have created the function, you can add it to the router using the AddModelValidator method.

Example

type Person struct {
	FirstName string `json:"firstName"`
	LastName  string `json:"lastName"`
	Age  int `json:"age"`
}

//...
	r.Post("/persons", func(c *mango.Context) {
		var p Person
		c.Bind(&p) 
		fails, ok := c.Validate(p) 
		if !ok {
			c.RespondWith(fails).WithStatus(http.StatusBadRequest)
			return
		}		
		// TODO: Save p to a database...		
		c.RespondWith(p).WithStatus(http.StatusCreated)
	})

// no changes required to handler, it is the same as if using validator tags,
// but we need a custom model validator:

	var personValidator = func(m interface{}) (map[string][]mango.ValidationFailure, bool) {
		// make the map that will hold any failure details
		results := make(map[string][]mango.ValidationFailure)
		// the model m is passed as an interface, so you'll need to use
		// type assertion to convert it
		p := m.(Person)
		// now carry out the contrived validations on properties of interest...
		if strings.Count(p.LastName, "e")>3 {
			results["LastName"] = []mango.ValidationFailure{
				{Code: "3emax",
					Message: "Must have 3 E's or fewer"},
			}
		}
		// another odd validation rule, involving two properties
		if len(p.FirstName) > len(p.LastName){
			results["FirstName"] = []mango.ValidationFailure{
				{Code: "firstshorterthanlast",
					Message: "Must be shorter than LastName"},
			}
		}
		// other property validations...
		
		return results, len(results) == 0
	}
	
	//...
	// add the validator to the router, passing an empty model instance
	// and the custom model validator function to AddModelValidator().
	r.AddModelValidator(Person{}, personValidator)

	// now whenever context Validate(p) method is called with a Person object,
	// the validation will be handled by the personValidator.

Try to prevent too much (any) business logic creep into your validators. Their primary purpose is to filter out any input which could be harmful or cause problems with processing further down the line, e.g. strings which are empty or too long to fit into a database field. If your validator has logic which borders on business logic you might want to consider moving it to another area of code.