Improved validation with generics in Go

go

I’m currently developing a webservice in Go for a customer, where I need to write a lot of HTTP endpoints. Most of them have a very similar structure:

  1. Binding request data (path parameter, query parameter, body parameter) to a var
  2. Validate data
  3. Work with data, mostly CRUD
  4. Return response

While the Echo framework already provides some useful helper to bind and plug in a validator, my function headers always hold the same boilerplate code:


type mgmtImportRequest struct {
    Email string `json:"foo" validate:"required,email"`
}

// handleMgmtImport
func (hs *HTTPServer) handleMgmtImport(c echo.Context) error {
	u := new(mgmtImportRequest)
	if err := c.Bind(u); err != nil {
        return c.String(http.StatusBadRequest, "bad request"
    }
    if err = c.Validate(u); err != nil {
      return err
    }

	// [...]
}

While binding and validation can be combined in one call, I still do not like to have this at every single function call.

Looking at other (new-ish) frameworks in other languages, I like how the validation is already part of the route definition. For example, this is how a route definition in FastAPI can look like:

from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None

app = FastAPI()

@app.post("/items/")
async def create_item(item: Item):
    return item

Passing Item here will bind and validate at once.

Or in JavaScript with fastify:

fastify.route({
  method: 'GET',
  url: '/',
  schema: {
    // request needs to have a querystring with a `name` parameter
    querystring: {
      type: 'object',
      properties: {
          name: { type: 'string'}
      },
      required: ['name'],
    },
    // the response needs to be an object with an `hello` property of type 'string'
    response: {
      200: {
        type: 'object',
        properties: {
          hello: { type: 'string' }
        }
      }
    }
  },
  // this function is executed for every request before the handler is executed
  preHandler: async (request, reply) => {
    // E.g. check authentication
  },
  handler: async (request, reply) => {
    return { hello: 'world' }
  }
})

So, how can we have those nice things in Go, too?

Let’s approach it with generics. We start with the example from the Echo docs:

package main

import (
  "net/http"

  "github.com/go-playground/validator"
  "github.com/labstack/echo/v4"
  "github.com/labstack/echo/v4/middleware"
)

type (
  User struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
  }

  CustomValidator struct {
    validator *validator.Validate
  }
)

// [1]
func (cv *CustomValidator) Validate(i interface{}) error {
  if err := cv.validator.Struct(i); err != nil {
    // Optionally, you could return the error to give each route more control over the status code
    return echo.NewHTTPError(http.StatusBadRequest, err.Error())
  }
  return nil
}

func main() {
  e := echo.New()
  e.Validator = &CustomValidator{validator: validator.New()} // [2]
  e.POST("/users", func(c echo.Context) (err error) {
    u := new(User)

    // [3]
    if err = c.Bind(u); err != nil {
      return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }

    // [4]
    if err = c.Validate(u); err != nil {
      return err
    }

    // [5]
    return c.JSON(http.StatusOK, u)
  })
  e.Logger.Fatal(e.Start(":1323"))
}

To summarize what is happening here:

  1. A custom validator is defined
  2. The validator is plugged in echo, allowing to call Validate on the echo.Context. This will directly call into the validator package.
  3. Request data is bound to a struct. This allows using struct tags like json:"foo" to define where request data is coming from.
  4. The validator is executed, and if a validation error occured, it is returned (and as can be seen in [1] transformed to a http.StatusBadRequest with an validation error message).
  5. A JSON response with the request input data is responded.

The first step is now to move binding and validation into some kind of middleware. Echo has ways to implement middleware, but we will go a different path here so we can later on also pass a type. What I am aiming for is a route definition which looks like this:

e := echo.New()
e.POST("/users", validated[User](users))

This way, I could annotate my route handling function with a (validation) type it has to adhere to.

Let’s create the binding/validation first:

package main

import (
	"net/http"

	"github.com/go-playground/validator/v10"
	"github.com/labstack/echo/v4"
)

type User struct {
	Name  string `json:"name" validate:"required"`
	Email string `json:"email" validate:"required,email"`
}

func users(c echo.Context, u User) (err error) {
	return c.JSON(http.StatusOK, u)
}

func main() {
	e := echo.New()
	e.POST("/users", validatedUser(users))

	e.Logger.Fatal(e.Start(":1323"))
}

func validatedUser(h func(echo.Context, User) error) echo.HandlerFunc {
	var validate *validator.Validate
	validate = validator.New(validator.WithRequiredStructEnabled())

	return func(c echo.Context) error { var u User
		if err := c.Bind(&u); err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, err.Error())
		}

		if err := validate.Struct(u); err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, err.Error())
		}

		return h(c, u)
	}
}

Now we can generalise the validation function. It will take a type T and use this type to let validator handle validation. Finally, it will pass this data to the handler function:

func validated[T any](h func(c echo.Context, t T) error) echo.HandlerFunc {
	var validate *validator.Validate
	validate = validator.New(validator.WithRequiredStructEnabled())

	return func(c echo.Context) error {
		var t T
		if err := c.Bind(&t); err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, err.Error())
		}

		if err := validate.Struct(t); err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, err.Error())
		}

		return h(c, t)
	}
}

This function will now bind, validate and only if this succeeds call the final handling function with the parsed data.

Setting up routes now looks like this:

e.POST("/users", validated[User](users))
e.POST("/post", validated[Post](posts))

And because Go can infer the generic type here, we can also omit it:

e.POST("/users", validated(users))

Here is a full working example:


package main

import (
	"net/http"

	"github.com/go-playground/validator/v10"
	"github.com/labstack/echo/v4"
)

type User struct {
	Name  string `json:"name" validate:"required"`
	Email string `json:"email" validate:"required,email"`
}

type Post struct {
	Name string `json:"name" validate:"required"`
}

func users(c echo.Context, u User) (err error) {
	return c.JSON(http.StatusOK, u)
}

func posts(c echo.Context, p Post) (err error) {
	return c.JSON(http.StatusOK, p)
}

func main() {
	e := echo.New()

	e.POST("/users", validated(users))
	e.POST("/post", validated(posts))

	e.Logger.Fatal(e.Start(":1323"))
}

func validated[T any](h func(c echo.Context, t T) error) echo.HandlerFunc {
	var validate *validator.Validate
	validate = validator.New(validator.WithRequiredStructEnabled())

	return func(c echo.Context) error {
		var t T
		if err := c.Bind(&t); err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, err.Error())
		}

		if err := validate.Struct(t); err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, err.Error())
		}

		return h(c, t)
	}
}

This approach can of course also be used when using HTTP handler with the stdlib.

See Also