Patterns for decoding and validating input for Go data APIs


Most APIs will need to decode input from the client, whether it’s JSON, post form data, or something else. Here is a summary of some patterns that have emerged as I have been writing APIs.

A struct for new stuff

Rather than trying to decode user input directly into the main struct for your model, create an interim struct prefixed with `New`.

Say we model a user as this:

type User struct {
ID bson.ObjectId
Name string
Email string
PasswordHash string
State string
}

When a user is signing up, they won’t be specifying most of those fields, and certainly won’t be specifying the `PasswordHash`.

The client is sending a different thing to our internal representation of users — so why should they be the same type?

So create a struct called `NewUser`:

type NewUser struct {
Name string
Email string
Password string
PasswordConfirm string
}

Now we can decode into this type, check the values and have more control over how we build objects of our `User` type.

A single decode function that validates too

A well designed `decode` function lets you speak JSON initially, and later extend it as your API evolves if you need to without changing any other code.

Since we now have dedicated structs for our user input, we can move any validation code to that type.

Add an interface to your project called `ok`:

// ok represents types capable of validating
// themselves.
type ok interface {
OK() error
}

The `OK` method will just validate the fields returning any errors — or nil if it passes validation.

Start with a simple decode function:

// decode can be this simple to start with, but can be extended
// later to support different formats and behaviours without
// changing the interface.
func decode(r *http.Request, v ok) error {
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
return err
}
return v.OK()
}

Our function is very simple; it takes the `http.Request` (where it will read from the body) and an object that implements our `ok` interface. We just pass the work on to the `json.Decoder` for now , but later we could make decisions based on the Content-Type header or even URL parameters to decide how best to decode the body.

Finally, we return the result of calling the `OK` validation method (which will be either an error if something is wrong, or else `nil`)

The `OK()` implementation for our `NewUser` type might look like this:

func (u *NewUser) OK() error {
if len(u.Email) == 0 {
return ErrMissingField("email")
}
if len(u.Password) == 0 {
return ErrMissingField("password")
}
if u.Password != u.PasswordConfirm {
return errors.New("passwords don’t match")
}
return nil
}

If anything is missing, or if the passwords don’t match it will return an error.

Dedicated error types for common things

You may have noticed the `ErrMissingField`type above. It makes sense to handle common validation failures in the same way each time — you’ll be able change the error messages etc. in one place.

type ErrMissingField string
func (e ErrMissingField) Error() string {
return string(e) + “ is required”
}

Here we create a new type that is based on a `string`, and make it implement the build-in error interface by adding the `Error()` method. Then we just build an error string that we can send back to the client.

Using the decode function

To use the decode function inside one of our handlers, we need only create the appropriate variable to hold the decoded value, and handle the error:

func handleCreateUser(w http.ResponseWriter, r *http.Request) {
var u NewUser
if err := decode(r, &u); err != nil {
respond.With(w, r, http.StatusBadRequest, err)
return
}
}
I’m using respond.With here which I wrote about in my post on API responses in Go last month.

We create a `NewUser` variable and pass it, along with the `http.Request`, into the `decode` function. If there’s an error — we just return it to the client. Now in only five lines of handler code, we have decoded and validated the input.

  • See also some real code on this.
  • Yes I do keep on using `backtics` in the hope that Medium will one day suddenly support them — anyway, you’re probably a programming, so I bet you didn’t even notice.