Skip to content

Проверка по схеме

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

Функция ValidateSchema

ValidateSchema сверяет JSON-строку со Schema и возвращает список всех нарушенных ограничений:

go
// Функция уровня пакета
func ValidateSchema(jsonStr string, schema *Schema, cfg ...Config) ([]ValidationError, error)

// Метод Processor
func (p *Processor) ValidateSchema(jsonStr string, schema *Schema, cfg ...Config) ([]ValidationError, error)

Семантика возвращаемых значений:

Возвращаемое значениеЗначение
([]ValidationError{}, nil)JSON корректен и удовлетворяет всем ограничениям
([]ValidationError{...}, nil)JSON разбираем, но есть нарушения ограничений (срез не пуст)
(nil, error)Сбой разбора или предшествующая ошибка (например, некорректный JSON, schema равна nil, превышение лимита)

Ключевое различие

Нарушения ограничений выражаются через возвращаемый срез (error при этом nil); только при сбое разбора, schema равной nil, превышении лимита размера и т. п. возвращается не nil error. Поэтому для проверки «пройдена ли валидация» следует смотреть len(errs) == 0, а не err != nil.

Базовый пример: структура объекта и обязательные поля

go
package main

import (
	"fmt"

	"github.com/cybergodev/json"
)

func main() {
	schema := &json.Schema{
		Type:     "object",
		Required: []string{"name", "email"},
		Properties: map[string]*json.Schema{
			"name":  {Type: "string"},
			"email": {Type: "string", Format: "email"},
			"age":   {Type: "number"},
		},
	}

	// Отсутствует обязательное поле email
	data := `{"name":"Alice","age":30}`

	errs, err := json.ValidateSchema(data, schema)
	if err != nil {
		panic(err)
	}
	for _, e := range errs {
		fmt.Printf("%s: %s\n", e.Path, e.Message)
	}
	// Вывод: email: required property 'email' is missing
}

Обзор полей ограничений Schema

Поля ограничений, поддерживаемые Schema (сгруппированы по категориям):

КатегорияПолеТипПрименимый типОписание
СтруктураTypestringВсеЗначения см. в таблице ниже
СтруктураRequired[]stringobjectСписок обязательных имён свойств
СтруктураPropertiesmap[string]*SchemaobjectПодсхема для каждого свойства
СтруктураItems*SchemaarrayПодсхема для элементов
СтруктураAdditionalPropertiesboolobjecttrue — разрешить дополнительные свойства, false — отклонить
СтрокиMinLength / MaxLengthintstringДиапазон длины (подсчёт по rune)
СтрокиPatternstringstringРегулярное выражение
СтрокиFormatstringstringСемантический формат (см. таблицу значений Format)
ЧислаMinimum / Maximumfloat64numberДиапазон значений
ЧислаExclusiveMinimum / ExclusiveMaximumboolnumberИсключение граничных значений
ЧислаMultipleOffloat64numberДолжно быть кратным этому значению
МассивыMinItems / MaxItemsintarrayДиапазон количества элементов
МассивыUniqueItemsboolarraytrue требует уникальности элементов
ЗначенияEnum[]anyВсеСписок допустимых перечислимых значений
ЗначенияConstanyВсеДолжно быть равно этому фиксированному значению
МетаданныеTitle / DescriptionstringДокументирующие метаданные, в проверке не участвуют
МетаданныеDefaultanyДокументирующие метаданные, в проверке не участвуют
МетаданныеExamples[]anyДокументирующие метаданные, в проверке не участвуют

Поддерживаемые значения Type: object, array, string, number, boolean, null.

Для числовых типов используйте "number"

После разбора JSON все числа (включая целые) являются float64, поэтому для числовых полей следует использовать Type: "number". Значение integer из JSON Schema Draft 7 не поддерживается — указание "integer" приведёт к ошибке expected type integer для всех значений. Числовые ограничения вроде Minimum/Maximum/MultipleOf также действуют только при Type равном number.

Ограничения объекта: Required / Properties / AdditionalProperties

AdditionalProperties управляет тем, разрешено ли появление свойств, не объявленных в Properties. При прямом конструировании Schema через структурный литерал это поле по умолчанию равно false (отклонять дополнительные свойства):

go
package main

import (
	"fmt"

	"github.com/cybergodev/json"
)

func main() {
	schema := &json.Schema{
		Type:     "object",
		Required: []string{"name"},
		Properties: map[string]*json.Schema{
			"name":  {Type: "string"},
			"email": {Type: "string"},
		},
		// AdditionalProperties не задан, по умолчанию для структурного литерала false → отклонять дополнительные свойства
	}

	// "extra" не объявлен в Properties
	data := `{"name":"Alice","extra":"x"}`

	errs, err := json.ValidateSchema(data, schema)
	if err != nil {
		panic(err)
	}
	for _, e := range errs {
		fmt.Printf("%s: %s\n", e.Path, e.Message)
	}
	// Вывод: extra: additional property 'extra' is not allowed
}

Разрешение дополнительных свойств

Чтобы разрешить дополнительные свойства, установите AdditionalProperties в true или используйте DefaultSchema() для конструирования (её AdditionalProperties по умолчанию true).

Строковые ограничения: MinLength / MaxLength / Pattern / Format

Ограничения MinLength, MaxLength, Minimum, Maximum, MinItems, MaxItems и т. д. действуют только при создании через NewSchemaWithConfig (причина см. в разделе Способ создания). Ниже с помощью указательных полей SchemaConfig задаются длины, а через Pattern ограничение — только строчные буквы:

go
package main

import (
	"fmt"

	"github.com/cybergodev/json"
)

func main() {
	nameCfg := json.DefaultSchemaConfig()
	nameCfg.Type = "string"
	minLen, maxLen := 3, 10
	nameCfg.MinLength = &minLen
	nameCfg.MaxLength = &maxLen
	nameCfg.Pattern = `^[a-z]+$`
	nameSchema := json.NewSchemaWithConfig(nameCfg)

	schema := &json.Schema{
		Type:     "object",
		Required: []string{"name"},
		Properties: map[string]*json.Schema{
			"name": nameSchema,
		},
	}

	// "AB": недостаточная длина и содержит заглавные буквы
	data := `{"name":"AB"}`

	errs, err := json.ValidateSchema(data, schema)
	if err != nil {
		panic(err)
	}
	for _, e := range errs {
		fmt.Printf("%s: %s\n", e.Path, e.Message)
	}
	// Вывод:
	// name: string length 2 is less than minimum 3
	// name: string 'AB' does not match pattern '^[a-z]+$'
}

Pattern лениво компилируется при первой проверке и кэшируется; один и тот же *Schema можно безопасно использовать для параллельных проверок. Если само регулярное выражение некорректно, ошибка компиляции будет сообщаться при каждой проверке.

Числовые ограничения: Minimum / Maximum / MultipleOf

go
package main

import (
	"fmt"

	"github.com/cybergodev/json"
)

func main() {
	ageCfg := json.DefaultSchemaConfig()
	ageCfg.Type = "number"
	minVal, maxVal := 0.0, 120.0
	ageCfg.Minimum = &minVal
	ageCfg.Maximum = &maxVal
	mult := 5.0
	ageCfg.MultipleOf = &mult
	ageSchema := json.NewSchemaWithConfig(ageCfg)

	schema := &json.Schema{
		Type: "object",
		Properties: map[string]*json.Schema{
			"age": ageSchema,
		},
	}

	// 148: превышает верхний предел 120 и не кратно 5
	data := `{"age":148}`

	errs, err := json.ValidateSchema(data, schema)
	if err != nil {
		panic(err)
	}
	for _, e := range errs {
		fmt.Printf("%s: %s\n", e.Path, e.Message)
	}
	// Вывод:
	// age: number 148 exceeds maximum 120
	// age: number 148 is not a multiple of 5
}

ExclusiveMinimum / ExclusiveMaximum задаются совместно с Minimum / Maximum через SchemaConfig (также указательные поля) для исключения самих граничных значений. MultipleOf использует сравнение с плавающей точкой и допуском (epsilon 1e-9), поэтому сценарии точности IEEE 754 вроде 0.1 + 0.2 не дают ложных срабатываний.

Ограничения массива: Items / MinItems / MaxItems / UniqueItems

go
package main

import (
	"fmt"

	"github.com/cybergodev/json"
)

func main() {
	tagsCfg := json.DefaultSchemaConfig()
	tagsCfg.Type = "array"
	minItems, maxItems := 1, 3
	tagsCfg.MinItems = &minItems
	tagsCfg.MaxItems = &maxItems
	tagsCfg.UniqueItems = true
	tagsCfg.Items = &json.Schema{Type: "string"}
	tagsSchema := json.NewSchemaWithConfig(tagsCfg)

	schema := &json.Schema{
		Type: "object",
		Properties: map[string]*json.Schema{
			"tags": tagsSchema,
		},
	}

	// 4 элемента (превышение верхнего предела 3) и "a" повторяется
	data := `{"tags":["a","a","b","c"]}`

	errs, err := json.ValidateSchema(data, schema)
	if err != nil {
		panic(err)
	}
	for _, e := range errs {
		fmt.Printf("%s: %s\n", e.Path, e.Message)
	}
	// Вывод:
	// tags: array length 4 exceeds maximum 3
	// tags[1]: duplicate item found: a
}

Items указывает подсхему, которой должен удовлетворять каждый элемент (в примере выше — строка); UniqueItems определяет дубликаты по комбинации «динамический тип + значение» — [1, "1"] считаются двумя разными элементами, ошибку дают только действительно повторяющиеся значения.

Защита от глубокой рекурсии

Schema — рекурсивный тип; при проверке действует верхний предел глубины рекурсии (DefaultMaxNestingDepth = 200). Самоссылающиеся схемы (например, s.Items = s) не приводят к переполнению стека — при превышении предела выдаётся ошибка schema nesting exceeds maximum depth.

Перечисление и константа: Enum / Const

Enum ограничивает значение одним из списка; Const требует равенства фиксированному значению. Оба действуют через прямое сравнение, не требуют NewSchemaWithConfig:

go
package main

import (
	"fmt"

	"github.com/cybergodev/json"
)

func main() {
	schema := &json.Schema{
		Type: "object",
		Properties: map[string]*json.Schema{
			"role":   {Enum: []any{"admin", "user", "guest"}},
			"status": {Const: "active"},
		},
	}

	// role не входит в перечисление; status соответствует константе
	data := `{"role":"superuser","status":"active"}`

	errs, err := json.ValidateSchema(data, schema)
	if err != nil {
		panic(err)
	}
	for _, e := range errs {
		fmt.Printf("%s: %s\n", e.Path, e.Message)
	}
	// Вывод: role: value 'superuser' is not in allowed enum values: [admin user guest]
}

Поддерживаемые значения Format

Семантические форматы, поддерживаемые полем Format (неизвестные форматы молча пропускаются: без ошибки и без самой проверки):

FormatПравило проверки
emailПроверка локальной части, домена, структуры и длины TLD
dateYYYY-MM-DD
date-timeRFC3339
timeHH:MM:SS
uriДолжен содержать ://
uuidСоответствие регулярному выражению UUID
ipv44 сегмента, каждый 0–255
ipv6Проходит net.ParseIP и содержит :

Тип ValidationError

Каждое нарушение ограничения — это ValidationError, содержащий путь в JSON и описание ошибки:

go
type ValidationError struct {
	Path    string `json:"path"`    // Путь ошибки (например, "user.email", "tags[1]")
	Message string `json:"message"` // Сообщение об ошибке
}

func (ve *ValidationError) Error() string

Поскольку ValidateSchema возвращает срез []ValidationError, можно напрямую обходить его, читая Path / Message; метод Error() используется для форматирования одной ошибки в строку (например, для логирования).

Способ создания Schema

Конструирование Schema возможно тремя способами; ключевое отличие — действуют ли ограничения длины/диапазона:

go
// 1) Прямой литерал: Type/Required/Properties/Items/Pattern/Format/Enum/Const/
// UniqueItems/MultipleOf действуют сразу; но MinLength/MaxLength/Minimum/Maximum/
// MinItems/MaxItems/ExclusiveMinimum/ExclusiveMaximum не действуют (см. описание ниже)
schema := &json.Schema{Type: "string", Pattern: `^\d+$`}

// 2) NewSchemaWithConfig: ограничения задаются через указательные поля SchemaConfig, все ограничения длины/диапазона действуют
cfg := json.DefaultSchemaConfig()
cfg.Type = "string"
minLen := 1
cfg.MinLength = &minLen
schema := json.NewSchemaWithConfig(cfg)

// 3) DefaultSchema: возвращает Schema со значениями по умолчанию (AdditionalProperties равно true)
schema := json.DefaultSchema()

Ограничения длины/диапазона требуют NewSchemaWithConfig

Группа ограничений MinLength, MaxLength, Minimum, Maximum, MinItems, MaxItems, ExclusiveMinimum, ExclusiveMaximum полагается на внутренний флаг отслеживания Schema, не задаваемый извне. Прямое присвоение этих полей в литерале &json.Schema{...} не действует; их необходимо включать через NewSchemaWithConfig с передачей соответствующих указательных полей (например, cfg.MinLength = &v). Type, Required, Properties, Items, Pattern, Format, Enum, Const, UniqueItems, MultipleOf не подпадают под это ограничение и действуют как в литерале, так и через NewSchemaWithConfig.

DefaultSchema

Сигнатура: func DefaultSchema() *Schema

DefaultSchema возвращает Schema со значениями по умолчанию: Properties инициализирована пустой map, Required — пустым срезом, AdditionalProperties равна true (дополнительные свойства разрешены); подходит как отправная точка для постепенного заполнения.

DefaultSchemaConfig

Сигнатура: func DefaultSchemaConfig() SchemaConfig

DefaultSchemaConfig возвращает вход по умолчанию для NewSchemaWithConfig: только AdditionalProperties предустановлена указателем на true, остальные поля — нулевые значения; задав в ней Type и указательные поля, можно сразу создавать Schema.

Результаты обоих согласуются: DefaultSchema() эквивалентна NewSchemaWithConfig(DefaultSchemaConfig()) — по умолчанию обе разрешают дополнительные свойства.

Поля SchemaConfig

Набор полей SchemaConfig один к одному соответствует Schema; при этом числовые/булевы ограничения являются указательными типамиnil означает, что ограничение не задано, и только передача не-nil указателя заставляет NewSchemaWithConfig включить соответствующее ограничение (именно поэтому ограничения длины/диапазона должны задаваться через NewSchemaWithConfig, см. предупреждение выше).

ПолеТипОписание
TypestringJSON-тип (то же, что Schema.Type)
Propertiesmap[string]*SchemaПодсхема для каждого свойства (при nil инициализируется пустой map)
Items*SchemaПодсхема для элементов массива
Required[]stringСписок обязательных имён свойств (при nil инициализируется пустым срезом)
MinLength*intМинимальная длина (nil = не задано)
MaxLength*intМаксимальная длина (nil = не задано)
Minimum*float64Минимальное значение (nil = не задано)
Maximum*float64Максимальное значение (nil = не задано)
PatternstringРегулярное выражение
FormatstringСемантический формат
AdditionalProperties*boolРазрешены ли дополнительные свойства (nil трактуется как true; в DefaultSchemaConfig предустановлен указатель на true)
MinItems*intМинимальное количество элементов (nil = не задано)
MaxItems*intМаксимальное количество элементов (nil = не задано)
UniqueItemsboolТребовать уникальности элементов
Enum[]anyСписок допустимых перечислимых значений
ConstanyФиксированное значение, которому должно равняться значение
MultipleOf*float64Ограничение кратности (nil = не задано)
ExclusiveMinimum*boolИсключить нижнюю границу (nil = не задано)
ExclusiveMaximum*boolИсключить верхнюю границу (nil = не задано)
TitlestringЗаголовок (метаданные)
DescriptionstringОписание (метаданные)
DefaultanyЗначение по умолчанию (метаданные)
Examples[]anyПримеры значений (метаданные)

Рекомендуется всегда создавать настроенную Schema через NewSchemaWithConfig (func NewSchemaWithConfig(cfg SchemaConfig) *Schema) — это единственный надёжный способ включить указательные ограничения; кроме того, он автоматически инициализирует Properties / Required и обрабатывает значение AdditionalProperties по умолчанию.

Поля Config, связанные с проверкой

ПолеТипОписание
EnableValidationboolВключает проверку ввода (влияет на проверку безопасности/структуры перед операцией)
ValidateInputboolПроверяет входной JSON
SkipValidationboolПропускает несущественные проверки (только для доверенного ввода)

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

Config.CustomValidators ([]Validator) и интерфейс Validator в текущей версии объявлены и участвуют в клонировании конфигурации и вычислении ключа кэша, но ещё не подключены к конвейеру операций. Регистрация валидаторов через Config.CustomValidators (или Config.AddValidator) не влияет на выполнение любой операции — операция не будет отклонена пользовательским валидатором. Интерфейс Validator в настоящее время является зарезервированным:

go
// Текущая версия: объявлен, но не подключён — регистрация не влияет на операции (зарезервированный интерфейс)
type Validator interface {
	Validate(jsonStr string) error
}

Для пользовательской проверки до и после операций используйте действующие перехватчики Hooks (например, ValidationHook).

См. также