Перейти к содержимому

encoding/json: сериализация JSON в Go

Кратко: encoding/json — стандартный пакет для работы с JSON. Используется в 99% сервисов: REST API, конфиги, логи. Несмотря на простоту API (Marshal/Unmarshal), там куча подводных камней: невидимое поведение для unexported полей, проблемы с числами в JS-клиентах, NaN, performance.

Зачем знать джуну: Каждый REST endpoint, который ты напишешь, использует JSON. Спросят про struct tags, custom marshalers, разницу json.Number vs float64. В production будут баги типа “поле теряется при сериализации” — нужно знать, что unexported не маршалится.

  1. Базовое API: Marshal, Unmarshal, Encoder, Decoder
  2. Под капотом: reflection, struct tags, generics
  3. Gotchas: типичные ловушки
  4. Производительность: альтернативы, бенчмарки
  5. Вопросы на собесе
  6. Practice
  7. Источники

package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
u := User{ID: 1, Name: "Алиса", Email: "a@b.com"}
// Marshal: struct → JSON
data, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(data))
// {"id":1,"name":"Алиса","email":"a@b.com"}
// Unmarshal: JSON → struct
var u2 User
err = json.Unmarshal(data, &u2) // обязательно указатель!
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", u2)
// {ID:1 Name:Алиса Email:a@b.com}
}

⚠️ Unmarshal требует указатель — иначе функция не сможет изменить значение.

data, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(data))
// {
// "id": 1,
// "name": "Алиса",
// "email": "a@b.com"
// }

Удобно для конфигов или дебага. В сети — лучше компактный Marshal.

type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price,omitempty"` // не сериализовать если 0
Internal string `json:"-"` // никогда не сериализовать
SKU string `json:"sku,string"` // как строка, даже если число
Notes string `json:",omitempty"` // имя по умолчанию = "Notes"
}
ТегЧто делает
json:"name"Использовать “name” вместо имени поля
json:"name,omitempty"Пропустить, если zero value
json:"-"Никогда не сериализовать
json:",omitempty"Имя по умолчанию + omit empty
json:"name,string"Сериализовать число как строку
  • 0 для чисел.
  • "" для строк.
  • nil для указателей, слайсов, мап, интерфейсов.
  • false для bool.
  • НЕ empty: пустой не-nil слайс []int{}, пустая не-nil мапа map[string]int{}, struct (даже если все поля нулевые).

⚠️ Поэтому omitempty не работает для struct полей. Чтобы пропускать nullable struct — используй указатель: *Address вместо Address.

// Encoder — пишет в io.Writer
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
enc.Encode(u)
// Decoder — читает из io.Reader
dec := json.NewDecoder(r)
dec.DisallowUnknownFields() // strict mode
err := dec.Decode(&u)

Когда использовать:

  • Marshal/Unmarshal — для маленьких объектов в памяти.
  • Encoder/Decoder — для streams (HTTP body, файлы, NDJSON).
dec := json.NewDecoder(file)
for dec.More() {
var item Item
if err := dec.Decode(&item); err != nil {
return err
}
// обработать item
}

Удобно для логов и больших файлов — не грузим всё в память.

type User struct {
name string `json:"name"` // unexported → НЕ маршалится! (silent)
Age int `json:"age"` // exported → ок
}
u := User{name: "Bob", Age: 30}
data, _ := json.Marshal(u)
fmt.Println(string(data))
// {"age":30} ← name пропал!

⚠️ Это молчаливый баг. Никаких предупреждений. Если поле начинается со строчной буквы — json пакет его не видит (reflect не может прочитать unexported).

type S struct {
A []int `json:"a"`
B map[string]int `json:"b"`
}
s1 := S{} // nil slice, nil map
data, _ := json.Marshal(s1)
fmt.Println(string(data))
// {"a":null,"b":null}
s2 := S{A: []int{}, B: map[string]int{}}
data, _ = json.Marshal(s2)
fmt.Println(string(data))
// {"a":[],"b":{}}

Разница важна для клиентов: JS-клиент может различать null и [].

Если структура заранее неизвестна:

var m map[string]interface{}
err := json.Unmarshal(data, &m)
// или:
var m map[string]any // Go 1.18+
JSONGo (через interface{})
stringstring
numberfloat64 (!)
boolbool
nullnil
array[]interface{}
objectmap[string]interface{}

⚠️ Все числа → float64! Даже целые. Это создаёт проблемы:

data := []byte(`{"id": 12345678901234567890}`)
var m map[string]interface{}
json.Unmarshal(data, &m)
fmt.Println(m["id"]) // 1.2345678901234567e+19 — потеря точности

Решения:

  • Использовать json.Number (см. ниже).
  • Парсить в конкретный struct.
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var m map[string]interface{}
dec.Decode(&m)
n := m["id"].(json.Number)
intVal, err := n.Int64()
floatVal, err := n.Float64()
stringVal := n.String() // оригинальная строка

json.Number — это просто type Number string. Хранит число как строку, конвертирует по требованию. Не теряет точность.

Для денег, ID, и других “точных” значений лучше слать как строки (особенно для JS-клиентов — JS Number теряет точность после 2^53):

type Order struct {
ID int64 `json:"id,string"` // {"id": "12345"}
Total float64 `json:"total,string"` // {"total": "99.99"}
}

При маршалинге пишет в строку, при анмаршалинге парсит обратно.

Если нужно нестандартное поведение — реализуй интерфейсы:

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

Пример: формат даты:

type Date struct {
time.Time
}
func (d Date) MarshalJSON() ([]byte, error) {
return []byte(`"` + d.Format("2006-01-02") + `"`), nil
}
func (d *Date) UnmarshalJSON(data []byte) error {
s := strings.Trim(string(data), `"`)
t, err := time.Parse("2006-01-02", s)
if err != nil {
return err
}
d.Time = t
return nil
}

⚠️ Аккуратно с указателями vs значениями:

  • func (d Date) MarshalJSON() — будет вызван и для Date, и для *Date.
  • func (d *Date) UnmarshalJSON() — нужен указатель, чтобы изменять.
type Envelope struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}
func main() {
data := []byte(`{"type":"user","payload":{"id":1,"name":"Alice"}}`)
var env Envelope
json.Unmarshal(data, &env)
switch env.Type {
case "user":
var u User
json.Unmarshal(env.Payload, &u)
// ...
case "order":
var o Order
json.Unmarshal(env.Payload, &o)
}
}

RawMessage — это []byte, но сохраняется как-есть и не парсится. Идеален для discriminated unions (type field + payload).

type Base struct {
ID int `json:"id"`
CreatedAt time.Time `json:"created_at"`
}
type User struct {
Base
Name string `json:"name"`
}
u := User{Base: Base{ID: 1, CreatedAt: time.Now()}, Name: "Alice"}
data, _ := json.Marshal(u)
// {"id":1,"created_at":"...","name":"Alice"}

Поля embedded struct “вмержены” в JSON. Это работает только если embedded — anonymous field (без имени).

Если хочешь, чтобы был вложенный объект:

type User struct {
Meta Base `json:"meta"`
Name string `json:"name"`
}
// {"meta":{"id":1,...},"name":"Alice"}
dec := json.NewDecoder(r)
dec.DisallowUnknownFields()
err := dec.Decode(&user)
// если в JSON есть поле, которого нет в struct → ошибка

Хорошо для API, чтобы ловить опечатки клиентов. По умолчанию лишние поля игнорируются.

По умолчанию <, >, & экранируются в <, >, & — защита от XSS если JSON вставляется в HTML.

data, _ := json.Marshal(map[string]string{"q": "a < b"})
fmt.Println(string(data))
// {"q":"a < b"}

Для API без HTML-контекста — отключи:

enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
enc.Encode(v)
// {"q":"a < b"}
type Event struct {
Time time.Time `json:"time"`
}
e := Event{Time: time.Now()}
data, _ := json.Marshal(e)
// {"time":"2026-05-21T15:04:05.123456789Z"}

Для кастомного формата — Custom Marshaler (см. 1.10) или wrapper-тип.

⚠️ Подвох: time.Time маршалится в RFC3339Nano, но часто клиенты ждут unix timestamp. Сделай отдельный wrapper-тип, если нужно.


encoding/json использует пакет reflect для прохода по полям структуры в runtime. Это удобно (один Marshal на всё), но медленно относительно генерированного кода.

Marshal(v interface{}):
1. reflect.ValueOf(v) → получаем Value
2. reflect.TypeOf(v) → получаем Type
3. Для каждого поля Type.Field(i):
- читаем tag через field.Tag.Get("json")
- читаем значение через value.Field(i)
- рекурсивно маршалим
4. Кешируем структуру (encode caching)
5. Пишем в bytes.Buffer

После первого Marshal структуры — её “схема” кешируется. Поэтому второй вызов быстрее.

field, ok := reflect.TypeOf(u).Elem().FieldByName("Name")
tag := field.Tag.Get("json") // "name,omitempty"

Пакет json сам парсит эту строку: имя поля и опции через запятую.

data := []byte(`{"NAME": "Alice"}`)
type User struct {
Name string `json:"name"`
}
var u User
json.Unmarshal(data, &u)
fmt.Println(u.Name) // "Alice"

При unmarshal Go сначала ищет точное совпадение, потом case-insensitive. Это упрощает работу с разными источниками, но может скрыть опечатки.

⚠️ Marshal всегда пишет точно как в теге.

Тип GoJSON
booltrue/false
Целые (int, int64, …)number
Float (float32, float64)number
stringstring
[]bytebase64-кодированная string
time.TimeRFC3339 string
*Tnull или значение
[]Tarray
map[string]Tobject
structobject
interface{}по типу значения
nil slice/mapnull
Каналы, функцииошибка
data, err := json.Marshal(math.NaN())
fmt.Println(err) // json: unsupported value: NaN

JSON не имеет специальных значений для NaN/Inf. Если у тебя в данных может быть NaN — придётся обработать самому (заменить на null или строку через Custom Marshaler).

Map в Go — unordered. При Marshal ключи сортируются для детерминированного вывода (с Go 1.12+). Это полезно для тестов и diff’ов.

m := map[string]int{"b": 2, "a": 1, "c": 3}
data, _ := json.Marshal(m)
// {"a":1,"b":2,"c":3} ← всегда отсортировано
m := map[int]string{1: "a"}
data, _ := json.Marshal(m)
// {"1":"a"} ← int → string ОК (с Go 1.7+)
type ID int
m2 := map[ID]string{1: "a"}
data, _ = json.Marshal(m2) // тоже работает
m3 := map[struct{ X int }]string{{1}: "a"}
data, _ = json.Marshal(m3) // ОШИБКА: unsupported type

Только числовые, строковые типы и реализующие encoding.TextMarshaler могут быть ключами.

data := []byte(`{"a":{"b":{"c":1}}}`)
var m map[string]interface{}
json.Unmarshal(data, &m)
// m["a"] — это map[string]interface{}
// m["a"].(map[string]interface{})["b"]...

Каждый уровень — отдельная аллокация. Очень медленно и больно для больших JSON. Используй конкретные структуры или RawMessage.


type S struct {
name string `json:"name"` // strolling case → INVISIBLE
Age int `json:"age"`
}

json.Marshal(S{name: "x", Age: 1}){"age":1}. Никаких ошибок!

Правило: все поля для JSON — с заглавной.

type S struct {
Name string `foo:"bar"` // нет json: → имя поля "Name"
}
s := S{Name: "x"}
data, _ := json.Marshal(s)
// {"Name":"x"} ← НЕ "name"!

Если пишешь json:"-" чтобы пропустить — ОК. Но json:"-," (с запятой) — это поле с именем -!

type S struct {
X int `json:"-,"` // имя поля "-"!
}
data, _ := json.Marshal(S{X: 1})
// {"-":1}
type Address struct {
City string `json:"city"`
}
type User struct {
Name string `json:"name"`
Address Address `json:"address,omitempty"` // НЕ пропустится!
}
u := User{Name: "Alice"} // Address — zero value
data, _ := json.Marshal(u)
// {"name":"Alice","address":{"city":""}}

Решение: использовать указатель:

Address *Address `json:"address,omitempty"`
m := map[int]string{1: "a", 2: "b"}
data, _ := json.Marshal(m)
// {"1":"a","2":"b"} ← числа конвертируются в строки

Это работает с Go 1.7+. До этого — ошибка. Для произвольных типов нужен encoding.TextMarshaler.

data := []byte(`{"x": 12345678901234567890}`)
var s struct{ X int }
err := json.Unmarshal(data, &s)
fmt.Println(err) // json: cannot unmarshal number ... into Go struct field

Если число не помещается в int — ошибка.

Но float → int с потерей молча:

data := []byte(`{"x": 1.5}`)
err := json.Unmarshal(data, &s) // error: ... into Go value of type int
type Stats struct {
Avg float64 `json:"avg"`
}
s := Stats{Avg: math.NaN()}
_, err := json.Marshal(s)
fmt.Println(err) // unsupported value: NaN

Проверяй: math.IsNaN(v), math.IsInf(v, 0). Заменяй на 0 или null.

JavaScript Number — это float64. Точность теряется после 2^53 ≈ 9e15.

{"id": 9007199254740993}

JS прочитает это как 9007199254740992 — последняя цифра потеряна!

Решение: ID-шники как строки:

ID int64 `json:"id,string"` // {"id": "9007199254740993"}
data, _ := json.Marshal(map[string]string{"q": "a < b"})
// {"q":"a < b"}

Если ты пишешь в HTML — это безопасно. В API — ужасно (увеличивает размер, нечитаемо).

Решение: Encoder.SetEscapeHTML(false) или ручная замена.

enc := json.NewEncoder(w)
enc.Encode(v) // добавляет \n в конце!

Если кому-то это мешает (например, парсер не любит) — используй Marshal + ручной Write.

data := []byte(`{"a":1}{"b":2}`) // два JSON
dec := json.NewDecoder(bytes.NewReader(data))
var x map[string]int
dec.Decode(&x) // прочитал только {"a":1}
fmt.Println(x) // map[a:1]
dec.Decode(&x) // следующий объект
fmt.Println(x) // map[b:2]

Это фича для NDJSON. Но json.Unmarshal требует, чтобы input был одним JSON-значением без лишнего.

t := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
data, _ := json.Marshal(t)
fmt.Println(string(data))
// "2026-01-01T00:00:00Z"
  • Если у time.Time нет nanoseconds — Z (UTC) или офсет (+03:00).
  • Если есть наносекунды — 2026-01-01T00:00:00.123456789Z.

Для разных нужд (unix timestamp, custom format) — оборачивай в свой тип.

type User struct {
Name *string `json:"name"`
}
u := User{} // Name == nil
data, _ := json.Marshal(u)
// {"name":null}
// При Unmarshal без значения:
json.Unmarshal([]byte(`{}`), &u)
fmt.Println(u.Name) // nil, не панику!

Используй pointer fields для опциональных значений (различать “нет” vs “пустая строка”).

dec := json.NewDecoder(strings.NewReader(""))
var v interface{}
err := dec.Decode(&v)
fmt.Println(err) // EOF

Учитывай, что пустой input — это EOF, а не валидный JSON.

⚠️ 3.14 Анмаршал в interface{} даёт float64 для чисел

Заголовок раздела «⚠️ 3.14 Анмаршал в interface{} даёт float64 для чисел»

Уже упоминалось, но повторим:

var m map[string]interface{}
json.Unmarshal([]byte(`{"id":42}`), &m)
id := m["id"].(int) // PANIC! interface conversion: interface {} is float64
// Нужно: id := int(m["id"].(float64))

type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func BenchmarkMarshal(b *testing.B) {
u := User{ID: 1, Name: "Alice", Email: "a@b.com"}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
json.Marshal(u)
}
}
BenchmarkMarshal-8 3000000 400 ns/op 96 B/op 2 allocs/op

Не быстро. Но для большинства задач — достаточно.

Для streams — Encoder быстрее (один буфер, не создаёт промежуточный []byte):

// БАГ: лишняя аллокация
data, _ := json.Marshal(v)
w.Write(data)
// OK:
json.NewEncoder(w).Encode(v)
БиблиотекаСкоростьОсобенности
encoding/json1xstdlib, на reflect, везде работает
json-iterator/go2-4xdrop-in replacement, тот же API
easyjson4-6xтребует codegen (easyjson -all file.go)
bytedance/sonic8-10xиспользует assembly, JIT, нужны определённые CPU
goccy/go-json3-5xdrop-in replacement
valyala/fastjson10-20xне-стандартный API, парсер по path

Для джуна: знай о существовании, но не используй без необходимости. stdlib хватает для 95% задач.

// БАГ: bytes.Buffer без капасити
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.Encode(v)
// OK: заранее выделяем
buf := bytes.NewBuffer(make([]byte, 0, 1024))
var encoderPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func marshal(v interface{}) ([]byte, error) {
buf := encoderPool.Get().(*bytes.Buffer)
buf.Reset()
defer encoderPool.Put(buf)
enc := json.NewEncoder(buf)
if err := enc.Encode(v); err != nil {
return nil, err
}
out := make([]byte, buf.Len())
copy(out, buf.Bytes())
return out, nil
}

4.6 Когда переходить на быстрые библиотеки?

Заголовок раздела «4.6 Когда переходить на быстрые библиотеки?»
  • Сериализация — узкое место в профиле (>20% CPU).
  • Тысячи RPS с большими JSON-объектами.
  • Большие batch-обработки.

Раньше не оптимизируй. Сначала измерь.

Окно терминала
go install github.com/mailru/easyjson/...@latest
easyjson -all user.go

Сгенерирует user_easyjson.go с быстрыми MarshalJSON/UnmarshalJSON. Эти методы будут использоваться вместо reflect.


1. Что такое json.Marshal и json.Unmarshal? Marshal — Go-значение в JSON []byte. Unmarshal — JSON []byte в Go-значение (нужен указатель).

2. Почему Unmarshal требует указатель? Чтобы изменять значение по адресу. Без указателя функция работает с копией.

3. Что делает omitempty? Пропускает поле, если оно zero value. Для чисел — 0, строк — "", указателей/слайсов/мап — nil, bool — false.

4. Почему unexported поля не маршалятся? Пакет json использует reflect, который не может читать unexported поля чужих пакетов (ограничение Go).

5. Что такое struct tags? Строковые метаданные на полях, формат key:"value". Пакет json читает json:"..." и применяет.

6. Какие опции в json теге есть? json:"name" — имя, ,omitempty — пропускать zero, ,string — как строку, json:"-" — никогда не сериализовать.

7. Что произойдёт при Unmarshal в interface{}? Числа → float64, строки → string, объекты → map[string]interface{}, массивы → []interface{}, null → nil, bool → bool.

8. Почему числа становятся float64? JSON не различает int и float — это все “number”. Без типовой информации Go использует float64 по умолчанию.

9. Что делать с большими числами?

  • json.Number (через Decoder.UseNumber()).
  • Парсить в конкретный struct с int64.
  • Для клиентов на JS — слать как строку через ,string.

10. Что такое json.RawMessage? Тип []byte, который не парсится при Unmarshal/Marshal. Полезен для отложенного парсинга (discriminated unions).

11. Как сделать custom формат для time.Time? Wrapper-тип с методами MarshalJSON() и UnmarshalJSON().

12. Почему omitempty не работает для struct? Пустой struct не считается empty (zero value есть, но не nil). Используй указатель *Address вместо Address.

13. Чем Encoder отличается от Marshal? Encoder пишет в io.Writer (без промежуточного []byte) и поддерживает streaming. Marshal возвращает []byte.

14. Как сделать strict-режим (запретить unknown fields)?

dec.DisallowUnknownFields()

15. Что делает SetEscapeHTML(false)? Не экранирует <, >, & в < и т.д. Для API-ответов часто отключают.

16. Можно ли иметь циклические ссылки? Нет, Marshal уйдёт в бесконечную рекурсию. Нужно вручную ломать циклы (например, MarshalJSON, который не сериализует обратную ссылку).

17. Как обработать NaN/Inf? По умолчанию — ошибка. Custom Marshaler может вернуть null или другую замену.

18. Что такое embedded struct в JSON? Anonymous fields “вмерживаются” — их поля попадают в верхний уровень JSON.

19. Как ускорить JSON?

  • Использовать json-iterator/go, easyjson, sonic, go-json.
  • sync.Pool для буферов.
  • Прекомпилированные структуры (Codegen).
  • Уменьшить размер JSON (omitempty, короткие имена).

20. Что такое MarshalJSON для интерфейса? Если тип реализует MarshalJSON() ([]byte, error), пакет json вызовет его вместо стандартной сериализации.

21. Map с не-string ключами? С Go 1.7+ числовые ключи конвертируются в строки. Произвольные типы — только через TextMarshaler.

22. Сортируются ли ключи map? Да, с Go 1.12+ Marshal сортирует ключи map для детерминированного вывода.

23. Decoder.More()? Используется в цикле для NDJSON (несколько JSON в одном потоке). Возвращает true, пока есть ещё данные.

24. Как валидировать input?

  • DisallowUnknownFields для strict.
  • Кастомный UnmarshalJSON с проверками.
  • Сторонние библиотеки (go-playground/validator) после Unmarshal.

25. Чем easyjson отличается от sonic?

  • easyjson: codegen, нужен build step, кросс-платформенный.
  • sonic: использует assembly и JIT, работает на amd64/arm64 Linux/macOS, быстрее, но “магический”.

Тебе приходит JSON с полями type и data. В зависимости от type нужно парсить data в разные структуры:

{"type": "user", "data": {"id": 1, "name": "Alice"}}
{"type": "order", "data": {"id": 100, "total": 50.0}}

Напиши парсер через json.RawMessage.

Сделай тип Date (только год-месяц-день) с поддержкой формата 2006-01-02 в JSON. Покрой тестами marshal/unmarshal.

Опиши структуру User со следующими опциональными полями:

  • email — может отсутствовать или быть "".
  • age — может отсутствовать или быть 0.
  • address — целая структура, опциональная.

Сделай так, чтобы JSON правильно различал “не пришло” и “пришло пустое”.

Напиши HTTP handler, который принимает POST JSON. При наличии лишних полей возвращает 400 с описанием. Используй DisallowUnknownFields.

Напиши функцию Process(r io.Reader), которая читает NDJSON построчно и обрабатывает каждый объект. Не грузи весь файл в память.

Сравни encoding/json и json-iterator/go на структуре с 10 полями. Замерь Marshal и Unmarshal, B/op, allocs/op.

type Product struct {
name string `json:"name"`
Price int `json:"price,omitempty"`
}
p := Product{name: "Apple", Price: 0}
data, _ := json.Marshal(p)
fmt.Println(string(data))

Что выведет? Почему?


  1. Документация encoding/jsonhttps://pkg.go.dev/encoding/json
  2. JSON and Go (Go Blog)https://go.dev/blog/json
  3. JSON best practices in Gohttps://www.alexedwards.net/blog/json-tips-and-tricks
  4. easyjsonhttps://github.com/mailru/easyjson
  5. sonichttps://github.com/bytedance/sonic
  6. json-iteratorhttps://github.com/json-iterator/go
  7. 100 Go Mistakes — Teiva Harsanyi (глава про JSON).

Чек-лист джуна:

  • Поля для JSON всегда с заглавной (exported).
  • Знаю про struct tags: json:"name,omitempty".
  • Понимаю, что omitempty не работает для struct (нужен pointer).
  • Использую json.Number для точных чисел.
  • Для больших ID шлю строки (,string или string-тип).
  • Знаю, что числа в interface{} → float64.
  • Использую Encoder для streams и больших объектов.
  • Знаю про DisallowUnknownFields для строгости.
  • Не забываю SetEscapeHTML(false) для API.
  • Custom Marshaler для time.Time в нестандартном формате.