A while ago I was solving a problem of serializing/deserializing cyclic references in golang, and all the links go to this question. However, it’s slightly misleading as the question is broader.
If you got into the same situation like me, and can’t find a solution on how to deal with cyclic references, you can now use tahwil — a new library that I published on github. To my knowledge it’s now the only library that facilitates serialization/deserialization of cyclic data structures in a generic way.
Readme gives the information on how to use the library, so I will only duplicate the examples here.
Encoding:
package main
import (
"encoding/json"
"fmt"
"github.com/go-extras/tahwil"
)
type Person struct {
Name string
Parent *Person
Children []*Person
}
func main() {
parent := &Person{
Name: "Arthur",
Children: []*Person{
{
Name: "Ford",
},
{
Name: "Trillian",
},
},
}
parent.Children[0].Parent = parent
parent.Children[1].Parent = parent
v, err := tahwil.ToValue(parent)
if err != nil {
panic(err)
}
res, err := json.Marshal(v)
if err != nil {
panic(err)
}
fmt.Println(string(res))
}
Decoding:
package main
import (
"encoding/json"
"fmt"
"github.com/go-extras/tahwil"
)
type Person struct {
Name string `json:"name"`
Parent *Person `json:"parent"`
Children []*Person `json:"children"`
}
func prepareData() []byte {
parent := &Person{
Name: "Arthur",
Children: []*Person{
{
Name: "Ford",
},
{
Name: "Trillian",
},
},
}
parent.Children[0].Parent = parent
parent.Children[1].Parent = parent
v, err := tahwil.ToValue(parent)
if err != nil {
panic(err)
}
res, err := json.Marshal(v)
if err != nil {
panic(err)
}
return res
}
func main() {
data := &tahwil.Value{}
res := prepareData()
err := json.Unmarshal(res, data)
if err != nil {
panic(err)
}
person := &Person{}
err = tahwil.FromValue(data, person)
if err != nil {
panic(err)
}
fmt.Printf(`Name: %s
Children:
- %s
-- parent name: %s
- %s
-- parent name: %s
`, person.Name,
person.Children[0].Name,
person.Children[0].Parent.Name,
person.Children[1].Name,
person.Children[1].Parent.Name)
}
The main idea is to transform the original data to tahwil.Value{}
, which essentially adds refid
‘s to all of your fields. Whenever tahwil
encounters a cyclic reference, it replaces the actual object with a reference. And after that the graph is technically not cyclic anymore and thus can be marshalled to json.
Restoring the data means a reverse operation, i.e. any reference will be replaced by a pointer to an object.
P.S. Why tahwil
? I tried to find some uncommon word for the name, and found an Arabic word (تحويل) that means conversion.
Недавно мне довелось разрабатывать на Go http-клиент для сервиса, предоставляющего REST API с json-ом в роли формата кодирования. Стандартная задача, но в ходе работы мне пришлось столкнуться с нестандартной проблемой. Рассказываю в чем суть.
Как известно, формат json имеет типы данных. Четыре примитивных: строка, число, логический, null; и два структурных типа: объект и массив. В данном случае нас интересуют примитивные типы. Вот пример json кода с четырьмя полями разных типов:
{
"name":"qwerty",
"price":258.25,
"active":true,
"description":null,
}
Как видно в примере, строковое значение заключается в кавычки. Числовое — не имеет кавычек. Логический тип может иметь только одно из двух значений: true или false (без кавычек). И тип null соответственно имеет значение null (также без кавычек).
А теперь собственно сама проблема. В какой-то момент, при детальном рассмотрении получаемого от стороннего сервиса json-кода, я обнаружил, что одно из полей (назовем его price) помимо числового значения периодически имеет строковое значение (число в кавычках). Т. е. один и тот же запрос с разными параметрами может вернуть число в виде числа, а может вернуть это же число в виде строки. Ума не приложу, как на том конце организован код, возвращающий такие результаты, но видимо, это связано с тем, что сервис сам является агрегатором и тянет данные из разных источников, а разработчики не привели json ответа сервера к единому формату. Тем не менее, надо работать с тем что есть.
Но далее меня ждало еще большее удивление. Логическое поле (назовем его active), помимо значений true и false, возвращало строковые значения «true», «false», и даже числовые 1 и 0 (истина и ложь соответственно).
Вся эта путаница с типами данных не была бы критичной, если бы я обрабатывал json скажем на слаботипизированном PHP, но Go имеет сильную типизацию, и требует четкого указания типа десериализуемого поля. В итоге возникла необходимость реализовать механизм, позволяющий в процессе десериализации преобразовывать все значения поля active в логический тип, и любые значения поля price — в числовой.
Начнем с числового поля price.
Предположим что у нас есть json-код следующего вида:
[
{"id":1,"price":2.58},
{"id":2,"price":7.15}
]
Т. е. json содержит массив объектов с двумя полями числового типа. Стандартный код десериализации данного json-а на Go выглядит так:
type Target struct {
Id int `json:"id"`
Price float64 `json:"price"`
}
func main() {
jsonString := `[{"id":1,"price":2.58},
{"id":4,"price":7.15}]`
targets := []Target{}
err := json.Unmarshal([]byte(jsonString), &targets)
if err != nil {
fmt.Println(err)
return
}
for _, t := range targets {
fmt.Println(t.Id, "-", t.Price)
}
}
В данном коде мы десериализуем поле id в тип int, а поле price в тип float64. Теперь предположим, что наш json-код выглядит так:
[
{"id":1,"price":2.58},
{"id":2,"price":"2.58"},
{"id":3,"price":7.15},
{"id":4,"price":"7.15"}
]
Т. е. поле price содержит значения как числового типа, так и строкового. В данном случае только числовые значения поля price могут быть декодированы в тип float64, строковые же значения вызовут ошибку о несовместимости типов. Это значит, что ни float64, ни любой другой примитивный тип не подходят для десериализации данного поля, и нам необходим свой пользовательский тип со своей логикой десериализации.
В качестве такого типа объявим структуру CustomFloat64 с единственным полем Float64 типа float64.
type CustomFloat64 struct{
Float64 float64
}
И сразу укажем данный тип для поля Price в структуре Target:
type Target struct {
Id int `json:"id"`
Price CustomFloat64 `json:"price"`
}
Теперь необходимо описать собственную логику декодирования поля c типом CustomFloat64.
В пакете «encoding/json» предусмотрены два специальных метода: MarshalJSON и UnmarshalJSON, которые и предназначены для кастомизации логики кодирования и декодирования конкретного пользовательского типа данных. Достаточно переопределить эти методы и описать собственную реализацию.
Переопределим метод UnmarshalJSON для произвольного типа CustomFloat64. При этом необходимо строго следовать сигнатуре метода, иначе он просто не сработает, а главное не выдаст при этом ошибку.
func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
На входе данный метод принимает слайс байт (data), в котором содержится значение конкретного поля декодируемого json. Если преобразовать данную последовательность байт в строку, то мы увидим значение поля именно в том виде, в каком оно записано в json. Т. е. если это строковый тип, то мы увидим именно строку с двойными кавычками («258»), если числовой тип, то увидим строку без кавычек (258).
Чтобы отличить числовое значение от строкового, необходимо проверить, является ли первый символ кавычкой. Так как символ двойной кавычки в таблице UNICODE занимает один байт, нам достаточно проверить первый байт слайса data, сравнив его с номером символа в UNICODE. Это номер 34. Обратите внимание, что в общем случае, символ не равнозначен байту, так как может занимать больше одного байта. Символу в Go равнозначен тип rune (руна). В нашем же случае достаточно данного условия:
if data[0] == 34 {
Если условие выполняется, то значение имеет строковый тип, и нам необходимо получить строку между кавычками, т. е. слайс байт между первым и последним байтом. Именно в этом слайсе содержится числовое значение, которое может быть декодировано в примитивный тип float64. Это значит, что мы можем применить к нему метод json.Unmarshal, при этом результат сохраняя в поле Float64 структуры CustomFloat64.
err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
Если же слайс data начинается не с кавычки, то значит в нем уже содержится числовой тип данных, и мы можем применить метод json.Unmarshal непосредственно ко всему слайсу data.
err := json.Unmarshal(data, &cf.Float64)
Вот полный код метода UnmarshalJSON:
func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
if data[0] == 34 {
err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
if err != nil {
return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
}
} else {
err := json.Unmarshal(data, &cf.Float64)
if err != nil {
return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
}
}
return nil
}
В итоге, с применением метода json.Unmarshal к нашему json-коду, все значения поля price будут прозрачно для нас преобразованы в примитивный тип float64, и результат запишется в поле Float64 структуры CustomFloat64.
Теперь нам может понадобиться преобразовать структуру Target обратно в json. Но, если мы применяем метод json.Marshal непосредственно к типу CustomFloat64, то сериализуем данную структуру в виде объекта. Нам же необходимо кодировать поле price в числовое значение. Чтобы кастомизировать логику кодирования пользовательского типа CustomFloat64, реализуем для него метод MarshalJSON, при этом строго соблюдая сигнатуру метода:
func (cf CustomFloat64) MarshalJSON() ([]byte, error) {
json, err := json.Marshal(cf.Float64)
return json, err
}
Все, что нужно сделать в этом методе, это опять же использовать метод json.Marshal, но уже применять его не к структуре CustomFloat64, а к ее полю Float64. Из метода возвращаем полученный слайс байт и ошибку.
Вот полный код с выводом результатов сериализации и десериализации (проверка ошибок опущена для краткости, номер байта с символом двойных кавычек вынесен в константу):
package main
import (
"encoding/json"
"errors"
"fmt"
)
type CustomFloat64 struct {
Float64 float64
}
const QUOTES_BYTE = 34
func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
if data[0] == QUOTES_BYTE {
err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
if err != nil {
return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
}
} else {
err := json.Unmarshal(data, &cf.Float64)
if err != nil {
return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
}
}
return nil
}
func (cf CustomFloat64) MarshalJSON() ([]byte, error) {
json, err := json.Marshal(cf.Float64)
return json, err
}
type Target struct {
Id int `json:"id"`
Price CustomFloat64 `json:"price"`
}
func main() {
jsonString := `[{"id":1,"price":2.58},
{"id":2,"price":"2.58"},
{"id":3,"price":7.15},
{"id":4,"price":"7.15"}]`
targets := []Target{}
_ := json.Unmarshal([]byte(jsonString), &targets)
for _, t := range targets {
fmt.Println(t.Id, "-", t.Price.Float64)
}
jsonStringNew, _ := json.Marshal(targets)
fmt.Println(string(jsonStringNew))
}
Результат выполнения кода:
1 - 2.58
2 - 2.58
3 - 7.15
4 - 7.15
[{"id":1,"price":2.58},{"id":2,"price":2.58},{"id":3,"price":7.15},{"id":4,"price":7.15}]
Перейдем ко второй части и реализуем аналогичный код для десериализации json-а с несогласованными значениями логического поля.
Предположим что у нас есть json-код следующего вида:
[
{"id":1,"active":true},
{"id":2,"active":"true"},
{"id":3,"active":"1"},
{"id":4,"active":1},
{"id":5,"active":false},
{"id":6,"active":"false"},
{"id":7,"active":"0"},
{"id":8,"active":0},
{"id":9,"active":""}
]
В данном случае поле active подразумевает логический тип и наличие только одного из двух значений: true и false. Значения не логического типа необходимо будет преобразовать в логический в ходе десериализации.
В текущем примере мы допускаем следующие соответствия. Значению true соответствуют: true (логическое), «true» (строковое), «1» (строковое), 1 (числовое). Значению false соответствуют: false (логическое), «false» (строковое), «0» (строковое), 0 (числовое), «» (пустая строка).
Для начала объявим целевую структуру десериализации. В качестве типа поля Active сразу указываем пользовательский тип CustomBool:
type Target struct {
Id int `json:"id"`
Active CustomBool `json:"active"`
}
CustomBool является структурой с одним единственным полем Bool типа bool:
type CustomBool struct {
Bool bool
}
Реализуем для данной структуры метод UnmarshalJSON. Сразу приведу код:
func (cb *CustomBool) UnmarshalJSON(data []byte) error {
switch string(data) {
case `"true"`, `true`, `"1"`, `1`:
cb.Bool = true
return nil
case `"false"`, `false`, `"0"`, `0`, `""`:
cb.Bool = false
return nil
default:
return errors.New("CustomBool: parsing "" + string(data) + "": unknown value")
}
}
Так как поле active в нашем случае имеет ограниченное количество значений, мы можем с помощью конструкции switch-case принять решение, о том, чему должно быть равно значение поля Bool структуры CustomBool. Для проверки понадобится всего два блока case. В первом блоке мы проверяем значение на соответствие true, во втором — false.
При записи возможных значений, следует обратить внимание на роль грависа (это такая кавычка на клавише с буквой Ё в английской раскладке). Данный символ позволяет экранировать двойные кавычки в строке. Для наглядности данным символом я обрамил и значения с кавычками и без кавычек. Таким образом, `false` соответствует строке false (без кавычек, тип bool в json), а `«false»` соответствует строке «false» (с кавычками, тип string в json). Тоже самое и со значениями `1` и `«1»` Первое — это число 1 (в json записано без кавычек), второе — строка «1» (в json записано с кавычками). Вот эта запись `»»` — это пустая строка, Т. е. в формате json она выглядит так: «».
Соответствующее значение (true или false) мы записываем непосредственно в поле Bool структуры CustomBool:
cb.Bool = true
В блоке defaul возвращаем ошибку о том, что поле имеет неизвестное значение:
return errors.New("CustomBool: parsing "" + string(data) + "": unknown value")
Теперь мы можем применять метод json.Unmarshal к нашему json-коду, и значения поля active будут преобразовываться в примитивный тип bool.
Реализуем метод MarshalJSON для структуры CustomBool:
func (cb CustomBool) MarshalJSON() ([]byte, error) {
json, err := json.Marshal(cb.Bool)
return json, err
}
Здесь ничего нового. Метод выполняет сериализацию поля Bool структуры CustomBool.
Вот полный код с выводом результатов сериализации и десериализации (проверка ошибок опущена для краткости):
package main
import (
"encoding/json"
"errors"
"fmt"
)
type CustomBool struct {
Bool bool
}
func (cb *CustomBool) UnmarshalJSON(data []byte) error {
switch string(data) {
case `"true"`, `true`, `"1"`, `1`:
cb.Bool = true
return nil
case `"false"`, `false`, `"0"`, `0`, `""`:
cb.Bool = false
return nil
default:
return errors.New("CustomBool: parsing "" + string(data) + "": unknown value")
}
}
func (cb CustomBool) MarshalJSON() ([]byte, error) {
json, err := json.Marshal(cb.Bool)
return json, err
}
type Target struct {
Id int `json:"id"`
Active CustomBool `json:"active"`
}
func main() {
jsonString := `[{"id":1,"active":true},
{"id":2,"active":"true"},
{"id":3,"active":"1"},
{"id":4,"active":1},
{"id":5,"active":false},
{"id":6,"active":"false"},
{"id":7,"active":"0"},
{"id":8,"active":0},
{"id":9,"active":""}]`
targets := []Target{}
_ = json.Unmarshal([]byte(jsonString), &targets)
for _, t := range targets {
fmt.Println(t.Id, "-", t.Active.Bool)
}
jsonStringNew, _ := json.Marshal(targets)
fmt.Println(string(jsonStringNew))
}
Результат выполнения кода:
1 - true
2 - true
3 - true
4 - true
5 - false
6 - false
7 - false
8 - false
9 - false
[{"id":1,"active":true},{"id":2,"active":true},{"id":3,"active":true},{"id":4,"active":true},{"id":5,"active":false},{"id":6,"active":false},{"id":7,"active":false},{"id":8,"active":false},{"id":9,"active":false}]
Выводы
Во-первых. Переопределение методов MarshalJSON и UnmarshalJSON для произвольных типов данных позволяет кастомизировать сериализацию и десериализацию конкретного поля json-кода. Помимо указанных вариантов использования, данные функции применяются для работы с полями, допускающими значение null.
Во-вторых. Формат текстового кодирования json — это широко используемый инструмент для обмена информацией, и одним из его преимуществ перед другими форматами является наличие типов данных. За соблюдением этих типов надо строго следить.
JSON in Go
In this article, I’ll tell you everything that you need to start using JSON in Go Fluent. We’ll start with some basic usage, we’ll talk about different ways of working in JSON and how to customize it. In the end, you’ll find a FAQ where I put the most common questions about JSON in Go.
Table of content
Basic usage
Marshaling
Go has a built-in mechanism for marshaling (encoding) and unmarshaling (decoding) JSON files. The functionality is paced into encoding/json
package. The basic usage is as follows:
data, err := json.Marshal(yourVar)
The Marshal()
method accepts any type that we want to marshal and returns a []byte
and an error
. The data is ready JSON we can use if the err
is a nil
.
data, err := json.Marshal(yourVar)
if err != nil {
return err
}
// we can use `data` without worries
The error is returned only when the type we pass to the Marshal()
method cannot be correctly encoded. Types that return the *json.UnsupportedTypeError
are:
- channels
- complex 1
- function values
ch := make(chan struct{})
_, err := json.Marshal(ch) // returns error
compl := complex(10, 11)
_, err = json.Marshal(compl) // returns error
fn := func() {}
_, err = json.Marshal(fn) // returns error
It means that if a struct or a map contains those values, we’ll get the error.
It’s a common mistake at the very beginning to forget that only **public struct fields** are used by the `json` package. It means that if the struct has a private field it won’t be both marshaled and unmarshalled
Unmarshalling
When we get a []byte
with our JSON we can easily decode it into our type thanks to the json.Unmarhal()
method.
myVal := MyVal{}
byte := `{"some":"json"}`
err := json.Unmarhal(byte, &myVal)
The error will be returned in the following cases:
- the data isn’t a valid JSON
- we didn’t provide the pointer to our local variable
- we provide a
nil
as the second parameter
It’s a common mistake to forget to add the pointer. Your IDE or linters may help you with catching such bugs.
Go unmarshals the data into struct fields using either the struct field name or its tag. If it won’t find it, it will try the case-insensitive match.
We can use struct tags to manipulate the way how fields are named in your JSON out or change mapping them in unmarshalling. Let me explain it in more detail.
Let’s say we have a struct with two fields as shown below. When we encode the struct into JSON both fields will be capitalized. Very often, it’s now what we want.
type User struct {
ID string
Username string
}
// the output may look like this:
{"ID":"some-id","Username":"admin"}
To change the behavior we can use struct tags. After the field type, we add text. The first word it’s the field tag name json
, after it, we put :
and in double quotes value of the tag. You can see an example below.
type User struct {
ID string `json:"id"`
Username string `json:"user"`
}
u := User{ID: "some-id", Username: "admin"}
// the output may look like this:
{"id":"some-id","user":"admin"}
In the example, we renamed both fields. The name can be anything that’s a valid JSON key. The standard library gives us one additional option: omitempty
. We add it to fields that should be skipped if its value is false
, 0
, a nil
pointer, a nil
interface value, and any empty array, slice, map, or string. We specify options after the JSON key and separate them with a comma (,
).
type User struct {
ID string `json:"id"`
Username string `json:"user"`
Age string `age,omitempty`
}
If we don’t want to change the default field name, we can skip it. We have to remember that in that case, the comma should be there anyway.
type User struct {
ID string `json:"id"`
Username string `json:"user"`
Age string `json:",omitempty"` // don't forget about the comma
}
If we want to keep the field public but tell the marshaller/unmarshaller to ignore it, we have to put a -
to the tag value.
type User struct {
ID string `json:"id"`
Username string `json:"user"`
Age string `json:"-"`
}
u := User{ID: "some-id", Username: "admin", Age: 18}
// the output looks like this (notice missing age):
{"id":"some-id","user":"admin"}
If the runtime will have problems with parsing them (a parse error) the compiler won’t complain about it. It’s error-prone so it’s important to remember about it
Encoder/decoder
There are also json.Decoder
and json.Encoder
in the json
package. They work similar to json.Marshal()
and json.Unmarshal()
methods. The biggest difference is that the first pair works on io.Reader
and io.Writer
. The second pair (marshal/unmarshal) work on a slice of bytes. It means it’s more handy to use json.Decoder
/json.Encoder
if we don’t have the data yet. I prepared two simple tables that should help us understand which option we should use.
When we decode data:
[]byte |
io.Reader |
|
---|---|---|
json.Unmarshal() |
yes | no |
json.Decoder |
no | yes |
When we encode data:
[]byte |
io.Writer |
|
---|---|---|
json.Marshal() |
yes | no |
json.Encoder |
no | yes |
That’s a general rule. You may ask why? The answer to the question is developer experience. Let’s consider an example where we have to read a body from a request. Let’s use both json.Unmarshal()
and json.Decoder
. The Request.Body
implements io.Reader
interface so we can use this fact.
req := CreateOrderRequest{}
if err := json.Decoder(r.Body).Decode(&req); err != nil {
// handle the error
}
// the req is ready to use
We can write a similar program but use json.Unmarshal()
to compare which code is more readable for us.
req := CreateOrderRequest{}
body, err := io.ReadAll(r.Body)
if err != nil {
// handle the error
}
if err = json.Unmarshal(body, &req); err != nil {
// handle the error
}
There’s more one difference that may tell us which one we should use. We can call json.Decoder
and json.Encoder
on a single io.Reader
and io.Writer
multiple times. It means that if the stream that we pass to the decoder contains multiple JSONs, we can create the decoder once but run Decode()
multiple times.
req := CreateOrderRequest{}
decoder := json.Decoder(r.Body)
for err := decoder.Decode(&req); err != nil {
// handle single request
}
If you’d like to use json.Decoder
but you have the []byte
you can wrap it with a buffer and use it instead.
var body []byte
buf := bytes.NewBuffer(body)
decoder := json.Decoder(buf)
for err := decoder.Decode(&req); err != nil {
// handle single request
}
The performance?
I wrote simple benchmarks to compare both approaches.
package jsons
import (
"bytes"
"encoding/json"
"io"
"testing"
)
var j = []byte(`{"user":"Johny Bravo","items":[{"id":"4983264583302173928","qty": 5}]}`)
var createRequest = CreateOrderRequest{
User: "Johny Bravo",
Items: []OrderItem{
{ID: "4983264583302173928", Qty: 5},
},
}
var err error
var body []byte
type OrderItem struct {
ID string `json:"id"`
Qty int `json:"qty"`
}
type CreateOrderRequest struct {
User string `json:"user"`
Items []OrderItem `json:"items"`
}
func BenchmarkJsonUnmarshal(b *testing.B) {
b.ReportAllocs()
req := CreateOrderRequest{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
err = json.Unmarshal(j, &req)
}
}
func BenchmarkJsonDecoder(b *testing.B) {
b.ReportAllocs()
req := CreateOrderRequest{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
buff := bytes.NewBuffer(j)
b.StartTimer()
decoder := json.NewDecoder(buff)
err = decoder.Decode(&req)
}
}
func BenchmarkJsonMarshal(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
body, err = json.Marshal(createRequest)
}
}
func BenchmarkJsonEncoder(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
encoder := json.NewEncoder(io.Discard)
err = encoder.Encode(createRequest)
}
}
When we run them we’ll see that json.Unmarshal()
is about 3 times faster than json.Decoder
. On the other hand, both json.Marshal()
and json.Encoder
have similar performance. At least with the input data I prepared.
BenchmarkJsonUnmarshal-10 1345796 894.4 ns/op 336 B/op 9 allocs/op
BenchmarkJsonDecoder-10 522276 2226 ns/op 1080 B/op 13 allocs/op
BenchmarkJsonMarshal-10 6257662 193.1 ns/op 128 B/op 2 allocs/op
BenchmarkJsonEncoder-10 6867033 174.9 ns/op 48 B/op 1 allocs/op
I encourage you to not take these or any other benchmarks as a go/no-go. You have to make similar tests in your application and then see if changing the function we use has any significant impact on the performance. Context is the king.
Indenting
You probably saw that the JSON file produced by both json.Marshal
or json.Encoder
is compacted. Meaning, it has no extra white spaces that’d make it more human-readable. There’s an alternative function called json.MarshalIndent
that will help you format the output.
data := map[string]int{
"a": 1,
"b": 2,
}
b, err := json.MarshalIndent(data, "<prefix>", "<indent>")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
// the output will be
{
<prefix><indent>"a": 1,
<prefix><indent>"b": 2
<prefix>}
We can use the prefix to embed the new JSON into an already existing one and keep proper nesting.
MarshalJSON
and UnmarshalJSON
We can decide how a specific part of the JSON will be processed. We can achieve that we have to implement specific interfaces.
To be able to change the way our object is processed we have to implement one of those interfaces.
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
UnmarshalJSON
example
It will be easier to explain how it works with an example. Let’s say we receive JSON with the PC’s specification. The problem is that we receive the RAM size and storage size in bytes but we need it in a more human-readable format.
{
"cpu": "Intel Core i5",
"operatingSystem": "Windows 11",
"memory": 17179869184,
"storage": 274877906944
}
Pretty unreadable, isn’t it? Let’s prepare our struct that will store these data.
type PC struct {
CPU string
OperatingSystem string
Memory string
Storage string
}
To handle this case correctly we have to introduce a new type that will be an alias to a string. We’ll implement the UnmarhalJSON
method for it.
type Memory string
func (m *Memory) UnmarshalJSON(b []byte) error {
size, err := strconv.Atoi(string(b))
if err != nil {
return err
}
for i, d := range memorySizes {
if size > d {
*m = Memory(fmt.Sprintf("%d %s", size/d, sizeSuffixes[i]))
return nil
}
}
*m = Memory(fmt.Sprintf("%d b", size))
return nil
}
We convert []byte
to an integer and then calculate the size in a human-readable format. The full source code is available here: https://goplay.tools/snippet/UfszC3iDvZW.
FAQ
What if I don’t know the schema?
If you’re not sure about the whole schema or part of it you have some options to handle it. One of the ways to go is using maps. Let’s say we’ll receive a JSON but we want to process it dynamically.
req := map[string]interface{}{}
if err != json.Decoder(r.Body).Decode(&req); err != nil {
// handle the err
}
We put the whole data into the map. Now, we can iterate over it and put our custom logic there. We’ll need to use the reflect
package to determine the type of value.
for k, v := range req {
refVal := reflect.TypeOf(v)
fmt.Printf("the key '%s' contains the value of type %sn", k, refVal)
}
/* sample output:
the key 'two' contains the value of type string
the key 'three' contains the value of type float64
the key 'one' contains the value of type int
*/
I cannot see my fields in JSON after marshaling
It can be caused by two things:
- the field isn’t public (doesn’t start with a capital letter)
- it’s marked to be ignored using the struct tag:
json:"-"
Can I skip the error check in Marshal()
method?
The general answer is NO
but… I sometimes do it
If you can cover unsuccessful marshaling in your unit tests, I think it’s OK to do it. Please just remember about adding a comment that it’s a handler somewhere else.
On the other hand, is it worth making things a bit more complicated just to save one if
statement? I’m not sure about it. It has chance to be an unpopular opinion.
If the std json
package good enough?
I’d say 99% of the answer is YES
. If you process huge JSON files or a lot of them and it’s a significant part of the work, you may start seeking some alternatives. Otherwise, I think it won’t disappoint you.
Outside of the standard library
If you’re looking for a faster alternative you can take a look at https://github.com/goccy/go-json. It’s a drop-in replacement for the standard encoding/json
package.
If the JSON is huge but you need only part of it, you can take a look at https://github.com/buger/jsonparser which allows you to just parse part of the whole file.
Summary
I tried to cover everything that’s needed to work with JSON in Go. If you have any other questions, feel free to use the comments section below. I’ll be happy to answer any of them.
Buy me a coffee
Tags:
#json
#reflect
Обзор
JSON — один из самых популярных форматов сериализации. Он удобен для чтения, достаточно лаконичен и может быть легко проанализирован любым веб-приложением, использующим JavaScript. Go как современный язык программирования имеет первоклассную поддержку сериализации JSON в своей стандартной библиотеке.
Но есть и укромные уголки. В этом руководстве вы узнаете, как эффективно сериализовать и десериализовать произвольные, а также структурированные данные в/из JSON. Вы также узнаете, как работать с расширенными сценариями, такими как перечисления сериализации.
Пакет JSON
Go поддерживает несколько форматов сериализации в пакете кодирования своей стандартной библиотеки. Одним из них является популярный формат JSON. Вы сериализуете значения Golang, используя функцию Marshal(), в кусочек байтов. Вы десериализуете часть байтов в значение Golang, используя функцию Unmarshal(). Это так просто. Следующие термины эквивалентны в контексте этой статьи:
- Сериализация/Кодирование/Маршалинг
- Десериализация/Декодирование/Демаршаллизация
Я предпочитаю сериализацию, потому что она отражает тот факт, что вы преобразуете потенциально иерархическую структуру данных в/из потока байтов.
Marshal
Функция Marshal() может принимать что угодно, что в Go означает пустой интерфейс и возвращает часть байтов и ошибку. Вот ее сигнатура:
func Marshal(v interface{}) ([]byte, error)
Если Marshal() не сможет сериализовать входное значение, она вернет ненулевую ошибку. У Marshal() есть некоторые строгие ограничения (позже мы увидим, как их преодолеть с помощью пользовательских маршаллеров):
- Ключи карты должны быть строками.
- Значения карты должны иметь типы, сериализуемые пакетом json.
- Следующие типы не поддерживаются: Channel, complex и function.
- Циклические структуры данных не поддерживаются.
- Указатели будут закодированы (и позже декодированы) как значения, на которые они указывают (или «null», если указатель равен нулю).
Unmarshal
Функция Unmarshal() принимает фрагмент байта, который, как мы надеемся, представляет действительный JSON, и целевой интерфейс, который обычно является указателем на структуру или базовый тип. Он десериализует JSON в интерфейс универсальным способом. Если сериализация не удалась, он вернет ошибку. Вот сигнатура:
func Unmarshal(data []byte, v interface{}) error
Сериализация простых типов
Вы можете легко сериализовать простые типы, например, используя пакет json. Результатом будет не полноценный объект JSON, а простая строка. Здесь int 5 сериализуется в байтовый массив [53], который соответствует строке «5».
1 |
// Serialize int
|
2 |
var x = 5 |
3 |
bytes, err := json.Marshal(x) |
4 |
if err != nil { |
5 |
fmt.Println("Can't serislize", x) |
6 |
}
|
7 |
|
8 |
fmt.Printf("%v => %v, '%v'n", x, bytes, string(bytes)) |
9 |
|
10 |
// Deserialize int
|
11 |
var r int |
12 |
err = json.Unmarshal(bytes, &r) |
13 |
if err != nil { |
14 |
fmt.Println("Can't deserislize", bytes) |
15 |
}
|
16 |
|
17 |
fmt.Printf("%v => %vn", bytes, r) |
18 |
|
19 |
Output: |
20 |
|
21 |
- 5 => [53], '5' |
22 |
- [53] => 5 |
Если вы попытаетесь сериализовать неподдерживаемые типы, такие как функция, вы получите ошибку:
1 |
// Trying to serialize a function
|
2 |
foo := func() { |
3 |
fmt.Println("foo() here") |
4 |
}
|
5 |
|
6 |
bytes, err = json.Marshal(foo) |
7 |
if err != nil { |
8 |
fmt.Println(err) |
9 |
}
|
10 |
|
11 |
Output: |
12 |
|
13 |
json: unsupported type: func() |
Сериализация произвольных данных с помощью карт
Сила JSON в том, что он может очень хорошо представлять произвольные иерархические данные. Пакет JSON поддерживает его и использует общий пустой интерфейс (interface {}) для представления любой иерархии JSON. Вот пример десериализации и последующей сериализации двоичного дерева, где каждый узел имеет значение int и две ветви, левую и правую, которые могут содержать другой узел или быть нулевыми.
JSON null эквивалентен Go nil. Как видно из выходных данных, функция json.Unmarshal()
успешно преобразовала большой двоичный объект JSON в структуру данных Go, состоящую из вложенной карты интерфейсов, и сохранила тип значения как int. Функция json.Marshal()
успешно сериализовала полученный вложенный объект в то же представление JSON.
1 |
// Arbitrary nested JSON
|
2 |
dd := ` |
3 |
{
|
4 |
"value": 3, |
5 |
"left": { |
6 |
"value": 1, |
7 |
"left": null, |
8 |
"right": { |
9 |
"value": 2, |
10 |
"left": null, |
11 |
"right": null |
12 |
}
|
13 |
},
|
14 |
"right": { |
15 |
"value": 4, |
16 |
"left": null, |
17 |
"right": null |
18 |
}
|
19 |
}` |
20 |
|
21 |
var obj interface{} |
22 |
err = json.Unmarshal([]byte(dd), &obj) |
23 |
if err != nil { |
24 |
fmt.Println(err) |
25 |
} else { |
26 |
fmt.Println("--------n", obj) |
27 |
}
|
28 |
|
29 |
|
30 |
data, err = json.Marshal(obj) |
31 |
if err != nil { |
32 |
fmt.Println(err) |
33 |
} else { |
34 |
fmt.Println("--------n", string(data)) |
35 |
}
|
36 |
}
|
37 |
|
38 |
Output: |
39 |
|
40 |
--------
|
41 |
map[right:map[value:4 |
42 |
left:<nil> |
43 |
right:<nil>] |
44 |
value:3 |
45 |
left:map[left:<nil> |
46 |
right:map[value:2 |
47 |
left:<nil> |
48 |
right:<nil>] |
49 |
value:1]] |
50 |
--------
|
51 |
{"left":{ |
52 |
"left":null, |
53 |
"right":{"left":null,"right":null,"value":2}, |
54 |
"value":1}, |
55 |
"right":{"left":null, |
56 |
"right":null, |
57 |
"value":4}, |
58 |
"value":3} |
Чтобы пройти общие карты интерфейсов, вам нужно использовать утверждения типа. Например:
1 |
func dump(obj interface{}) error { |
2 |
if obj == nil { |
3 |
fmt.Println("nil") |
4 |
return nil |
5 |
}
|
6 |
switch obj.(type) { |
7 |
case bool: |
8 |
fmt.Println(obj.(bool)) |
9 |
case int: |
10 |
fmt.Println(obj.(int)) |
11 |
case float64: |
12 |
fmt.Println(obj.(float64)) |
13 |
case string: |
14 |
fmt.Println(obj.(string)) |
15 |
case map[string]interface{}: |
16 |
for k, v := range(obj.(map[string]interface{})) { |
17 |
fmt.Printf("%s: ", k) |
18 |
err := dump(v) |
19 |
if err != nil { |
20 |
return err |
21 |
}
|
22 |
}
|
23 |
default: |
24 |
return errors.New( |
25 |
fmt.Sprintf("Unsupported type: %v", obj)) |
26 |
}
|
27 |
|
28 |
return nil |
29 |
}
|
Сериализация структурированных данных
Работа со структурированными данными часто является лучшим выбором. Go предоставляет отличную поддержку для сериализации JSON в/из структур через его теги struct
. Давайте создадим struct
, которая соответствует нашему дереву JSON и более умной функции Dump()
, которая ее печатает:
1 |
type Tree struct { |
2 |
value int |
3 |
left *Tree |
4 |
right *Tree |
5 |
}
|
6 |
|
7 |
func (t *Tree) Dump(indent string) { |
8 |
fmt.Println(indent + "value:", t.value) |
9 |
fmt.Print(indent + "left: ") |
10 |
if t.left == nil { |
11 |
fmt.Println(nil) |
12 |
} else { |
13 |
fmt.Println() |
14 |
t.left.Dump(indent + " ") |
15 |
}
|
16 |
|
17 |
fmt.Print(indent + "right: ") |
18 |
if t.right == nil { |
19 |
fmt.Println(nil) |
20 |
} else { |
21 |
fmt.Println() |
22 |
t.right.Dump(indent + " ") |
23 |
}
|
24 |
}
|
Это здорово и намного чище, чем произвольный подход JSON. Но работает ли это? На самом деле, нет. Там нет ошибки, но наш объект дерева не заполняется JSON.
1 |
jsonTree := ` |
2 |
{
|
3 |
"value": 3, |
4 |
"left": { |
5 |
"value": 1, |
6 |
"left": null, |
7 |
"right": { |
8 |
"value": 2, |
9 |
"left": null, |
10 |
"right": null |
11 |
}
|
12 |
},
|
13 |
"right": { |
14 |
"value": 4, |
15 |
"left": null, |
16 |
"right": null |
17 |
}
|
18 |
}` |
19 |
|
20 |
|
21 |
var tree Tree |
22 |
err = json.Unmarshal([]byte(dd), &tree) |
23 |
if err != nil { |
24 |
fmt.Printf("- Can't deserislize tree, error: %vn", err) |
25 |
} else { |
26 |
tree.Dump("") |
27 |
}
|
28 |
|
29 |
Output: |
30 |
|
31 |
value: 0 |
32 |
left: <nil> |
33 |
right: <nil> |
Проблема в том, что поля дерева являются приватными. Сериализация JSON работает только для открытых полей. Таким образом, мы можем сделать поля структуры общедоступными. Пакет json достаточно умен, чтобы прозрачно преобразовать строчные буквы «value», «left» и «right» в соответствующие им имена полей верхнего регистра.
1 |
type Tree struct { |
2 |
Value int `json:"value"` |
3 |
Left *Tree `json:"left"` |
4 |
Right *Tree `json:"right"` |
5 |
}
|
6 |
|
7 |
|
8 |
Output: |
9 |
|
10 |
value: 3 |
11 |
left: |
12 |
value: 1 |
13 |
left: <nil> |
14 |
right: |
15 |
value: 2 |
16 |
left: <nil> |
17 |
right: <nil> |
18 |
right: |
19 |
value: 4 |
20 |
left: <nil> |
21 |
right: <nil> |
Пакет json будет автоматически игнорировать не отображенные поля в JSON, а также приватные поля в вашей struct
. Но иногда вам может потребоваться отобразить определенные ключи в JSON на поле с другим именем в вашей struct
. Вы можете использовать теги struct
для этого. Например, предположим, что мы добавили еще одно поле с именем «label» в JSON, но нам нужно сопоставить его с полем «Tag» в нашей структуре.
1 |
type Tree struct { |
2 |
Value int |
3 |
Tag string `json:"label"` |
4 |
Left *Tree |
5 |
Right *Tree |
6 |
}
|
7 |
|
8 |
func (t *Tree) Dump(indent string) { |
9 |
fmt.Println(indent + "value:", t.Value) |
10 |
if t.Tag != "" { |
11 |
fmt.Println(indent + "tag:", t.Tag) |
12 |
}
|
13 |
fmt.Print(indent + "left: ") |
14 |
if t.Left == nil { |
15 |
fmt.Println(nil) |
16 |
} else { |
17 |
fmt.Println() |
18 |
t.Left.Dump(indent + " ") |
19 |
}
|
20 |
|
21 |
fmt.Print(indent + "right: ") |
22 |
if t.Right == nil { |
23 |
fmt.Println(nil) |
24 |
} else { |
25 |
fmt.Println() |
26 |
t.Right.Dump(indent + " ") |
27 |
}
|
28 |
}
|
Вот новый JSON с корневым узлом дерева, помеченным как «root», правильно сериализованный в поле Tag и напечатанный в выходных данных:
1 |
dd := ` |
2 |
{
|
3 |
"label": "root", |
4 |
"value": 3, |
5 |
"left": { |
6 |
"value": 1, |
7 |
"left": null, |
8 |
"right": { |
9 |
"value": 2, |
10 |
"left": null, |
11 |
"right": null |
12 |
}
|
13 |
},
|
14 |
"right": { |
15 |
"value": 4, |
16 |
"left": null, |
17 |
"right": null |
18 |
}
|
19 |
}` |
20 |
|
21 |
|
22 |
var tree Tree |
23 |
err = json.Unmarshal([]byte(dd), &tree) |
24 |
if err != nil { |
25 |
fmt.Printf("- Can't deserislize tree, error: %vn", err) |
26 |
} else { |
27 |
tree.Dump("") |
28 |
}
|
29 |
|
30 |
Output: |
31 |
|
32 |
value: 3 |
33 |
tag: root |
34 |
left: |
35 |
value: 1 |
36 |
left: <nil> |
37 |
right: |
38 |
value: 2 |
39 |
left: <nil> |
40 |
right: <nil> |
41 |
right: |
42 |
value: 4 |
43 |
left: <nil> |
44 |
right: <nil> |
Написание обычного упаковщика
Вы часто захотите сериализовать объекты, которые не соответствуют строгим требованиям функции Marshal(). Например, вы можете захотеть сериализовать карту с помощью ключей int. В этих случаях вы можете написать собственный упаковщик/распаковщик, реализовав интерфейсы Marshaler
и Unmarshaler
.
Примечание о правописании. В Go принято называть интерфейс одним методом, добавляя суффикс «er» к имени метода. Таким образом, несмотря на то, что более распространенным написанием является «Marshaller» (с двойным L), имя интерфейса — просто «Marshaler» (один L).
Вот интерфейсы Marshaler и Unmarshaler:
1 |
type Marshaler interface { |
2 |
MarshalJSON() ([]byte, error) |
3 |
}
|
4 |
|
5 |
type Unmarshaler interface { |
6 |
UnmarshalJSON([]byte) error |
7 |
}
|
Вы должны создать тип при выполнении пользовательской сериализации, даже если вы хотите сериализовать встроенный тип или композицию встроенных типов, таких как map[int]string
. Здесь я определяю тип с именем IntStringMap
и реализую интерфейсы Marshaler
и Unmarshaler
для этого типа.
Метод MarshalJSON()
создает map[string]string
, преобразует каждый из своих собственных ключей int в строку и сериализует карту со строковыми ключами, используя стандартную функцию json.Marshal()
.
1 |
type IntStringMap map[int]string |
2 |
|
3 |
func (m *IntStringMap) MarshalJSON() ([]byte, error) { |
4 |
ss := map[string]string{} |
5 |
for k, v := range *m { |
6 |
i := strconv.Itoa(k) |
7 |
ss[i] = v |
8 |
}
|
9 |
return json.Marshal(ss) |
10 |
}
|
Метод UnmarshalJSON() делает прямо противоположное. Он десериализует массив байтов данных в map[string]string
, а затем преобразует каждый строковый ключ в int и заполняет сам себя.
1 |
func (m *IntStringMap) UnmarshalJSON(data []byte ) error { |
2 |
ss := map[string]string{} |
3 |
err := json.Unmarshal(data, &ss) |
4 |
if err != nil { |
5 |
return err |
6 |
}
|
7 |
for k, v := range ss { |
8 |
i, err := strconv.Atoi(k) |
9 |
if err != nil { |
10 |
return err |
11 |
}
|
12 |
(*m)[i] = v |
13 |
}
|
14 |
return nil |
15 |
}
|
Вот как это использовать в программе:
1 |
m := IntStringMap{4: "four", 5: "five"} |
2 |
data, err := m.MarshalJSON() |
3 |
if err != nil { |
4 |
fmt.Println(err) |
5 |
}
|
6 |
fmt.Println("IntStringMap to JSON: ", string(data)) |
7 |
|
8 |
|
9 |
m = IntStringMap{} |
10 |
|
11 |
jsonString := []byte("{"1": "one", "2": "two"}") |
12 |
m.UnmarshalJSON(jsonString) |
13 |
|
14 |
fmt.Printf("IntStringMap from JSON: %vn", m) |
15 |
fmt.Println("m[1]:", m[1], "m[2]:", m[2]) |
16 |
|
17 |
Output: |
18 |
|
19 |
IntStringMap to JSON: {"4":"four","5":"five"} |
20 |
IntStringMap from JSON: map[2:two 1:one] |
21 |
m[1]: one m[2]: two |
Сериализация Enums
Перечисления Go могут быть довольно неприятными для сериализации. Идея написать статью о сериализации Go json возникла из-за вопроса, который мне спросил коллега о том, как сериализовать перечисления. Вот enum
Go. Константы Zero и One равны целым числам 0 и 1.
1 |
type EnumType int |
2 |
|
3 |
const ( |
4 |
Zero EnumType = iota |
5 |
One
|
6 |
)
|
Хотя вы можете думать, что это int, и во многих отношениях это так, вы не можете сериализовать его напрямую. Вы должны написать собственный маршалер/демаршалер. Это не проблема после последнего раздела. Следующие MarshalJSON()
и UnmarshalJSON()
будут сериализовать/десериализовать константы ZERO и ONE в/из соответствующих строк «Zero» и «One».
1 |
func (e *EnumType) UnmarshalJSON(data []byte) error { |
2 |
var s string |
3 |
err := json.Unmarshal(data, &s) |
4 |
if err != nil { |
5 |
return err |
6 |
}
|
7 |
|
8 |
value, ok := map[string]EnumType{"Zero": Zero, "One": One}[s] |
9 |
if !ok { |
10 |
return errors.New("Invalid EnumType value") |
11 |
}
|
12 |
*e = value |
13 |
return nil |
14 |
}
|
15 |
|
16 |
func (e *EnumType) MarshalJSON() ([]byte, error) { |
17 |
value, ok := map[EnumType]string{Zero: "Zero", One:"One"}[*e] |
18 |
if !ok { |
19 |
return nil, errors.New("Invalid EnumType value") |
20 |
}
|
21 |
return json.Marshal(value) |
22 |
}
|
Давайте попробуем встроить этот EnumType
в struct
и сериализовать его. Основная функция создает EnumContainer
и инициализирует его с именем «Uno» и значением нашей константы enum
ONE
, которая равна int 1.
1 |
type EnumContainer struct { |
2 |
Name string |
3 |
Value EnumType |
4 |
}
|
5 |
|
6 |
|
7 |
func main() { |
8 |
x := One |
9 |
ec := EnumContainer{ |
10 |
"Uno", |
11 |
x, |
12 |
}
|
13 |
s, err := json.Marshal(ec) |
14 |
if err != nil { |
15 |
fmt.Printf("fail!") |
16 |
}
|
17 |
|
18 |
var ec2 EnumContainer |
19 |
err = json.Unmarshal(s, &ec2) |
20 |
|
21 |
fmt.Println(ec2.Name, ":", ec2.Value) |
22 |
}
|
23 |
|
24 |
Output: |
25 |
|
26 |
Uno : 0 |
Ожидаемый результат — «Uno: 1», но вместо этого «Uno: 0». Что случилось? В коде упаковщика/распаковщика нет ошибки. Оказывается, вы не можете встраивать перечисления по значению, если хотите их сериализовать. Вы должны вставить указатель на перечисление. Вот модифицированная версия, где она работает как положено:
1 |
type EnumContainer struct { |
2 |
Name string |
3 |
Value *EnumType |
4 |
}
|
5 |
|
6 |
func main() { |
7 |
x := One |
8 |
ec := EnumContainer{ |
9 |
"Uno", |
10 |
&x, |
11 |
}
|
12 |
s, err := json.Marshal(ec) |
13 |
if err != nil { |
14 |
fmt.Printf("fail!") |
15 |
}
|
16 |
|
17 |
var ec2 EnumContainer |
18 |
err = json.Unmarshal(s, &ec2) |
19 |
|
20 |
fmt.Println(ec2.Name, ":", *ec2.Value) |
21 |
}
|
22 |
|
23 |
Output: |
24 |
|
25 |
Uno : 1 |
Заключение
Go предоставляет много опций для сериализации и десериализации JSON. Важно понять все входы и выходы пакета encoding/json, чтобы воспользоваться преимуществами.
В этом уроке вы получите всю мощь, в том числе и сериализацию неуловимых перечислений Go.
Иди и сериализуй некоторые объекты!
As a language designed for the web, Go provides extensive support for working with JSON data. JSON (JavaScript Object Notation) is an incredibly popular data exchange format whose syntax resembles simple JavaScript objects. It’s one of the most common ways for applications to communicate on the modern web.
Go takes a unique approach for working with JSON data. The best way to think about JSON data in Go is as an encoded struct
. When you encode and decode a struct to JSON, the key of the JSON object will be the name of the struct field unless you give the field an explicit JSON tag.
type User struct {
FirstName string `json:"first_name"` // key will be "first_name"
BirthYear int `json:"birth_year"` // key will be "birth_year"
Email string // key will be "Email"
}
Example marshal JSON from struct (encode) 🔗
The encoding/json
package exposes a json.Marshal
function that allows us to generate the JSON encoding of any value, assuming that type has an encoder implemented. The good news is, all the default types in Go have an encoder created out-of-the-box, and you’ll usually be working with structs containing default-type fields.
func Marshal(v interface{}) ([]byte, error)
As you can see, Marshal()
takes a value as input, and returns the encoded JSON as a slice of bytes on success, or an error
if something went wrong.
dat, _ := json.Marshal(`User{
FirstName: "Lane",
BirthYear: 1990,
Email: "example@gmail.com",
}`)
fmt.Println(string(dat))
// prints:
// {"first_name":"Lane","birth_year":1990,"Email":"example@gmail.com"}
Example unmarshal JSON to struct (decode) 🔗
func Unmarshal(data []byte, v interface{}) error
Similarly, the json.Unmarshal()
function takes some encoded JSON data and a pointer to a value where the encoded JSON should be written, and returns an error if something goes wrong.
dat := []byte(`{
"first_name":"Lane",
"birth_year":1990,
"Email":"example@gmail.com"
}`)
user := User{}
err := json.Unmarshal(dat, &user)
if err != nil {
fmt.Println(err)
}
fmt.Println(user)
// prints:
// {Lane 1990 example@gmail.com}
Example — Go JSON HTTP server 🔗
Building a JSON API in Go is simple, you don’t even need a framework to get access to convenient high-level HTTP support. I typically start by writing two little helper functions, respondWithJSON
and responsdWithError
.
func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) error {
response, err := json.Marshal(payload)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(code)
w.Write(response)
return nil
}
respondWithJSON
makes it easy to send a JSON response by simply providing the handler’s ResponseWriter, an HTTP status code, and a payload to be marshaled (typically a struct).
func respondWithError(w http.ResponseWriter, code int, msg string) error {
return respondWithJSON(w, code, map[string]string{"error": msg})
}
The respondWithError
function wraps the respondWithJSON
function and always sends an error message. Now let’s take a look at how to build a full HTTP handler.
func handler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
type requestBody struct {
Email string `json:"email"`
Password string `json:"password"`
}
type responseBody struct {
Token string `json:"token"`
}
dat, err := io.ReadAll(r.Body)
if err != nil {
respondWithError(w, 500, "couldn't read request")
return
}
params := requestBody{}
err = json.Unmarshal(dat, ¶ms)
if err != nil {
respondWithError(w, 500, "couldn't unmarshal parameters")
return
}
// do stuff with username and password
respondWithJSON(w, 200, responseBody{
Token: "example-auth-token",
})
}
Since the json.Marshal
and json.Unmarshal
function work on the []byte
type, it’s really easy to send those bytes over the wire or write them to disk.
Example — Reading and writing JSON files 🔗
I use JSON files to store configuration from time to time. Go makes it easy to read and write JSON files.
Write JSON to a file in Go 🔗
type car struct {
Speed int `json:"speed"`
Make string `json:"make"`
}
c := car{
Speed: 10,
Make: "Tesla",
}
dat, err := json.Marshal(c)
if err != nil {
return err
}
err = io.WriteFile("/tmp/file.json", dat, 0644)
if err != nil {
return err
}
Read JSON from a file in Go
type car struct {
Speed int `json:"speed"`
Make string `json:"make"`
}
dat, err := io.ReadFile("/tmp/file.json")
if err != nil {
return err
}
c := car{}
err = json.Unmarshal(dat, &c)
if err != nil {
return err
}
Tag Options — Omitempty 🔗
When marshaling data you can leave out a key completely if the key’s value contains a zero value using the omitempty
tag.
type User struct {
FirstName string `json:"first_name,omitempty"`
BirthYear int `json:"birth_year"`
}
// if FirstName = "" and BirthYear = 0
// marshaled JSON will be:
// {"birth_year":0}
// if FirstName = "lane" and BirthYear = 0
// marshaled JSON will be:
// {"first_name":"lane","birth_year":0}
Tag Options — Ignore field 🔗
As mentioned above, non-exported (lowercase) fields are ignored by the marshaler. If you want to ignore additional fields you can use the -
tag.
type User struct {
// FirstName will never be encoded
FirstName string `json:"-"`
BirthYear int `json:"birth_year"`
}
Default encoding types 🔗
JSON and Go types don’t match up 1-to-1. Below is a table that describes the type relationships when encoding and decoding.
Go Type | JSON Type |
---|---|
bool |
boolean |
float64 |
number |
string |
string |
nil pointer | null |
time.Time | RFC 3339 timestamp (string) |
You will notice that the float32
and int
types are missing. Don’t worry, you can certainly encode and decode numbers into these types, they just don’t have an explicit type in the JSON specification. For example, if you encode an integer in JSON, it’s guaranteed not to have a decimal point. However, if someone converts that JSON value to a floating-point number before you decode it, you’ll get a runtime error.
It’s rare to encounter an error when marshaling JSON data, but unmarshalling JSON can often cause errors. Here are some things to watch out for:
- Any type conflicts will result in an error. For example, you can’t unmarshal a
string
into aint
, even if the string value is a stringified number:"speed": "42"
- A floating-point number can’t be decoded into an integer
- A
null
value can’t be decoded into a value that doesn’t have anil
option. For example, if you have a number field that can benull
, you should unmarshal into a*int
- A
time.Time
can only decode an RFC 3339 string — other kinds of timestamps will fail
Get a back-end job without spending $10k on a
bootcamp
- Learn Python, Javascript and Go
- Build the professional projects you need to land your first job
- Spend about 6 months (when done part-time)
- Pricing as low as $24/month*
- No risk. Cancel anytime.
Custom JSON marshaling 🔗
While most types have a default way to encode and decode JSON data, you may want custom behavior from time to time. Luckily, the json.Marshal
and json.Unmarshal
respect the json.Marshaler
and json.Unmarshaler
interfaces. To customize your behavior you just need to overwrite their methods MarshalJSON
and UnmarshalJSON
respectively.
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
One of the most common scenarios for me is want to encode and decode timestamps in a different format, usually due to interoperability with another language like JavaScript.
type Group struct {
ID string `json:"id"`
CreatedAt unixTimestamp `json:"created_at"`
}
type unixTimestamp time.Time
func (ut unixTimestamp) MarshalJSON() ([]byte, error) {
s := strconv.Itoa(int(time.Time(ut).Unix()))
return []byte(s), nil
}
func (ut *unixTimestamp) UnmarshalJSON(dat []byte) error {
unix, err := strconv.Atoi(string(dat))
if err != nil {
return err
}
*ut = unixTimestamp(time.Unix(int64(unix), 0))
return nil
}
func main() {
g := Group{
ID: "my-id",
CreatedAt: unixTimestamp(time.Unix(1619544689, 0)),
}
dat, _ := json.Marshal(g)
fmt.Println(string(dat))
// prints
// {"id":"my-id","created_at":1619544689}
newG := Group{}
json.Unmarshal(dat, &newG)
fmt.Println(newG)
// prints
// {my-id {0 63755141489 0x1694c0}}
}
Arbitrary JSON with map[string]interface 🔗
It’s unfortunate when this is the case, but sometimes we have to work with arbitrary JSON data. For example, you need to decode some JSON data, but you aren’t sure what the key structure or shape of the data is.
The best way to handle this case is to unmarshal the data into a map[string]interface{}
dat := []byte(`{
"first_name": "lane",
"age": 30
}`)
m := map[string]interface{}{}
json.Unmarshal(dat, &m)
for k, v := range m {
fmt.Printf("key: %v, value: %vn", k, v)
}
// prints
// key: first_name, value: lane
// key: age, value: 30
I want to point out that map[string]interface{}
should only be used when you absolutely have to. If you have a priori knowledge of the shape of the data, please use a struct
or some other concrete type. Avoid the dynamic typing provided by interfaces when working with JSON, if you want, you can always use anonymous structs for one-off usage.
A simple path to your career in back-end development
The pace of Boot.dev’s JavaScript, Python and Go courses has been perfect for me.
The diverse community in Discord is a blast,
and other members are quick to help out with detailed answers and explanations.
— Daniel Gerep from Cassia, Brasil
Streaming JSON encodings 🔗
Sometimes you don’t have the luxury of reading all the JSON data to or from a []byte
. If you need to be able to parse data as it’s streamed in or out of your program the encoding/json
package provides Decoder and Encoder types.
func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder
Take a look at the following example. It decodes data from standard in, adds a new key "id"
with a value of "gopher-man"
and writes the result to standard out.
dec := json.NewDecoder(os.Stdin)
enc := json.NewEncoder(os.Stdout)
for {
v := map[string]interface{}{}
if err := dec.Decode(&v); err != nil {
log.Fatal(err)
}
v["id"] = "gopher-man"
if err := enc.Encode(&v); err != nil {
log.Fatal(err)
}
}
Pretty printing JSON 🔗
By default, the json.Marshal
function compresses all the whitespace in the encoded data for efficiency. If you need to print out your JSON data so that it’s more easily readable you can pretty-print it using the json.MarshalIndent function.
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
You can customize how you want your pretty JSON to be formatted, but if you just want it to have proper tabs and newlines you can do the following.
type user struct {
Name string
Age int
}
json, err := json.MarshalIndent(user{Name: "lane", Age: 30}, "", " ")
if err != nil {
return err
}
fmt.Println(string(json))
// prints
// {
// "Name": "lane",
// "Age": 30
// }
Faster JSON encoding and decoding 🔗
Sometimes performance is key, and when you need to squeeze every bit of power out of your CPU cycles you may notice that the standard library’s JSON marshaling and unmarshalling isn’t as performant as you might like. There’s a fairly popular library out there called ffjson that can speed up your marshaling performance by ~2-3x.
ffjson
generates staticMarshalJSON
andUnmarshalJSON
functions for structures in Go. The generated functions reduce the reliance upon runtime reflection to do serialization and are generally 2 to 3 times faster. In cases whereffjson
doesn’t understand a Type involved, it falls back toencoding/json
, meaning it is a safe drop in replacement. By usingffjson
your JSON serialization just gets faster with no additional code changes.ffjson Readme.md
If you’re thinking about using this library let me give you my quick two cents: I’ve never actually needed to speed up my JSON performance, it’s never been a bottleneck in my apps. If you desperately need to increase performance I think this is a great tool to look into, but don’t add needless dependencies to your code when the benefit they offer isn’t truly necessary.
Learn back-end without spending $10,000
- Write modern code in JavaScript, Python and Go
- Build and deploy real backend projects to your personal portfolio
- Compete in the job market by mastering computer science fundamentals
Find a problem with this article?
Report an issue on GitHub