ProgrammingBackend Go Developer

How is JSON work (encoding/json) implemented in Go: features of serialization, features of struct tags, common pains during marshaling/unmarshaling and workarounds?

Pass interviews with Hintsage AI assistant

Answer.

In Go, JSON marshaling is implemented through the encoding/json package. Only exported (uppercase) fields of structs can be serialized. Struct tags are used to manage names and processing:

type User struct { ID int `json:"id"` Name string `json:"name,omitempty"` Age int `json:"age,string"` }
  • omitempty excludes the field when it has a zero value;
  • string serializes the value as a string, even if it's a number.

Example of serialization:

user := User{ID: 1, Name: "Oleg"} b, _ := json.Marshal(user) fmt.Println(string(b)) // {"id":1,"name":"Oleg","age":"0"}

Example of JSON parsing

var u User json.Unmarshal([]byte('{"id":2,"name":"Ivan"}'), &u)

Trick question.

How are unexported (lowercase) fields of a struct serialized when marshaling to JSON?

Correct answer: They are ignored; only exported (uppercase) fields are involved in serialization. This often unexpectedly breaks APIs:

type Foo struct { bar int // will not be serialized! }

Examples of real mistakes due to ignorance of the subtleties of the topic.


Story

The backend was returning incomplete JSON objects because some needed fields were unexported (lowercase). It took several days to figure out why clients couldn't see these fields.


Story

In the API response, a field contained the omitempty construct, but sometimes it came with an empty value because the zero value for a slice is nil, not an empty slice. Clients received null instead of an empty array [] and crashed during parsing.


Story

In the project, dynamic fields were mixed into structs through map[string]interface{}, but a custom UnmarshalJSON was forgotten to be implemented, causing some data to be "lost" without errors. Client data was lost and had to be manually restored from backups.