Error unmarshaling json

Недавно мне довелось разрабатывать на Go http-клиент для сервиса, предоставляющего REST API с json-ом в роли формата кодирования. Стандартная задача, но в ходе...

image

Недавно мне довелось разрабатывать на 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 — это широко используемый инструмент для обмена информацией, и одним из его преимуществ перед другими форматами является наличие типов данных. За соблюдением этих типов надо строго следить.

Package json implements encoding and decoding of JSON as defined in
RFC 7159. The mapping between JSON and Go values is described
in the documentation for the Marshal and Unmarshal functions.

See «JSON and Go» for an introduction to this package:
https://golang.org/doc/articles/json_and_go.html

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"strings"
)

type Animal int

const (
	Unknown Animal = iota
	Gopher
	Zebra
)

func (a *Animal) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}
	switch strings.ToLower(s) {
	default:
		*a = Unknown
	case "gopher":
		*a = Gopher
	case "zebra":
		*a = Zebra
	}

	return nil
}

func (a Animal) MarshalJSON() ([]byte, error) {
	var s string
	switch a {
	default:
		s = "unknown"
	case Gopher:
		s = "gopher"
	case Zebra:
		s = "zebra"
	}

	return json.Marshal(s)
}

func main() {
	blob := `["gopher","armadillo","zebra","unknown","gopher","bee","gopher","zebra"]`
	var zoo []Animal
	if err := json.Unmarshal([]byte(blob), &zoo); err != nil {
		log.Fatal(err)
	}

	census := make(map[Animal]int)
	for _, animal := range zoo {
		census[animal] += 1
	}

	fmt.Printf("Zoo Census:n* Gophers: %dn* Zebras:  %dn* Unknown: %dn",
		census[Gopher], census[Zebra], census[Unknown])

}
Output:

Zoo Census:
* Gophers: 3
* Zebras:  2
* Unknown: 3
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"strings"
)

type Size int

const (
	Unrecognized Size = iota
	Small
	Large
)

func (s *Size) UnmarshalText(text []byte) error {
	switch strings.ToLower(string(text)) {
	default:
		*s = Unrecognized
	case "small":
		*s = Small
	case "large":
		*s = Large
	}
	return nil
}

func (s Size) MarshalText() ([]byte, error) {
	var name string
	switch s {
	default:
		name = "unrecognized"
	case Small:
		name = "small"
	case Large:
		name = "large"
	}
	return []byte(name), nil
}

func main() {
	blob := `["small","regular","large","unrecognized","small","normal","small","large"]`
	var inventory []Size
	if err := json.Unmarshal([]byte(blob), &inventory); err != nil {
		log.Fatal(err)
	}

	counts := make(map[Size]int)
	for _, size := range inventory {
		counts[size] += 1
	}

	fmt.Printf("Inventory Counts:n* Small:        %dn* Large:        %dn* Unrecognized: %dn",
		counts[Small], counts[Large], counts[Unrecognized])

}
Output:

Inventory Counts:
* Small:        3
* Large:        2
* Unrecognized: 3
  • func Compact(dst *bytes.Buffer, src []byte) error
  • func HTMLEscape(dst *bytes.Buffer, src []byte)
  • func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error
  • func Marshal(v any) ([]byte, error)
  • func MarshalIndent(v any, prefix, indent string) ([]byte, error)
  • func Unmarshal(data []byte, v any) error
  • func Valid(data []byte) bool
  • type Decoder
    • func NewDecoder(r io.Reader) *Decoder
    • func (dec *Decoder) Buffered() io.Reader
    • func (dec *Decoder) Decode(v any) error
    • func (dec *Decoder) DisallowUnknownFields()
    • func (dec *Decoder) InputOffset() int64
    • func (dec *Decoder) More() bool
    • func (dec *Decoder) Token() (Token, error)
    • func (dec *Decoder) UseNumber()
  • type Delim
    • func (d Delim) String() string
  • type Encoder
    • func NewEncoder(w io.Writer) *Encoder
    • func (enc *Encoder) Encode(v any) error
    • func (enc *Encoder) SetEscapeHTML(on bool)
    • func (enc *Encoder) SetIndent(prefix, indent string)
  • type InvalidUTF8Errordeprecated
    • func (e *InvalidUTF8Error) Error() string
  • type InvalidUnmarshalError
    • func (e *InvalidUnmarshalError) Error() string
  • type Marshaler
  • type MarshalerError
    • func (e *MarshalerError) Error() string
    • func (e *MarshalerError) Unwrap() error
  • type Number
    • func (n Number) Float64() (float64, error)
    • func (n Number) Int64() (int64, error)
    • func (n Number) String() string
  • type RawMessage
    • func (m RawMessage) MarshalJSON() ([]byte, error)
    • func (m *RawMessage) UnmarshalJSON(data []byte) error
  • type SyntaxError
    • func (e *SyntaxError) Error() string
  • type Token
  • type UnmarshalFieldErrordeprecated
    • func (e *UnmarshalFieldError) Error() string
  • type UnmarshalTypeError
    • func (e *UnmarshalTypeError) Error() string
  • type Unmarshaler
  • type UnsupportedTypeError
    • func (e *UnsupportedTypeError) Error() string
  • type UnsupportedValueError
    • func (e *UnsupportedValueError) Error() string
  • Package (CustomMarshalJSON)
  • Package (TextMarshalJSON)
  • Decoder
  • Decoder.Decode (Stream)
  • Decoder.Token
  • HTMLEscape
  • Indent
  • Marshal
  • MarshalIndent
  • RawMessage (Marshal)
  • RawMessage (Unmarshal)
  • Unmarshal
  • Valid

This section is empty.

This section is empty.

Compact appends to dst the JSON-encoded src with
insignificant space characters elided.

HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029
characters inside string literals changed to u003c, u003e, u0026, u2028, u2029
so that the JSON will be safe to embed inside HTML <script> tags.
For historical reasons, web browsers don’t honor standard HTML
escaping within <script> tags, so an alternative JSON encoding must
be used.

package main

import (
	"bytes"
	"encoding/json"
	"os"
)

func main() {
	var out bytes.Buffer
	json.HTMLEscape(&out, []byte(`{"Name":"<b>HTML content</b>"}`))
	out.WriteTo(os.Stdout)
}
Output:

{"Name":"u003cbu003eHTML contentu003c/bu003e"}

Indent appends to dst an indented form of the JSON-encoded src.
Each element in a JSON object or array begins on a new,
indented line beginning with prefix followed by one or more
copies of indent according to the indentation nesting.
The data appended to dst does not begin with the prefix nor
any indentation, to make it easier to embed inside other formatted JSON data.
Although leading space characters (space, tab, carriage return, newline)
at the beginning of src are dropped, trailing space characters
at the end of src are preserved and copied to dst.
For example, if src has no trailing spaces, neither will dst;
if src ends in a trailing newline, so will dst.

package main

import (
	"bytes"
	"encoding/json"
	"log"
	"os"
)

func main() {
	type Road struct {
		Name   string
		Number int
	}
	roads := []Road{
		{"Diamond Fork", 29},
		{"Sheep Creek", 51},
	}

	b, err := json.Marshal(roads)
	if err != nil {
		log.Fatal(err)
	}

	var out bytes.Buffer
	json.Indent(&out, b, "=", "t")
	out.WriteTo(os.Stdout)
}
Output:

[
=	{
=		"Name": "Diamond Fork",
=		"Number": 29
=	},
=	{
=		"Name": "Sheep Creek",
=		"Number": 51
=	}
=]

Marshal returns the JSON encoding of v.

Marshal traverses the value v recursively.
If an encountered value implements the Marshaler interface
and is not a nil pointer, Marshal calls its MarshalJSON method
to produce JSON. If no MarshalJSON method is present but the
value implements encoding.TextMarshaler instead, Marshal calls
its MarshalText method and encodes the result as a JSON string.
The nil pointer exception is not strictly necessary
but mimics a similar, necessary exception in the behavior of
UnmarshalJSON.

Otherwise, Marshal uses the following type-dependent default encodings:

Boolean values encode as JSON booleans.

Floating point, integer, and Number values encode as JSON numbers.

String values encode as JSON strings coerced to valid UTF-8,
replacing invalid bytes with the Unicode replacement rune.
So that the JSON will be safe to embed inside HTML <script> tags,
the string is encoded using HTMLEscape,
which replaces «<«, «>», «&», U+2028, and U+2029 are escaped
to «u003c»,»u003e», «u0026», «u2028», and «u2029».
This replacement can be disabled when using an Encoder,
by calling SetEscapeHTML(false).

Array and slice values encode as JSON arrays, except that
[]byte encodes as a base64-encoded string, and a nil slice
encodes as the null JSON value.

Struct values encode as JSON objects.
Each exported struct field becomes a member of the object, using the
field name as the object key, unless the field is omitted for one of the
reasons given below.

The encoding of each struct field can be customized by the format string
stored under the «json» key in the struct field’s tag.
The format string gives the name of the field, possibly followed by a
comma-separated list of options. The name may be empty in order to
specify options without overriding the default field name.

The «omitempty» option specifies that the field should be omitted
from the encoding if the field has an empty value, defined as
false, 0, a nil pointer, a nil interface value, and any empty array,
slice, map, or string.

As a special case, if the field tag is «-«, the field is always omitted.
Note that a field with name «-» can still be generated using the tag «-,».

Examples of struct field tags and their meanings:

// Field appears in JSON as key "myName".
Field int `json:"myName"`

// Field appears in JSON as key "myName" and
// the field is omitted from the object if its value is empty,
// as defined above.
Field int `json:"myName,omitempty"`

// Field appears in JSON as key "Field" (the default), but
// the field is skipped if empty.
// Note the leading comma.
Field int `json:",omitempty"`

// Field is ignored by this package.
Field int `json:"-"`

// Field appears in JSON as key "-".
Field int `json:"-,"`

The «string» option signals that a field is stored as JSON inside a
JSON-encoded string. It applies only to fields of string, floating point,
integer, or boolean types. This extra level of encoding is sometimes used
when communicating with JavaScript programs:

Int64String int64 `json:",string"`

The key name will be used if it’s a non-empty string consisting of
only Unicode letters, digits, and ASCII punctuation except quotation
marks, backslash, and comma.

Anonymous struct fields are usually marshaled as if their inner exported fields
were fields in the outer struct, subject to the usual Go visibility rules amended
as described in the next paragraph.
An anonymous struct field with a name given in its JSON tag is treated as
having that name, rather than being anonymous.
An anonymous struct field of interface type is treated the same as having
that type as its name, rather than being anonymous.

The Go visibility rules for struct fields are amended for JSON when
deciding which field to marshal or unmarshal. If there are
multiple fields at the same level, and that level is the least
nested (and would therefore be the nesting level selected by the
usual Go rules), the following extra rules apply:

1) Of those fields, if any are JSON-tagged, only tagged fields are considered,
even if there are multiple untagged fields that would otherwise conflict.

2) If there is exactly one field (tagged or not according to the first rule), that is selected.

3) Otherwise there are multiple fields, and all are ignored; no error occurs.

Handling of anonymous struct fields is new in Go 1.1.
Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of
an anonymous struct field in both current and earlier versions, give the field
a JSON tag of «-«.

Map values encode as JSON objects. The map’s key type must either be a
string, an integer type, or implement encoding.TextMarshaler. The map keys
are sorted and used as JSON object keys by applying the following rules,
subject to the UTF-8 coercion described for string values above:

  • keys of any string type are used directly
  • encoding.TextMarshalers are marshaled
  • integer keys are converted to strings

Pointer values encode as the value pointed to.
A nil pointer encodes as the null JSON value.

Interface values encode as the value contained in the interface.
A nil interface value encodes as the null JSON value.

Channel, complex, and function values cannot be encoded in JSON.
Attempting to encode such a value causes Marshal to return
an UnsupportedTypeError.

JSON cannot represent cyclic data structures and Marshal does not
handle them. Passing cyclic structures to Marshal will result in
an error.

package main

import (
	"encoding/json"
	"fmt"
	"os"
)

func main() {
	type ColorGroup struct {
		ID     int
		Name   string
		Colors []string
	}
	group := ColorGroup{
		ID:     1,
		Name:   "Reds",
		Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
	}
	b, err := json.Marshal(group)
	if err != nil {
		fmt.Println("error:", err)
	}
	os.Stdout.Write(b)
}
Output:

{"ID":1,"Name":"Reds","Colors":["Crimson","Red","Ruby","Maroon"]}

MarshalIndent is like Marshal but applies Indent to format the output.
Each JSON element in the output will begin on a new line beginning with prefix
followed by one or more copies of indent according to the indentation nesting.

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	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))
}
Output:

{
<prefix><indent>"a": 1,
<prefix><indent>"b": 2
<prefix>}

Unmarshal parses the JSON-encoded data and stores the result
in the value pointed to by v. If v is nil or not a pointer,
Unmarshal returns an InvalidUnmarshalError.

Unmarshal uses the inverse of the encodings that
Marshal uses, allocating maps, slices, and pointers as necessary,
with the following additional rules:

To unmarshal JSON into a pointer, Unmarshal first handles the case of
the JSON being the JSON literal null. In that case, Unmarshal sets
the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into
the value pointed at by the pointer. If the pointer is nil, Unmarshal
allocates a new value for it to point to.

To unmarshal JSON into a value implementing the Unmarshaler interface,
Unmarshal calls that value’s UnmarshalJSON method, including
when the input is a JSON null.
Otherwise, if the value implements encoding.TextUnmarshaler
and the input is a JSON quoted string, Unmarshal calls that value’s
UnmarshalText method with the unquoted form of the string.

To unmarshal JSON into a struct, Unmarshal matches incoming object
keys to the keys used by Marshal (either the struct field name or its tag),
preferring an exact match but also accepting a case-insensitive match. By
default, object keys which don’t have a corresponding struct field are
ignored (see Decoder.DisallowUnknownFields for an alternative).

To unmarshal JSON into an interface value,
Unmarshal stores one of these in the interface value:

bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null

To unmarshal a JSON array into a slice, Unmarshal resets the slice length
to zero and then appends each element to the slice.
As a special case, to unmarshal an empty JSON array into a slice,
Unmarshal replaces the slice with a new empty slice.

To unmarshal a JSON array into a Go array, Unmarshal decodes
JSON array elements into corresponding Go array elements.
If the Go array is smaller than the JSON array,
the additional JSON array elements are discarded.
If the JSON array is smaller than the Go array,
the additional Go array elements are set to zero values.

To unmarshal a JSON object into a map, Unmarshal first establishes a map to
use. If the map is nil, Unmarshal allocates a new map. Otherwise Unmarshal
reuses the existing map, keeping existing entries. Unmarshal then stores
key-value pairs from the JSON object into the map. The map’s key type must
either be any string type, an integer, implement json.Unmarshaler, or
implement encoding.TextUnmarshaler.

If the JSON-encoded data contain a syntax error, Unmarshal returns a SyntaxError.

If a JSON value is not appropriate for a given target type,
or if a JSON number overflows the target type, Unmarshal
skips that field and completes the unmarshaling as best it can.
If no more serious errors are encountered, Unmarshal returns
an UnmarshalTypeError describing the earliest such error. In any
case, it’s not guaranteed that all the remaining fields following
the problematic one will be unmarshaled into the target object.

The JSON null value unmarshals into an interface, map, pointer, or slice
by setting that Go value to nil. Because null is often used in JSON to mean
“not present,” unmarshaling a JSON null into any other Go type has no effect
on the value and produces no error.

When unmarshaling quoted strings, invalid UTF-8 or
invalid UTF-16 surrogate pairs are not treated as an error.
Instead, they are replaced by the Unicode replacement
character U+FFFD.

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	var jsonBlob = []byte(`[
	{"Name": "Platypus", "Order": "Monotremata"},
	{"Name": "Quoll",    "Order": "Dasyuromorphia"}
]`)
	type Animal struct {
		Name  string
		Order string
	}
	var animals []Animal
	err := json.Unmarshal(jsonBlob, &animals)
	if err != nil {
		fmt.Println("error:", err)
	}
	fmt.Printf("%+v", animals)
}
Output:

[{Name:Platypus Order:Monotremata} {Name:Quoll Order:Dasyuromorphia}]

Valid reports whether data is a valid JSON encoding.

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	goodJSON := `{"example": 1}`
	badJSON := `{"example":2:]}}`

	fmt.Println(json.Valid([]byte(goodJSON)), json.Valid([]byte(badJSON)))
}
Output:

true false

A Decoder reads and decodes JSON values from an input stream.

This example uses a Decoder to decode a stream of distinct JSON values.

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"strings"
)

func main() {
	const jsonStream = `
	{"Name": "Ed", "Text": "Knock knock."}
	{"Name": "Sam", "Text": "Who's there?"}
	{"Name": "Ed", "Text": "Go fmt."}
	{"Name": "Sam", "Text": "Go fmt who?"}
	{"Name": "Ed", "Text": "Go fmt yourself!"}
`
	type Message struct {
		Name, Text string
	}
	dec := json.NewDecoder(strings.NewReader(jsonStream))
	for {
		var m Message
		if err := dec.Decode(&m); err == io.EOF {
			break
		} else if err != nil {
			log.Fatal(err)
		}
		fmt.Printf("%s: %sn", m.Name, m.Text)
	}
}
Output:

Ed: Knock knock.
Sam: Who's there?
Ed: Go fmt.
Sam: Go fmt who?
Ed: Go fmt yourself!

NewDecoder returns a new decoder that reads from r.

The decoder introduces its own buffering and may
read data from r beyond the JSON values requested.

Buffered returns a reader of the data remaining in the Decoder’s
buffer. The reader is valid until the next call to Decode.

Decode reads the next JSON-encoded value from its
input and stores it in the value pointed to by v.

See the documentation for Unmarshal for details about
the conversion of JSON into a Go value.

This example uses a Decoder to decode a streaming array of JSON objects.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"strings"
)

func main() {
	const jsonStream = `
	[
		{"Name": "Ed", "Text": "Knock knock."},
		{"Name": "Sam", "Text": "Who's there?"},
		{"Name": "Ed", "Text": "Go fmt."},
		{"Name": "Sam", "Text": "Go fmt who?"},
		{"Name": "Ed", "Text": "Go fmt yourself!"}
	]
`
	type Message struct {
		Name, Text string
	}
	dec := json.NewDecoder(strings.NewReader(jsonStream))

	// read open bracket
	t, err := dec.Token()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%T: %vn", t, t)

	// while the array contains values
	for dec.More() {
		var m Message
		// decode an array value (Message)
		err := dec.Decode(&m)
		if err != nil {
			log.Fatal(err)
		}

		fmt.Printf("%v: %vn", m.Name, m.Text)
	}

	// read closing bracket
	t, err = dec.Token()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%T: %vn", t, t)

}
Output:

json.Delim: [
Ed: Knock knock.
Sam: Who's there?
Ed: Go fmt.
Sam: Go fmt who?
Ed: Go fmt yourself!
json.Delim: ]
func (dec *Decoder) DisallowUnknownFields()

DisallowUnknownFields causes the Decoder to return an error when the destination
is a struct and the input contains object keys which do not match any
non-ignored, exported fields in the destination.

func (dec *Decoder) InputOffset() int64

InputOffset returns the input stream byte offset of the current decoder position.
The offset gives the location of the end of the most recently returned token
and the beginning of the next token.

func (dec *Decoder) More() bool

More reports whether there is another element in the
current array or object being parsed.

Token returns the next JSON token in the input stream.
At the end of the input stream, Token returns nil, io.EOF.

Token guarantees that the delimiters [ ] { } it returns are
properly nested and matched: if Token encounters an unexpected
delimiter in the input, it will return an error.

The input stream consists of basic JSON values—bool, string,
number, and null—along with delimiters [ ] { } of type Delim
to mark the start and end of arrays and objects.
Commas and colons are elided.

This example uses a Decoder to decode a stream of distinct JSON values.

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"strings"
)

func main() {
	const jsonStream = `
	{"Message": "Hello", "Array": [1, 2, 3], "Null": null, "Number": 1.234}
`
	dec := json.NewDecoder(strings.NewReader(jsonStream))
	for {
		t, err := dec.Token()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
		fmt.Printf("%T: %v", t, t)
		if dec.More() {
			fmt.Printf(" (more)")
		}
		fmt.Printf("n")
	}
}
Output:

json.Delim: { (more)
string: Message (more)
string: Hello (more)
string: Array (more)
json.Delim: [ (more)
float64: 1 (more)
float64: 2 (more)
float64: 3
json.Delim: ] (more)
string: Null (more)
<nil>: <nil> (more)
string: Number (more)
float64: 1.234
json.Delim: }
func (dec *Decoder) UseNumber()

UseNumber causes the Decoder to unmarshal a number into an interface{} as a
Number instead of as a float64.

A Delim is a JSON array or object delimiter, one of [ ] { or }.

An Encoder writes JSON values to an output stream.

NewEncoder returns a new encoder that writes to w.

Encode writes the JSON encoding of v to the stream,
followed by a newline character.

See the documentation for Marshal for details about the
conversion of Go values to JSON.

func (enc *Encoder) SetEscapeHTML(on bool)

SetEscapeHTML specifies whether problematic HTML characters
should be escaped inside JSON quoted strings.
The default behavior is to escape &, <, and > to u0026, u003c, and u003e
to avoid certain safety problems that can arise when embedding JSON in HTML.

In non-HTML settings where the escaping interferes with the readability
of the output, SetEscapeHTML(false) disables this behavior.

func (enc *Encoder) SetIndent(prefix, indent string)

SetIndent instructs the encoder to format each subsequent encoded
value as if indented by the package-level function Indent(dst, src, prefix, indent).
Calling SetIndent(«», «») disables indentation.

type InvalidUTF8Error struct {
	S string 
}

Before Go 1.2, an InvalidUTF8Error was returned by Marshal when
attempting to encode a string value with invalid UTF-8 sequences.
As of Go 1.2, Marshal instead coerces the string to valid UTF-8 by
replacing invalid bytes with the Unicode replacement rune U+FFFD.

Deprecated: No longer used; kept for compatibility.

type InvalidUnmarshalError struct {
	Type reflect.Type
}

An InvalidUnmarshalError describes an invalid argument passed to Unmarshal.
(The argument to Unmarshal must be a non-nil pointer.)

type Marshaler interface {
	MarshalJSON() ([]byte, error)
}

Marshaler is the interface implemented by types that
can marshal themselves into valid JSON.

A MarshalerError represents an error from calling a MarshalJSON or MarshalText method.

Unwrap returns the underlying error.

A Number represents a JSON number literal.

Float64 returns the number as a float64.

Int64 returns the number as an int64.

String returns the literal text of the number.

RawMessage is a raw encoded JSON value.
It implements Marshaler and Unmarshaler and can
be used to delay JSON decoding or precompute a JSON encoding.

This example uses RawMessage to use a precomputed JSON during marshal.

package main

import (
	"encoding/json"
	"fmt"
	"os"
)

func main() {
	h := json.RawMessage(`{"precomputed": true}`)

	c := struct {
		Header *json.RawMessage `json:"header"`
		Body   string           `json:"body"`
	}{Header: &h, Body: "Hello Gophers!"}

	b, err := json.MarshalIndent(&c, "", "t")
	if err != nil {
		fmt.Println("error:", err)
	}
	os.Stdout.Write(b)

}
Output:

{
	"header": {
		"precomputed": true
	},
	"body": "Hello Gophers!"
}

This example uses RawMessage to delay parsing part of a JSON message.

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	type Color struct {
		Space string
		Point json.RawMessage // delay parsing until we know the color space
	}
	type RGB struct {
		R uint8
		G uint8
		B uint8
	}
	type YCbCr struct {
		Y  uint8
		Cb int8
		Cr int8
	}

	var j = []byte(`[
	{"Space": "YCbCr", "Point": {"Y": 255, "Cb": 0, "Cr": -10}},
	{"Space": "RGB",   "Point": {"R": 98, "G": 218, "B": 255}}
]`)
	var colors []Color
	err := json.Unmarshal(j, &colors)
	if err != nil {
		log.Fatalln("error:", err)
	}

	for _, c := range colors {
		var dst any
		switch c.Space {
		case "RGB":
			dst = new(RGB)
		case "YCbCr":
			dst = new(YCbCr)
		}
		err := json.Unmarshal(c.Point, dst)
		if err != nil {
			log.Fatalln("error:", err)
		}
		fmt.Println(c.Space, dst)
	}
}
Output:

YCbCr &{255 0 -10}
RGB &{98 218 255}

MarshalJSON returns m as the JSON encoding of m.

UnmarshalJSON sets *m to a copy of data.

type SyntaxError struct {
	Offset int64 
	
}

A SyntaxError is a description of a JSON syntax error.
Unmarshal will return a SyntaxError if the JSON can’t be parsed.

A Token holds a value of one of these types:

Delim, for the four JSON delimiters [ ] { }
bool, for JSON booleans
float64, for JSON numbers
Number, for JSON numbers
string, for JSON string literals
nil, for JSON null

An UnmarshalFieldError describes a JSON object key that
led to an unexported (and therefore unwritable) struct field.

Deprecated: No longer used; kept for compatibility.

An UnmarshalTypeError describes a JSON value that was
not appropriate for a value of a specific Go type.

type Unmarshaler interface {
	UnmarshalJSON([]byte) error
}

Unmarshaler is the interface implemented by types
that can unmarshal a JSON description of themselves.
The input can be assumed to be a valid encoding of
a JSON value. UnmarshalJSON must copy the JSON data
if it wishes to retain the data after returning.

By convention, to approximate the behavior of Unmarshal itself,
Unmarshalers implement UnmarshalJSON([]byte(«null»)) as a no-op.

An UnsupportedTypeError is returned by Marshal when attempting
to encode an unsupported value type.

An UnsupportedValueError is returned by Marshal when attempting
to encode an unsupported value.

Sometimes your client side application could send a valid integer as a string while your application expect an integer.
This leads you to getting the error json: cannot unmarshal string into Go struct field Item.item_id of type int when you try to convert the json into a struct.

For example consider this snippet.

You can find the full code snippet on github

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
	type Item struct {
		Name   string `json:"name"`
		ItemId int    `json:"item_id"`
	}
	jsonData:= []byte(`{"name":"item 1","item_id":"30"}`)
	var item Item
	err:=json.Unmarshal(jsonData,&item)
	if err!=nil {
		log.Fatal(err)
	}

We would expect that our application will convert string “30” to integer 30 since it’s a valid integer.

However, we will get the following error instead:
json: cannot unmarshal string into Go struct field Item.item_id of type int

How to solve this Issue

The good news is golang allows us to write our own custom json marshaller and unmarshalers.
We can create our own type alias for int type and add a custom marshaller and unmarshaler for our json
this way we can check our data and convert our string to int before unmarshal

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// StringInt create a type alias for type int
type StringInt int

// UnmarshalJSON create a custom unmarshal for the StringInt
/// this helps us check the type of our value before unmarshalling it

func (st *StringInt) UnmarshalJSON(b []byte) error {
    //convert the bytes into an interface
	//this will help us check the type of our value
	//if it is a string that can be converted into an int we convert it
	///otherwise we return an error
	var item interface{}
	if err := json.Unmarshal(b, &item); err != nil {
		return err
	}
	switch v := item.(type) {
	case int:
		*st = StringInt(v)
	case float64:
		*st = StringInt(int(v))
	case string:
		///here convert the string into
		///an integer
		i, err := strconv.Atoi(v)
		if err != nil {
			///the string might not be of integer type
			///so return an error
			return err

		}
		*st = StringInt(i)

	}
	return nil
}

func main() {

 //We can now send our item as either an int or string without getting any error
	type Item struct {
		Name   string    `json:"name"`
		ItemId StringInt `json:"item_id"`
	}
	jsonData := []byte(`{"name":"item 1","item_id":"30"}`)
	var item Item
	err := json.Unmarshal(jsonData, &item)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%+vn", item)

}

The output will now be:
{Name:item 1 ItemId:30}
We can now send ItemId json as a string without getting any error.

You can find the full code snippet on github

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 coffeeBuy me a coffee

Tags:
#json
#reflect

Понравилась статья? Поделить с друзьями:
  • Error unmappable character for encoding utf8
  • Error unmappable character 0x98 for encoding windows 1251 javadoc
  • Error unmappable character 0x98 for encoding windows 1251 gradle
  • Error unmappable character 0x81 for encoding windows 1252
  • Error unlocking cassettes