Skip to content

Пользовательское кодирование

Библиотека json поддерживает совместимость кодирования со стандартной библиотекой encoding/json, поэтому JSON-форма пользовательских типов определяется в основном через реализацию интерфейсов стандартной библиотеки. Эта страница описывает точки расширения кодирования, фактически действующие в текущей версии:

  • json.Marshaler — тип сам определяет своё JSON-кодирование
  • encoding.TextMarshaler — тип сам определяет своё текстовое кодирование (вывод как JSON-строка)
  • time.Time — встроенный в библиотеку формат времени RFC3339Nano
  • Config.CustomEscapes — пользовательское сопоставление экранирования символов

Приоритет интерфейсов

Для задачи «как кодировать некий тип» предпочтительно реализовать MarshalJSON или MarshalText; такие реализации совместимы с данной библиотекой, стандартной библиотекой encoding/json и любыми совместимыми библиотеками — максимальная переносимость.

Интерфейс json.Marshaler

Типы, реализующие MarshalJSON() ([]byte, error), могут полностью определить своё JSON-представление. При кодировании библиотека приоритетно вызывает этот метод (поддерживаются как приёмник-значение, так и приёмник-указатель), в соответствии с поведением стандартной библиотеки encoding/json.

Сигнатура интерфейса (совместима с encoding/json.Marshaler):

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

Ниже определяется тип Hex, кодирующий uint64 как шестнадцатеричную строку с префиксом 0x:

go
package main

import (
	"fmt"
	"strconv"

	"github.com/cybergodev/json"
)

// Hex оборачивает uint64 как шестнадцатеричное представление.
type Hex uint64

// MarshalJSON реализует json.Marshaler, кодируя число как строку "0x..".
func (h Hex) MarshalJSON() ([]byte, error) {
	return []byte(`"0x` + strconv.FormatUint(uint64(h), 16) + `"`), nil
}

func main() {
	type Device struct {
		ID    Hex    `json:"id"`
		Label string `json:"label"`
	}
	d := Device{ID: Hex(255), Label: "sensor-1"}

	out, err := json.Marshal(d)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(out))
	// Вывод: {"id":"0xff","label":"sensor-1"}
}

Избегайте бесконечной рекурсии

Если внутри MarshalJSON нужен «обычный кодировщик» в качестве помощника, используйте стандартную библиотеку stdjson.Marshal или вызывайте библиотечный Marshal для **другого конкретного типа». Прямой повторный вызов Marshal для этого же типа снова войдёт в MarshalJSON, образуя бесконечную рекурсию.

Интерфейс encoding.TextMarshaler

Типы, не реализующие MarshalJSON, но реализующие MarshalText() ([]byte, error), кодируются как JSON-строка, значением которой является текстовое содержимое (кавычки и экранирование добавляются автоматически). Подходит для типов, форма которых полностью выражается текстом.

Сигнатура интерфейса (совместима с encoding.TextMarshaler):

go
type TextMarshaler interface {
    MarshalText() ([]byte, error)
}

Ниже определяется тип Slug, автоматически нормализуемый при кодировании к нижнему регистру с дефисами:

go
package main

import (
	"fmt"
	"strings"

	"github.com/cybergodev/json"
)

// Slug представляет URL-дружелюбный короткий текст.
type Slug string

// MarshalText реализует encoding.TextMarshaler, выводит нормализованный текст.
func (s Slug) MarshalText() ([]byte, error) {
	return []byte(strings.ToLower(strings.ReplaceAll(string(s), " ", "-"))), nil
}

func main() {
	type Article struct {
		Title string `json:"title"`
		Slug  Slug   `json:"slug"`
	}
	a := Article{Title: "Hello World", Slug: Slug("Hello World")}

	out, err := json.Marshal(a)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(out))
	// Вывод: {"title":"Hello World","slug":"hello-world"}
}

Приоритет двух интерфейсов

Если тип реализует оба интерфейса одновременно, MarshalJSON имеет приоритет над MarshalText. Если нужно закодировать тип как JSON-строку, реализация MarshalText обычно лаконичнее (не нужно самостоятельно обрабатывать кавычки и экранирование).

Встроенная обработка time.Time

Библиотека имеет встроенную обработку time.Time с единым выводом в формате RFC3339Nano (с сохранением субсекундной точности, как в стандартной библиотеке encoding/json). Не требует никаких настроек:

go
package main

import (
	"fmt"
	"time"

	"github.com/cybergodev/json"
)

func main() {
	type Event struct {
		Name string    `json:"name"`
		At   time.Time `json:"at"`
	}
	t := time.Date(2026, 1, 15, 10, 30, 0, 0, time.UTC)
	e := Event{Name: "deploy", At: t}

	out, err := json.Marshal(e)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(out))
	// Вывод: {"name":"deploy","at":"2026-01-15T10:30:00Z"}
}

Если нужен другой формат времени, реализуйте MarshalJSON для этого типа (см. выше), чтобы переопределить встроенное поведение — MarshalJSON пользовательского типа всегда имеет приоритет над обработкой time.Time по умолчанию.

Пользовательское экранирование символов CustomEscapes

Config.CustomEscapes — это map[rune]string для глобального переопределения экранирования некоторых символов. При кодировании строк библиотека сначала ищет rune в этом сопоставлении: при совпадении соответствующая строка записывается в вывод как есть (вы сами несёте ответственность за её JSON-корректность), при несовпадении применяется экранирование по умолчанию.

Ниже знак авторского права © заменяется ASCII-текстом (при совпадении записывается как есть, остальные символы идут через обработку по умолчанию):

go
package main

import (
	"fmt"

	"github.com/cybergodev/json"
)

func main() {
	cfg := json.DefaultConfig()
	// © по умолчанию выводится как есть; здесь заменяется ASCII-текстом
	cfg.CustomEscapes = map[rune]string{
		'©': "(c)",
	}

	out, err := json.EncodeWithConfig(map[string]string{"note": "Copyright © 2026"}, cfg)
	if err != nil {
		panic(err)
	}
	fmt.Println(out)
	// Вывод: {"note":"Copyright (c) 2026"}
}

Пользовательская escape-строка должна быть JSON-корректной

Значения CustomEscapes записываются в вывод как есть, без повторной обработки, поэтому будьте внимательны к экранированию строк в самом исходном коде Go: если в выводе нужна буквальная последовательность экранирования с обратной косой чертой, в исходном коде Go следует писать двойную косую черту \\ (одинарная косая черта будет интерпретирована Go как escape-последовательность, и в результате получится сам этот символ, а не escape-последовательность).

Когда активируется путь пользовательского экранирования

Установка CustomEscapes (не nil) активирует путь пользовательского кодирования. Этот путь также считывает поля EscapeHTML, EscapeUnicode, EscapeSlash, EscapeNewlines, EscapeTabs, SortKeys, FloatPrecision, IncludeNulls (подробнее см. Параметры конфигурации).

Как выбрать точку расширения

ПотребностьСпособ
Пользовательская JSON-форма типаРеализовать MarshalJSON()
Кодирование типа как JSON-строка (текстовое представление)Реализовать MarshalText()
Глобальное изменение правил экранирования некоторых символовConfig.CustomEscapes
Управление отступами, HTML-экранированием, Unicode-экранированием, сортировкой ключей, точностью чисел и т. д.Поля Config: Pretty/EscapeHTML/EscapeUnicode/SortKeys/FloatPrecision и др. (см. Параметры конфигурации)
Переопределение формата времени по умолчанию для time.TimeРеализовать MarshalJSON() для пользовательского типа времени

Поля Config, связанные с кодированием

ПолеТипОписание
CustomEscapesmap[rune]stringПользовательское сопоставление экранирования символов (при совпадении выводится как есть)
EscapeHTMLboolЭкранировать ли < > & (по умолчанию true)
EscapeUnicodeboolЭкранировать ли символы >0x7F как \uXXXX
EscapeSlashboolЭкранировать ли /
EscapeNewlines / EscapeTabsboolЭкранировать ли переводы строк / табуляции
SortKeysboolСортировать ли ключи объектов (ключи объектов сортируются по умолчанию)
FloatPrecisionintТочность чисел с плавающей точкой (-1 по умолчанию)
IncludeNullsboolВключать ли поля со значениями null

Не подключённые поля расширения (зарезервировано)

Не подключённые поля расширения

Config.CustomEncoder (интерфейс CustomEncoder) и Config.CustomTypeEncoders (интерфейс TypeEncoder) в текущей версии объявлены и участвуют в клонировании конфигурации и вычислении ключа кэша, но ещё не подключены к конвейеру кодирования. Установка этих двух полей не изменяет вывод кодирования. Они являются зарезервированными точками расширения для будущих версий; до этого используйте описанные выше действующие механизмы MarshalJSON/MarshalText/CustomEscapes.

go
// Текущая версия: следующие два поля объявлены, но не подключены — установка не даёт эффекта (зарезервированные интерфейсы)
type CustomEncoder interface {
    Encode(value any) (string, error)
}

type TypeEncoder interface {
    Encode(v reflect.Value) (string, error)
}

См. также