Skip to main content
Version: 1.1.0

Validator API

The Validator API provides advanced configuration and performance optimization for validation workflows. This is the underlying API used by the Simple API and offers fine-grained control for specialized use cases.

Simple API Recommended

For most applications, use the Simple API instead. It provides automatic caching and requires less code.

Overview

The Validator API creates reusable validator instances with custom configuration. Each validator is specific to a struct type and applies the same rules consistently across requests.

When to Use Validator API

  • Need custom validation options (StrictMissingFields, ExtraForbid)
  • Reusing the same validator across many requests
  • Performance-critical paths (avoid global cache lookup)
  • Implementing custom validator registration
  • Building discriminated unions (advanced feature)

Creating a Validator

Default Options

import "github.com/SmrutAI/pedantigo"

type User struct {
Email string `pedantigo:"required,email"`
Age int `pedantigo:"required,min=18"`
}

// Create validator with default options
validator := pedantigo.New[User]()

Default options:

  • StrictMissingFields: true - Missing required fields are errors
  • ExtraFields: ExtraIgnore - Unknown JSON fields are silently ignored

Custom Options

import "github.com/SmrutAI/pedantigo"

// Create with custom options
validator := pedantigo.New[User](pedantigo.ValidatorOptions{
StrictMissingFields: false, // Allow missing fields (use pointers for optional)
ExtraFields: pedantigo.ExtraForbid, // Reject unknown fields
})

ValidatorOptions

type ValidatorOptions struct {
// StrictMissingFields controls whether missing fields without defaults cause errors
// Default: true (missing fields are errors)
// Set to false if using pointers for optional fields
StrictMissingFields bool

// ExtraFields controls how unknown JSON fields are handled during Unmarshal
// Options:
// - ExtraIgnore (default): Unknown fields are silently ignored
// - ExtraForbid: Unknown fields cause validation errors
// - ExtraAllow: Reserved for future use
ExtraFields ExtraFieldsMode
}

Validator Methods

Unmarshal

Unmarshal JSON data and validate it in a single operation.

// Unmarshal JSON and validate
user, err := validator.Unmarshal([]byte(`{"email": "test@example.com", "age": 25}`))
if err != nil {
// Handle validation error
fmt.Printf("Validation failed: %v\n", err)
}

// Access validated data
fmt.Printf("Email: %s, Age: %d\n", user.Email, user.Age)

Behavior:

  • Parses JSON and validates fields according to struct tags
  • Applies defaults and defaultFactory functions
  • Returns error slice on validation failure
  • Reuses field deserializers for efficiency

Validate

Validate an existing struct instance.

user := &User{
Email: "invalid-email",
Age: 15,
}

err := validator.Validate(user)
if err != nil {
// Handle validation error
fmt.Printf("Validation failed: %v\n", err)
}

Use Case: Post-construction validation or programmatic struct creation.

NewModel

Create a validated instance from various input types.

// From JSON bytes
user, err := validator.NewModel([]byte(`{"email": "test@example.com", "age": 25}`))

// From map (kwargs pattern)
user, err := validator.NewModel(map[string]any{
"email": "test@example.com",
"age": 25,
})

// From existing struct (validates it)
existing := User{Email: "test@example.com", Age: 25}
user, err := validator.NewModel(existing)

// From pointer
existing := &User{Email: "test@example.com", Age: 25}
user, err := validator.NewModel(existing)

Accepts:

  • []byte - JSON data
  • map[string]any - Field values
  • T - Struct value
  • *T - Struct pointer

Schema Methods

Schema

Get the JSON Schema as a Go object.

schema := validator.Schema()
// schema is *jsonschema.Schema

// Access schema properties
fmt.Printf("Title: %s\n", schema.Title)
fmt.Printf("Type: %s\n", schema.Type)

Performance: First call generates schema (~10ms). Subsequent calls return from cache (under 100ns).

SchemaJSON

Get the JSON Schema as JSON bytes.

schemaBytes, err := validator.SchemaJSON()
if err != nil {
// Handle error
}

// Use for API documentation, storage, etc.
fmt.Println(string(schemaBytes))

SchemaOpenAPI

Get OpenAPI 3.1 compatible component schema.

schema := validator.SchemaOpenAPI()
// Returns component schema with $defs (OpenAPI 3.1 / JSON Schema Draft 2020-12)

SchemaJSONOpenAPI

Get OpenAPI 3.1 compatible component schema as JSON bytes.

schemaBytes, err := validator.SchemaJSONOpenAPI()
if err != nil {
// Handle error
}
// Embed in OpenAPI 3.1 spec under components/schemas

Marshal Methods

Validate and convert struct to JSON.

user := &User{
Email: "test@example.com",
Age: 25,
}

// With default options
jsonData, err := validator.Marshal(user)
if err != nil {
// Handle validation or marshal error
}

// With custom options (context-based field exclusion)
opts := pedantigo.ForContext("api") // Excludes fields marked with exclude:api
jsonData, err := validator.MarshalWithOptions(user, opts)

Behavior:

  • Validates struct before marshaling
  • Supports context-based field inclusion/exclusion
  • Supports omitzero tag for sparse output

Dict

Convert struct to map.

user := &User{
Email: "test@example.com",
Age: 25,
}

dict, err := validator.Dict(user)
if err != nil {
// Handle error
}

// Access as map
fmt.Printf("Email: %s\n", dict["email"])
fmt.Printf("Age: %d\n", dict["age"])

Use Case: API responses, dynamic field access, data transformation.

Usage Patterns

Reusing Validators

Create once, use many times for best performance:

// At initialization
validator := pedantigo.New[User]()

// In request handler
func handleUserCreation(w http.ResponseWriter, r *http.Request) {
var data []byte
// ... read request body ...

user, err := validator.Unmarshal(data)
if err != nil {
// Handle error
}
// ... process user ...
}

// Reuse same validator for all requests
func handleUserUpdate(w http.ResponseWriter, r *http.Request) {
var data []byte
// ... read request body ...

user, err := validator.Unmarshal(data)
if err != nil {
// Handle error
}
// ... process user ...
}

Multiple Validators

Use different validators with different configurations:

// Strict validation for admin operations
adminValidator := pedantigo.New[User](pedantigo.ValidatorOptions{
StrictMissingFields: true,
ExtraFields: pedantigo.ExtraForbid,
})

// Lenient validation for imports
importValidator := pedantigo.New[User](pedantigo.ValidatorOptions{
StrictMissingFields: false,
ExtraFields: pedantigo.ExtraIgnore,
})

// Use as appropriate
adminUser, err := adminValidator.Unmarshal(data)
importedUser, err := importValidator.Unmarshal(data)

Schema Caching

Validators cache schemas internally per type:

// First call - generates schema (~10ms)
schema1 := validator.Schema()

// Subsequent calls - from cache (<100ns)
schema2 := validator.Schema()

// Same cached schema is returned
fmt.Println(schema1 == schema2) // true

Performance Considerations

Validator Creation Cost

Creating a validator is moderately expensive (~1-2ms):

  • Parses all struct tags
  • Builds field deserializer closures
  • Validates defaultFactory functions
  • Sets up cross-field validation

Best Practice: Create once at initialization, reuse for multiple operations.

Reuse vs. Cache Lookup

Comparison for high-throughput paths:

Validator instance reuse:       ~500ns per unmarshal
Simple API (cache lookup): ~700ns per unmarshal (includes lock acquisition)

For performance-critical code, maintain a validator instance.

Memory Considerations

Each validator maintains:

  • Field deserializer closures
  • Schema cache (lazy initialized)
  • Cross-field constraint cache

Memory overhead is minimal (~10-50KB per validator instance).

Comparison with Simple API

FeatureValidator APISimple API
SetupExplicit instanceAutomatic caching
ConfigurationCustom optionsDefault only
PerformanceFastest (no cache lookup)Fast (global cache)
ReusabilityManual managementAutomatic
Use CaseHigh-throughput, custom configGeneral purpose
Code Examplevalidator.Unmarshal(data)pedantigo.Unmarshal[User](data)

When to Switch to Simple API

If any of these apply, use the Simple API instead:

  • Configuration not needed (using defaults)
  • Multiple different types validated
  • Simplicity over micro-optimization
  • General-purpose application code

When to Use Validator API

If any of these apply, use the Validator API:

  • High-throughput request handler
  • Custom validator options needed
  • Validator reused across many requests
  • Building a framework or library
  • Discriminated unions or advanced features

Error Handling

Both methods return errors on validation failure:

user, err := validator.Unmarshal(data)
if err != nil {
// err is a validation error
fmt.Printf("Validation failed: %v\n", err)
}

// Check specific validation errors
errs := user, err // err is a FieldError slice
for _, fieldErr := range errs {
fmt.Printf("Field %s: %s\n", fieldErr.Field, fieldErr.Message)
}

See Errors for detailed error handling.

Advanced Topics

Custom Validator Registration

Register custom validation functions per validator instance:

validator := pedantigo.New[User]()
// Custom validators can be registered at validator creation
// See ValidatorOptions for details

Discriminated Unions

For complex validation scenarios with union types:

// Create union validator (advanced feature)
validator := pedantigo.NewUnion[T](opts...)

Refer to advanced examples for union validation patterns.

Complete Example

package main

import (
"fmt"
"github.com/SmrutAI/pedantigo"
)

type User struct {
Email string `pedantigo:"required,email"`
Age int `pedantigo:"required,min=18,max=120"`
Username string `pedantigo:"required,min=3,max=50"`
}

func main() {
// Create validator with custom options
validator := pedantigo.New[User](pedantigo.ValidatorOptions{
StrictMissingFields: true,
ExtraFields: pedantigo.ExtraForbid,
})

// Example 1: Unmarshal JSON
jsonData := []byte(`{
"email": "alice@example.com",
"age": 28,
"username": "alice_wonder"
}`)

user, err := validator.Unmarshal(jsonData)
if err != nil {
fmt.Printf("Validation error: %v\n", err)
return
}
fmt.Printf("Valid user: %+v\n", user)

// Example 2: Validate existing struct
invalidUser := &User{
Email: "not-an-email",
Age: 17,
Username: "ab",
}

err = validator.Validate(invalidUser)
if err != nil {
fmt.Printf("Validation error: %v\n", err)
}

// Example 3: Get schema
schema := validator.Schema()
fmt.Printf("Schema: %+v\n", schema)

// Example 4: Marshal to JSON
validUser := &User{
Email: "bob@example.com",
Age: 30,
Username: "bob_builder",
}

jsonOutput, err := validator.Marshal(validUser)
if err != nil {
fmt.Printf("Marshal error: %v\n", err)
return
}
fmt.Printf("JSON output: %s\n", string(jsonOutput))
}

See Also