Skip to content

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:

go
// 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 valueMeaning
([]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

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"},
		},
	}

	// 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):

CategoryFieldTypeApplies toDescription
StructureTypestringAllValues listed below
StructureRequired[]stringobjectList of property names that must be present
StructurePropertiesmap[string]*SchemaobjectSub-schema for each property
StructureItems*SchemaarraySub-schema for the elements
StructureAdditionalPropertiesboolobjecttrue allows extra properties, false rejects them
StringMinLength / MaxLengthintstringLength range (counted by rune)
StringPatternstringstringRegular expression
StringFormatstringstringSemantic format (see the Format value table)
NumberMinimum / Maximumfloat64numberValue range
NumberExclusiveMinimum / ExclusiveMaximumboolnumberExcludes the boundary value
NumberMultipleOffloat64numberMust be a multiple of this value
ArrayMinItems / MaxItemsintarrayElement count range
ArrayUniqueItemsboolarraytrue requires elements to be unique
ValueEnum[]anyAllList of allowed enum values
ValueConstanyAllMust equal this fixed value
MetadataTitle / DescriptionstringDocumentation metadata; not validated
MetadataDefaultanyDocumentation metadata; not validated
MetadataExamples[]anyDocumentation 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):

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 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:

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": 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

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: 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

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 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:

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 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):

FormatValidation rule
emailValidates local part, domain, TLD structure, and lengths
dateYYYY-MM-DD
date-timeRFC3339
timeHH:MM:SS
uriMust contain ://
uuidUUID regex match
ipv44 segments, each 0–255
ipv6Parses via net.ParseIP and contains :

The ValidationError Type

Each constraint violation is a ValidationError carrying the offending JSON path and a description:

go
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() string

Since 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:

go
// 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 typesnil 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).

FieldTypeDescription
TypestringJSON type (same as Schema.Type)
Propertiesmap[string]*SchemaSub-schema for each property (initialized to an empty map when nil)
Items*SchemaSub-schema for array elements
Required[]stringList of property names that must be present (initialized to an empty slice when nil)
MinLength*intMinimum length (nil = unset)
MaxLength*intMaximum length (nil = unset)
Minimum*float64Minimum value (nil = unset)
Maximum*float64Maximum value (nil = unset)
PatternstringRegular expression
FormatstringSemantic format
AdditionalProperties*boolWhether extra properties are allowed (nil treated as true; DefaultSchemaConfig presets a pointer to true)
MinItems*intMinimum element count (nil = unset)
MaxItems*intMaximum element count (nil = unset)
UniqueItemsboolRequires elements to be unique
Enum[]anyList of allowed enum values
ConstanyFixed value that must be matched
MultipleOf*float64Multiple-of constraint (nil = unset)
ExclusiveMinimum*boolExcludes the lower boundary (nil = unset)
ExclusiveMaximum*boolExcludes the upper boundary (nil = unset)
TitlestringTitle (metadata)
DescriptionstringDescription (metadata)
DefaultanyDefault value (metadata)
Examples[]anyExample 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.

FieldTypeDescription
EnableValidationboolEnables input validation (affects pre-operation security/structure checks)
ValidateInputboolValidates the input JSON
SkipValidationboolSkips 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:

go
// 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) Validator interface and Schema-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)