Struct Tags, JSON, and Validation
Contents
Structs in Go
One of my favorite things in Go, is how simple it is to serialize/deserialize things like JSON responses. Coming from languages where parsing things like this can be difficult, I’ve found doing so in Go is quite intuitive!
While Go does not have classes, it does have the concept of structs. Structs in Go can have fields/variables like in other languages, however it uses receiver functions as opposed to struct/class methods.
We can declare a struct such as Person using the following syntax:
type Person struct {
FirstName string
LastName string
Age int
}
Structs and functions do have the concept of being private or public. However unlike other languages there’s no keyword; if a struct field is Uppercase it’s considered exported/public, if it’s lowercase then we say it’s unexported/private.
type Foo struct {
FieldOne string // Exported field
fieldTwo string // Unexported field
}
Likewise for functions, or receiver functions for a type, if it is Uppercase it’s exported or callable from outside our package, if it’s lowercase then it is considered unexported and only callable from within our package.
// Exported function, callable from outside our package
func ExportedFunc() string {
return ""
}
// Unexported function, only callable from within our package
func unexportedFunc() string {
return ""
}
In our simple Person type above, we have three fields, all of which are exported/public.
Unlike other languages, in Go you do not declare methods within the struct object itself. Instead you use a special syntax that designates the function as a receiver for a specified type.
To declare a receiver function that works with a pointer to our Person type we could use:
func (p *Person) GetFirstName() string {
return p.FirstName
}
This says that the function GetFirstName() is a receiver for a pointer to the Person type that returns a string. When you instantiate a Person object, you call GetFirstName() on it and use the result. What Go does under the hood is sends the object you called the function on, as the parameter p (similar to how in C++ you get a *this pointer to the current object). That looks like:
// First declare a pointer to a new Person object
frank := &Person{
FirstName: "Frank",
LastName: "Morales",
Age: 35, // Hehe
}
// Then retrieve the FirstName using its receiver function
firstName := frank.GetFirstName()
On the line firstName := frank.GetFirstName(), inside the GetFirstName() function, the object p points to, will be the frank object we called it on.
The last thing to cover before moving onto struct tags, is the use of free form strings in Go, done by using backticks `…`.
We can use them to declare a string with special characters without the need to escape them. Like in a JSON string:
jsonInput := `{
"first-name" : "Frank",
"last-name" : "Morales",
"age" : 35
}`
Go uses backticks after the type in a struct field, to define tags.
Struct tags and JSON in Go
There are many types of tags that can be used on structs, the two we look at are json and validate. First for JSON it would look like:
type Person struct {
FirstName string `json:"first-name"`
LastName string `json:"last-name"`
Age: int `json:"age"`
}
The format of the tags is json: being the key, and the item(s) in double quotes, "first-name" is the value. When we serialize or deserialize JSON (known as marshaling or unmarshaling in Go) sometimes the API we’re sending it to, or receiving it from, will have a different field name than our struct. By default without a tag, the JSON package will use the lowercase version of the struct field, as the name in the JSON output, vice versa. By giving the name explicitly we can ensure our generated JSON matches API expectations.
One thing to note is when marshaling, or unmarshaling JSON, any unexported fields will not be included. If all struct fields are unexported, you’ll end up with just {} as JSON output.
In Go we can use the JSON package by adding:
import "encoding/json"
The two main methods we’ll use from that package are:
json.Marshal()
json.Unmarshal()
Used to encode/decode JSON respectively. If you plan on printing out your JSON you can use the json.MarshalIndent() function that lets you add spacing/newlines so it prints out much neater.
Some end points don’t play well with empty fields being sent. In struct tags, we can separate values with a comma. A useful tag is omitempty this tells the JSON package that if our struct has a zero value for a field, simply exclude it from the generated JSON.
If our target end point has the Age field as optional, but doesn’t do well when it gets an empty value for it, we can add the tag like so:
type Person struct {
FirstName string `json:"first-name"`
LastName string `json:"last-name"`
Age int `json:"age,omitempty"`
}
Validating struct tags
Like our JSON tags, validate tags are specified using a keyword/value(s). The Validator package defines several values including various expressions. It’s outside the scope of the post, so if you have more complex needs or just want to learn more, you should read the documentation in the references.
In this example we want to focus on a couple values specifically:
required- MUST have a valuealpha- Alphabet characters onlyomitempty- Empty field is oklte=- On strings, length is less than or equal to some amountnumber- Only numbersgt=- On numbers, value must be greater than a valuelt=- On numbers, value must be less than a value
In our contrived API example we’ve alluded to the fact that our first and last name must be given, but the age can be optional. If we expand on that and say for example our API expects a first and last name no longer than 15 letters, and an age if specified to be numeric and greater than 0 or less than 120.
We can codify this into our structs validate tags:
type Person struct {
FirstName string `json:"first-name" validate:"required,alpha,lte=15"`
LastName string `json:"last-name" validate:"required,alpha,lte=15"`
Age int `json:"age,omitempty" validate:"omitempty,number,gt=0,lt=120"`
}
To actually use the validator package we can install it using:
go get github.com/go-playground/validator/v10
Then import it into your project using:
import (
"github.com/go-playground/validator/v10"
)
We can then create a new instance of our validator and use its .Struct() method. That will use the values in our struct tags to validate the field values. If no error is returned then our struct was validated successfully.
p := Person{
FirstName: "Frank",
LastName: "Morales",
Age: 35,
}
validate := validator.New()
if err := validate.Struct(p); err != nil {
fmt.Printf("error validating struct: %s\n", err.Error())
} else {
// This will print because all fields are valid according to our
// validate struct tag rules
fmt.Println("struct validated ok!")
}
If we had a struct with an invalid first name say something like Frank03 the error it would output is similar to:
error validating struct: Key: 'Person.FirstName' Error:Field validation for 'FirstName' failed on the 'alpha' tag
The beauty is we can use this to not only validate incoming JSON responses from APIs, but also outgoing structs to ensure they adhere to our APIs requirements.
When I first started using Go/validator package I really wasn’t sure I liked it. However I’ve really come to enjoy it, as it removes A LOT of boiler plate code checking and validating values. They make it very easy to write unit tests to ensure your rules are validating your fields as expected.
There is so much more you can do with these fields, like have one be required if another is present, excluding fields, etc… The uses are quite vast and this package is definitely worth learning further.
The example code will show various success/failures and is best read from the top down.
NOTE: A careful reader will notice we don’t validate a minimum length of the first and last name. If you pretend the API has one, feel free to add it and write examples to test it as an exercise.
Example Code
If you’d like to run a full example, please check out the blog posts GitHub repository, located here: https://github.com/altf2o/blog.git
The code for this post is in the structtags folder.
Final Note
As usual nothing is ever perfect. So if anyone finds errors or has ideas to make parts clearer, please feel free to reach out to me altf2o@gmail.com - Thank You!
References
Learn more about Structs in Go from: A Tour in Go
JSON Package has a great tutorial: JSON in Go
JSON Package Documentation: https://pkg.go.dev/encoding/json
Validator Package Documentation: https://pkg.go.dev/github.com/go-playground/validator/v10