Schema Validation
The json library provides data validation based on JSON Schema: define a Schema describing the structure and constraints your data must satisfy, then validate a piece of JSON with ValidateSchema. This is the feature-complete validation system in the current version.
The ValidateSchema Function
ValidateSchema validates a JSON string against a Schema and returns the list of all constraint violations:
// Package-level function
func ValidateSchema(jsonStr string, schema *Schema, cfg ...Config) ([]ValidationError, error)
// Processor method
func (p *Processor) ValidateSchema(jsonStr string, schema *Schema, cfg ...Config) ([]ValidationError, error)Return value semantics:
| Return value | Meaning |
|---|---|
([]ValidationError{}, nil) | The JSON is valid and satisfies every constraint |
([]ValidationError{...}, nil) | The JSON parses, but constraint violations exist (non-empty slice) |
(nil, error) | Parsing or a precondition failed (e.g. invalid JSON, nil schema, limit exceeded) |
The key distinction
Constraint violations are expressed via the returned slice (error stays nil); only parse failures, a nil schema, exceeding size limits, and the like produce a non-nil error. So "did validation pass" is len(errs) == 0, not err != nil.
Basic Example: Object Structure and Required Fields
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"},
},
}
// The required field email is missing
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)
}
// Output: email: required property 'email' is missing
}Schema Constraint Fields Overview
Constraint fields supported by Schema (grouped by category):
| Category | Field | Type | Applies to | Description |
|---|---|---|---|---|
| Structure | Type | string | All | Values listed below |
| Structure | Required | []string | object | List of property names that must be present |
| Structure | Properties | map[string]*Schema | object | Sub-schema for each property |
| Structure | Items | *Schema | array | Sub-schema for the elements |
| Structure | AdditionalProperties | bool | object | true allows extra properties, false rejects them |
| String | MinLength / MaxLength | int | string | Length range (counted by rune) |
| String | Pattern | string | string | Regular expression |
| String | Format | string | string | Semantic format (see the Format value table) |
| Number | Minimum / Maximum | float64 | number | Value range |
| Number | ExclusiveMinimum / ExclusiveMaximum | bool | number | Excludes the boundary value |
| Number | MultipleOf | float64 | number | Must be a multiple of this value |
| Array | MinItems / MaxItems | int | array | Element count range |
| Array | UniqueItems | bool | array | true requires elements to be unique |
| Value | Enum | []any | All | List of allowed enum values |
| Value | Const | any | All | Must equal this fixed value |
| Metadata | Title / Description | string | — | Documentation metadata; not validated |
| Metadata | Default | any | — | Documentation metadata; not validated |
| Metadata | Examples | []any | — | Documentation metadata; not validated |
Supported Type values: object, array, string, number, boolean, null.
Use "number" for numeric types
After JSON parsing, every number (integers included) is a float64, so numeric fields should use Type: "number". The JSON Schema Draft 7 integer value is not supported — writing "integer" causes every value to fail with an expected type integer error. Numeric constraints such as Minimum/Maximum/MultipleOf also take effect only when Type is number.
Object Constraints: Required / Properties / AdditionalProperties
AdditionalProperties controls whether properties not declared in Properties may appear. When constructing a Schema directly with a struct literal, the field defaults to false (extra properties rejected):
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 unset; struct literal defaults to false -> extra properties rejected
}
// "extra" is not declared in 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)
}
// Output: extra: additional property 'extra' is not allowed
}Allowing extra properties
To allow extra properties, set AdditionalProperties to true, or construct with DefaultSchema() (whose default AdditionalProperties is true).
String Constraints: MinLength / MaxLength / Pattern / Format
Constraints such as MinLength, MaxLength, Minimum, Maximum, MinItems, MaxItems take effect only when created via NewSchemaWithConfig (see Creating a Schema for why). The example below sets lengths via SchemaConfig pointer fields and restricts to lowercase letters with Pattern:
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": too short and contains uppercase letters
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)
}
// Output:
// name: string length 2 is less than minimum 3
// name: string 'AB' does not match pattern '^[a-z]+$'
}Pattern is compiled lazily on first validation and cached, so the same *Schema is safe for concurrent validation. If the regex itself is invalid, every validation reports that compile error.
Numeric Constraints: Minimum / Maximum / MultipleOf
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: exceeds the 120 cap and is not a multiple of 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)
}
// Output:
// age: number 148 exceeds maximum 120
// age: number 148 is not a multiple of 5
}ExclusiveMinimum / ExclusiveMaximum must be set together with Minimum / Maximum via SchemaConfig (also pointer fields) to exclude the boundary value itself. MultipleOf compares with a float tolerance (epsilon 1e-9), so IEEE 754 precision cases like 0.1 + 0.2 produce no false positives.
Array Constraints: Items / MinItems / MaxItems / UniqueItems
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 elements (exceeds the cap of 3), and "a" is duplicated
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)
}
// Output:
// tags: array length 4 exceeds maximum 3
// tags[1]: duplicate item found: a
}Items specifies the sub-schema every element must satisfy (a string in the example above); UniqueItems judges duplicates by the combination of "dynamic type + value" — [1, "1"] counts as two distinct elements, and only genuinely duplicated values are reported.
Recursion depth protection
Schema is a recursive type; validation enforces a recursion-depth cap (DefaultMaxNestingDepth = 200). Self-referencing schemas (e.g. s.Items = s) do not cause stack overflow — exceeding the cap yields a schema nesting exceeds maximum depth error.
Enum and Const: Enum / Const
Enum restricts the value to one of a set; Const requires equality with a fixed value. Both work by direct comparison and need no NewSchemaWithConfig:
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 is not in the enum; status matches the constant
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)
}
// Output: role: value 'superuser' is not in allowed enum values: [admin user guest]
}Supported Format Values
Semantic formats supported by the Format field (unknown formats are silently skipped: no error, no check):
| Format | Validation rule |
|---|---|
email | Validates local part, domain, TLD structure, and lengths |
date | YYYY-MM-DD |
date-time | RFC3339 |
time | HH:MM:SS |
uri | Must contain :// |
uuid | UUID regex match |
ipv4 | 4 segments, each 0–255 |
ipv6 | Parses via net.ParseIP and contains : |
The ValidationError Type
Each constraint violation is a ValidationError carrying the offending JSON path and a description:
type ValidationError struct {
Path string `json:"path"` // Error path (e.g. "user.email", "tags[1]")
Message string `json:"message"` // Error message
}
func (ve *ValidationError) Error() stringSince ValidateSchema returns a []ValidationError slice, simply iterate and read Path / Message; the Error() method formats a single error as a string (e.g. for logging).
Creating a Schema
There are three ways to construct a Schema; the key difference is whether length/range constraints take effect:
// 1) Direct literal: Type/Required/Properties/Items/Pattern/Format/Enum/Const/
// UniqueItems/MultipleOf work immediately; but MinLength/MaxLength/Minimum/Maximum/
// MinItems/MaxItems/ExclusiveMinimum/ExclusiveMaximum do not (see note below)
schema := &json.Schema{Type: "string", Pattern: `^\d+$`}
// 2) NewSchemaWithConfig: set constraints via SchemaConfig pointer fields; all
// length/range constraints work
cfg := json.DefaultSchemaConfig()
cfg.Type = "string"
minLen := 1
cfg.MinLength = &minLen
schema := json.NewSchemaWithConfig(cfg)
// 3) DefaultSchema: returns a Schema with default values (AdditionalProperties true)
schema := json.DefaultSchema()Length/range constraints require NewSchemaWithConfig
MinLength, MaxLength, Minimum, Maximum, MinItems, MaxItems, ExclusiveMinimum, and ExclusiveMaximum rely on tracking flags inside Schema that cannot be set externally. Assigning these fields directly in an &json.Schema{...} literal has no effect; they are enabled only through NewSchemaWithConfig with the corresponding pointer fields (e.g. cfg.MinLength = &v). Type, Required, Properties, Items, Pattern, Format, Enum, Const, UniqueItems, and MultipleOf are not subject to this restriction and work both in literals and via NewSchemaWithConfig.
DefaultSchema
Signature: func DefaultSchema() *Schema
DefaultSchema returns a Schema with default values: Properties initialized to an empty map, Required to an empty slice, and AdditionalProperties to true (extra properties allowed) — a good starting point to fill in incrementally.
DefaultSchemaConfig
Signature: func DefaultSchemaConfig() SchemaConfig
DefaultSchemaConfig returns the default input for NewSchemaWithConfig: only AdditionalProperties is preset to a pointer to true; every other field is the zero value. Set Type and the pointer fields on it, then create the Schema.
The two produce consistent results: DefaultSchema() equals NewSchemaWithConfig(DefaultSchemaConfig()) — both allow extra properties by default.
SchemaConfig Fields
The fields of SchemaConfig correspond one-to-one with Schema; the numeric/boolean constraints are pointer types — nil means the constraint is unset, and NewSchemaWithConfig enables a constraint only when a non-nil pointer is passed (this is exactly why length/range constraints must go through NewSchemaWithConfig; see the warning above).
| Field | Type | Description |
|---|---|---|
Type | string | JSON type (same as Schema.Type) |
Properties | map[string]*Schema | Sub-schema for each property (initialized to an empty map when nil) |
Items | *Schema | Sub-schema for array elements |
Required | []string | List of property names that must be present (initialized to an empty slice when nil) |
MinLength | *int | Minimum length (nil = unset) |
MaxLength | *int | Maximum length (nil = unset) |
Minimum | *float64 | Minimum value (nil = unset) |
Maximum | *float64 | Maximum value (nil = unset) |
Pattern | string | Regular expression |
Format | string | Semantic format |
AdditionalProperties | *bool | Whether extra properties are allowed (nil treated as true; DefaultSchemaConfig presets a pointer to true) |
MinItems | *int | Minimum element count (nil = unset) |
MaxItems | *int | Maximum element count (nil = unset) |
UniqueItems | bool | Requires elements to be unique |
Enum | []any | List of allowed enum values |
Const | any | Fixed value that must be matched |
MultipleOf | *float64 | Multiple-of constraint (nil = unset) |
ExclusiveMinimum | *bool | Excludes the lower boundary (nil = unset) |
ExclusiveMaximum | *bool | Excludes the upper boundary (nil = unset) |
Title | string | Title (metadata) |
Description | string | Description (metadata) |
Default | any | Default value (metadata) |
Examples | []any | Example values (metadata) |
Creating configured schemas via NewSchemaWithConfig (func NewSchemaWithConfig(cfg SchemaConfig) *Schema) is always recommended — it is the only reliable way to enable pointer constraints, and it initializes Properties / Required automatically and handles the AdditionalProperties default.
Validation-Related Config Fields
| Field | Type | Description |
|---|---|---|
EnableValidation | bool | Enables input validation (affects pre-operation security/structure checks) |
ValidateInput | bool | Validates the input JSON |
SkipValidation | bool | Skips non-essential validation (trusted input only) |
Extension fields not yet wired
Config.CustomValidators ([]Validator) and the Validator interface are declared and take part in config cloning and cache-key computation, but are not yet wired into the operation pipeline in the current version. Registering validators via Config.CustomValidators (or Config.AddValidator) does not affect the execution of any operation — operations are never rejected by custom validators. The Validator interface is currently reserved:
// Current version: declared but not wired; registering has no effect (reserved interface)
type Validator interface {
Validate(jsonStr string) error
}For custom validation before/after operations, use the effective Hooks (e.g. ValidationHook).
See Also
- Interface Definitions - The (reserved)
Validatorinterface andSchema-related types - Type Definitions - Core types (Config / Schema / Stats / AccessResult)
- Parse & Validate - Parse / Valid / ValidateSchema functions
- Config - Validation-related configuration fields
- Hooks - The effective pre/post interception mechanism (incl.
ValidationHook)