Extending Go Struct tags without getting (too) dirty

Pedro Lourenco
CodeX
Published in
5 min readJul 26, 2022

As Software developers, sometimes we would like to extend built-in language features, but commonly we come to the conclusion that it would be too much pain and work to seamlessly add those features.

Here, we will see how we can extend Go’s existing Struct tags without too much pain and while still getting the reward we want!

Photo by Gilly on Unsplash

What are Struct Tags?

Struct tags are a way to “signal the expected behaviour for struct fields in a specific scope”.

Not the best explanation ever, but it will become clear via an example:

In the struct above, we are making use of the json tag to describe the expected behaviour for each one of the struct’s fields, namely: the name it will have when converted to json and what to do if there is no value for that field (omitempty).

Meaning that, after marshalling an instance of the above struct to json, the output would look something like this:

As you can see, the fields assume the names we specified in the json tags. Also, since e-mail was not provided and the e-mail field has the “omitempty” keyword, it simply does not show up on the json representation of the struct.

How to Extend Functionality

Now, back to the core subject: “how can we extend a tag’s functionality without too much fuss?”. Let’s use the json tag as a test subject.

There is one obvious functionality that would be lovely to have supported, which is the “required” keyword.

The required keyword would make json ingestion cleaner by specifying, on the struct declaration, which fields must have a value for a json body to be compliant with the struct.

Unfortunately it is not supported as of now, meaning that an added layer of validation on the ingested json has to be done. Usually via some sort of a validation function.

Photo by Hello I'm Nik on Unsplash

That being said, there are a few ways to have this added functionality without resorting to our own validation function for each struct:

  1. Create our own implementation of Go’s Unmarshaller interface and use it instead of the default one → Too much work.
  2. Use a json schema validator like this oneGreat stuff, but it still is one more dependency on your project for just a small thing that we want.
  3. Use Reflection to detect your new keywords and support them on top of the currently supported behaviours → 😎🥳

Implementation

Our modus operandi will basically be to wrap around what Go already provides and process our new tag via reflection.

We want to make it as seamless as possible, as such:

  • The required keyword will be used inside the json tag like all other keywords currently supported.
  • Everything that is currently supported continues to work as it always did.

The only change in the way we handle json ingestion is that, from now on, we will use this new function:

To sum up what is going on in this function:

  1. We do the Unmarshal with Go’s default json package and, if everything goes well, we proceed to our own new stuff;
  2. We go through each field and get the json tag;
  3. We dive into that tag and check if it contains the “required” keyword;
  4. If it contains the required keyword and the field’s value is the “zero value” for that type, we have a problem and should fail with a meaningful message.

Usage: Before vs. After

Below you can see how life is with the common approach of “json.Unmarshal + validation function” versus our new approach of “tag + ImprovedUnmarshal function”.

  • New Keyword + ImprovedUnmarshal function
  • json.Unmarshal + validation function

As visible in the two examples above, by using our new keyword and function, we now have a reusable platform to validate required fields in json bodies instead of having to write validation functions for every struct for which we want to have required fields.

Further Improvements

What we have at this point is already pretty cool… but we can have more 😈.
Wouldn’t it be sweet if, instead of just knowing the first issue we encounter while validating our custom tags, we got every issue existing in the json body?

We have that power in our hands now: Instead of returning at the first issue found, we append all issues into a slice and return it instead of a single error.

Check the entire example below.

The code above would return:

Instead of the our previous implementation which would only return the first error found:

Main Takeaways

  • We can easily extend tags in Go by wrapping around existing processes and using reflection.
  • We can easily make our code more compact, reusable and readable.
  • Apart from extending existing tags, we can add support for our own new tags for scenarios other than json handling. Clear use cases would be formatting, redacting PII, Nullability checks, etc.
  • It was easy (just one added function) and not painful (the way we write code is pretty much the same), and we did reap the benefits 😎
  • Apart from added functionalities, we can improve upon the status-quo by enriching the typical way things operate: Instead of returning the first error, we return all errors in the json body regarding its validation against our new tags.😈

--

--