Go Puzzlers: JSON Unmarshaling
Go does pretty well with JSON, but there are demons lurking on the margins. Here are three edge cases that might bite you.
#1 — JSON Tags and Casing
What does this code print out?
Note what’s occuring here — the raw JSON data has fields whose casing is the OPPOSITE of the casing defined in the struct tags. Do you think this unmarshals okay?
Answer:
Yep, everything unmarshals okay.
Golang doesn’t care about casing when unmarshaling. As long as your struct field is Exported, Go will unmarshal a raw field with any casing whatsoever. So here, for Field1, you could hand Go raw data with “field1”, “Field1”, “FIELD1”, or “fIeLd1” — Go doesn’t care, and will successfully unmarshal any of these JSON fields into the struct’s Field1.
#2 — Default JSON Tags and Casing
What does this code print out?
Here we have the same thing, just with no explicit JSON struct tags. Is the behavior the same?
Yes. This is good. Although #1’s behavior may be surprising, it shouldn’t change based on whether you have explicit JSON tags or not.
#3 — Duplicate JSON Fields
What does this code print?
Now we have many Field1 raw fields, with a mixture of different casing as well as an exact duplicate. What does Go do?
Answer:
It takes the last occurrence of the field, case-insensitive.
Without the raw payload, there’s no way to know that Golang has arbitrarily chosen the last occurrence of the field and dropped the others.
Comments
I’m far from the first person to write something up about this behavior. Here’s a GitHub thread dating back to 2016, which itself dates this issue back to Go 1.2.
RFC 7159 defining JSON seems to be silent on this issue of case sensitivity. This means that Go’s take is just one possible take. Jackson seems to be case-insensitive like Go, but .NET Core requires you to enable case-insensitivity.
Therefore, it’s not terribly hard to get yourself in a situation where you end up with two duplicate fields with different casing in one JSON payload.
- Perhaps whatever API you consume emits JSON with two field names that differ only in their casing, demanding that you treat them as semantically different.
- Perhaps one application emitted the field with lowercase, and another one, handling serialization differently, emitted the field with uppercase. This is already a bad place to be in, but Go doesn’t help, because it will simply choose the last occurrence without you being any wiser.
So what do you do if you want case-sensitive JSON unmarshaling? Let’s tackle that in another post!