I was recently skimming my Twitter feed for new Go articles, libraries, etc and I stumbled across a tweet with this message:
#Golang’s JSON Unmarshalling Is Bad
I’m not even sure how it managed to find its way into my feed. I wasn’t following the person who said it, so the only reason it would show up in my feed is the #Golang
hashtag. Regardless, this struck me as a big cry for help. I have written about using JSON with Go in the past on the Gopher Academy’s Advent series (see https://blog.gopheracademy.com/advent-2016/advanced-encoding-decoding/) and I never personally found JSON to be handled poorly in Go. Perhaps not the easiest language in the world, but definitely not “bad”.
null
valueAfter reaching out it became clear to me that the issue here wasn’t specific to Go, but rather a shortcoming of JSON used in conjunction with any typed language. In JSON there are effectively two types of null
.
null
.null
.Unfortunately (or fortunately?), that isn’t how Go, or really any typed languages work. We don’t declare a struct (or class) and magically lose a field when it isn’t defined. That field will always be present, and the value of that field will be nil
or a valid value.
For example, let’s imagine we have a Blog struct and the PublishedAt
is an optional JSON attribute, your code might look like this.
type Blog struct {
ID int `json:"id"`
Title string `json:"title"`
Markdown string `json:"markdown"`
PublishedAt *int `json:"published_at"`
}
This leaves us with two options for PublishedAt
- either it will reference an integer, or it will be nil. We don’t have any way to determine whether it being nil
was implicit or explicit, we just know whether it is nil or not.
Most of the time the difference between a key being set to null and not being set at all isn’t relevant. If the published at value is set to null during a POST
we can likely just assume that the post shouldn’t be published yet and move along.
Where this does come into play is when we are doing partial updates. For instance, if we have a JSON API and we are letting users update their blog posts via a PATCH
request, an API user might send the following JSON payload to update the title of a blog post.
{"title": "some new title"}
When the published_at
field isn’t provided our Blog
type will set the field to null and we can assume that this means the user didn’t want to update the field. But what happens when the user does want to update this field? Specifically, what happens if the user wants to unpublish a blog post by setting the published_at
field to null?
{"published_at":null}
Unfortunately, our code won’t know the difference between explicitly setting the field to null and not providing the key. So how to we handle this in Go?
The first thing to note is that we can customize how JSON gets unmarshalled for any type by implementing the Unmarshaler interface. That means we can replace our PublishedAt
field with a new type and write our own code to handle JSON parsing.
The second thing to remember is that if our field is a pointer type (eg PublishedAt *int
), then its UnmarshalJSON()
method will never be called if the value is null
or if the key was never provided. That means we need to make sure we DO NOT use a pointer if we need to determine if a key has been set.
Putting those two tidbits together, we can create a custom JSONInt
type that will be used to determine if a value has been set, and whether or not it is null.
type JSONInt struct {
Value int
Valid bool
Set bool
}
func (i *JSONInt) UnmarshalJSON(data []byte) error {
// If this method was called, the value was set.
i.Set = true
if string(data) == "null" {
// The key was set to null
i.Valid = false
return nil
}
// The key isn't set to null
var temp int
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
i.Value = temp
i.Valid = true
return nil
}
If we replace our *int
type with the JSONInt
type (not a pointer) the UnmarshalJSON()
method will get called whenever we parse some JSON into a Blog
object, giving us an opportunity to handle that portion of the unmarshalling with our custom code. Specifically, we can set a few flags to dictate whether or not the key was provided.
If you want to see this in action, we can test it out with a few JSON strings using the code below, or you can run it on the Go Playground.
func main() {
notSet := `{}`
setNull := `{"published_at": null}`
setValid := `{"published_at": 123}`
parseAndPrint(notSet)
parseAndPrint(setNull)
parseAndPrint(setValid)
}
func parseAndPrint(str string) {
var b Blog
json.Unmarshal([]byte(str), &b)
fmt.Printf("<Value:%d> <Set:%t> <Valid:%t>\n",
b.PublishedAt.Value, b.PublishedAt.Set, b.PublishedAt.Valid)
}
While there are likely better API designs to avoid this issue, if you do ever run into a situation where you need to support both types of null
for a JSON key you can leverage this technique to make it happen. Simply replace the Value
field with whatever type you need to support and you should be set.
Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.
You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.
Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.
Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.
Related articles
Spread the word
Did you find this page helpful? Let others know about it!
Sharing helps me continue to create both free and premium Go resources.
Want to discuss the article?
See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!
You can reach me via email or via twitter.
©2024 Jonathan Calhoun. All rights reserved.