Skip to main content
Version: 1.1.2

JSON Schema Generation

Automatically generate JSON Schemas from your Go structs. Pedantigo schemas map validation constraints to standard JSON Schema keywords for API documentation, form generation, LLM structured output, and more.

Quick Start

Get a JSON Schema with a single function call:

package main

import (
"fmt"
"encoding/json"
"github.com/smrutai/pedantigo"
)

type User struct {
Name string `json:"name" pedantigo:"required,min=2,max=50"`
Email string `json:"email" pedantigo:"required,email"`
Age int `json:"age" pedantigo:"min=18,max=120"`
}

func main() {
// Get schema as object
schema := pedantigo.Schema[User]()

// Get schema as JSON bytes
schemaBytes, _ := pedantigo.SchemaJSON[User]()
fmt.Println(string(schemaBytes))
}

Output:

{
"title": "User",
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 2,
"maxLength": 50
},
"email": {
"type": "string",
"format": "email"
},
"age": {
"type": "integer",
"minimum": 18,
"maximum": 120
}
},
"required": ["name", "email", "age"]
}

LLM Tool Calling Compatibility

Pedantigo's default Schema() and SchemaJSON() produce flattened schemas optimized for LLM tool calling APIs.

Schema Output Comparison

Function$ref/$defsBest For
Schema() / SchemaJSON()No - all types expanded inlineLLM tool calling (OpenAI, Anthropic, Gemini)
SchemaOpenAPI() / SchemaJSONOpenAPI()Yes - uses referencesOpenAPI specs, Swagger docs

Example: Flattened Schema Output

type Address struct {
Street string `json:"street" pedantigo:"required"`
City string `json:"city" pedantigo:"required"`
}

type User struct {
Name string `json:"name" pedantigo:"required"`
Address Address `json:"address"`
}

schemaBytes, _ := pedantigo.SchemaJSON[User]()

Output (flattened - ready for LLM tools):

{
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
}
},
"required": ["name"]
}

No $ref or $defs - the schema works directly with OpenAI, Anthropic, and Gemini function calling APIs.

tip

For LLM integrations, see Streaming Validation for handling streaming responses, and LLM Streaming Examples for complete integration patterns.

Simple API Functions

Schema()

Get the JSON Schema as a jsonschema.Schema object.

func Schema[T any]() *jsonschema.Schema

Returns: A *jsonschema.Schema object from the invopop/jsonschema package.

Cached: First call generates schema (~10ms), subsequent calls return cached result (sub-100ns).

Example:

schema := pedantigo.Schema[User]()
fmt.Println("Title:", schema.Title)
fmt.Println("Required fields:", schema.Required)
fmt.Println("Properties:", schema.Properties)

SchemaJSON()

Get the JSON Schema as JSON bytes.

func SchemaJSON[T any]() ([]byte, error)

Returns: JSON bytes, or error if serialization fails.

Use cases:

  • Write schema to file
  • Return schema in HTTP response
  • Send to frontend for form generation
  • Store in database

Example:

schemaBytes, err := pedantigo.SchemaJSON[User]()
if err != nil {
log.Fatal(err)
}

// Write to file
os.WriteFile("user-schema.json", schemaBytes, 0644)

// Return in HTTP response
w.Header().Set("Content-Type", "application/json")
w.Write(schemaBytes)

SchemaOpenAPI()

Get an OpenAPI 3.1 compatible component schema.

func SchemaOpenAPI[T any]() *jsonschema.Schema

Returns: A JSON Schema compatible with OpenAPI 3.1 specifications.

Important: This generates a component schema (for use in components/schemas), not a complete OpenAPI document. Pedantigo is a validation library, not an API framework like Huma. For benchmarks, we only measure component schema generation performance.

Features:

  • Support for nullable fields (pointer types)
  • Compatible with OpenAPI 3.1 specifications (uses $defs syntax)
  • Uses $ref and $defs for better composition

Example:

type APIResponse struct {
Success bool `json:"success" pedantigo:"required"`
Data *User `json:"data"` // nullable
Message string `json:"message,omitempty"`
}

schema := pedantigo.SchemaOpenAPI[APIResponse]()
// Use in OpenAPI spec

SchemaJSONOpenAPI()

Get OpenAPI-compatible schema as JSON bytes.

func SchemaJSONOpenAPI[T any]() ([]byte, error)

Returns: JSON bytes with OpenAPI enhancements.

Example:

schemaBytes, err := pedantigo.SchemaJSONOpenAPI[APIResponse]()
if err != nil {
log.Fatal(err)
}

// Embed in OpenAPI YAML/JSON specification

Performance & Caching

Pedantigo schemas are automatically cached with a 240x speedup:

OperationTimeDetails
First call (any type)~10msIncludes reflection and generation
Cached calls~100ns240x faster
Memory overheadMinimalPer-type cache with type hash validation

How it works:

  1. First call: Type is reflected, constraints parsed, schema generated
  2. Cached calls: Previous result returned instantly
  3. Thread-safe: sync.Map ensures only one schema per type
  4. Invalidation: Type hash detects if struct changes between calls

Benchmark example:

// First call: ~10ms
schema1 := pedantigo.Schema[User]()

// Subsequent calls: <100ns (nearly free)
for i := 0; i < 1000000; i++ {
schema := pedantigo.Schema[User]()
}

// Different types get separate caches
schemaUser := pedantigo.Schema[User]() // ~10ms
schemaProduct := pedantigo.Schema[Product]() // ~10ms
schemaOrder := pedantigo.Schema[Order]() // ~10ms

Constraint Mapping

Pedantigo validation constraints are automatically mapped to JSON Schema keywords. This means your validation rules are instantly documented in the generated schema.

Core Constraints

ConstraintJSON SchemaDescriptionExample
requiredrequired: [...]Field is requiredpedantigo:"required"
minminimum / minLengthNumeric min or string lengthpedantigo:"min=18"
maxmaximum / maxLengthNumeric max or string lengthpedantigo:"max=100"
gtexclusiveMinimumGreater than (numeric)pedantigo:"gt=0"
gteminimumGreater than or equalpedantigo:"gte=1"
ltexclusiveMaximumLess than (numeric)pedantigo:"lt=100"
ltemaximumLess than or equalpedantigo:"lte=99"
lenminLength=X, maxLength=XExact string/array lengthpedantigo:"len=32"
oneof / enumenum: [...]Must be one of valuespedantigo:"oneof=draft published archived"

String Constraints

ConstraintJSON SchemaExample
minLengthminLengthpedantigo:"minLength=3"
maxLengthmaxLengthpedantigo:"maxLength=255"
patternpatternpedantigo:"pattern=^[a-z]+$"
emailformat: "email"pedantigo:"email"
urlformat: "uri"pedantigo:"url"
uuidformat: "uuid"pedantigo:"uuid"
lowercasepattern (enforced)pedantigo:"lowercase"
uppercasepattern (enforced)pedantigo:"uppercase"

Numeric Constraints

ConstraintJSON SchemaExample
positiveexclusiveMinimum: 0pedantigo:"positive"
negativeexclusiveMaximum: 0pedantigo:"negative"
multiple_ofmultipleOfpedantigo:"multiple_of=5"
decimal_placesCustom formatpedantigo:"decimal_places=2"

Format Constraints

All standard formats map to format keyword:

ConstraintFormat ValueExample
email"email"pedantigo:"email"
ipv4"ipv4"pedantigo:"ipv4"
ipv6"ipv6"pedantigo:"ipv6"
hostname"hostname"pedantigo:"hostname"
fqdn"fqdn"pedantigo:"fqdn"
port"port"pedantigo:"port"
jwt"jwt"pedantigo:"jwt"
semver"semver"pedantigo:"semver"

Collection Constraints

ConstraintJSON SchemaExample
minItemsminItemspedantigo:"minItems=1"
maxItemsmaxItemspedantigo:"maxItems=100"
uniqueuniqueItems: truepedantigo:"unique"

Schema Metadata Tags

Control schema generation with metadata tags in the pedantigo struct tag:

Title

Set the schema title for a field:

type User struct {
Name string `json:"name" pedantigo:"required,title=Full Name"`
}

Generated schema:

{
"type": "object",
"properties": {
"name": {
"title": "Full Name",
"type": "string"
}
}
}

Description

Add field description (appears in API docs, forms, etc.):

type User struct {
Email string `json:"email" pedantigo:"required,email,description=User's primary email address"`
Age int `json:"age" pedantigo:"min=0,max=150,description=Age in years (0-150)"`
}

Generated schema:

{
"properties": {
"email": {
"type": "string",
"description": "User's primary email address",
"format": "email"
},
"age": {
"type": "integer",
"description": "Age in years (0-150)",
"minimum": 0,
"maximum": 150
}
}
}

Examples

Provide example values in the schema:

type Product struct {
Name string `json:"name" pedantigo:"required,examples=Laptop|Monitor|Keyboard"`
Price float64 `json:"price" pedantigo:"gt=0,examples=99.99|299.99|1999.99"`
Discount float64 `json:"discount" pedantigo:"min=0,max=100,examples=10|25|50"`
}

Syntax: Pipe-separated values examples=value1|value2|value3

Generated schema:

{
"properties": {
"name": {
"type": "string",
"examples": ["Laptop", "Monitor", "Keyboard"]
},
"price": {
"type": "number",
"examples": [99.99, 299.99, 1999.99],
"exclusiveMinimum": 0
}
}
}

Deprecated

Mark fields as deprecated:

type User struct {
Name string `json:"name" pedantigo:"required"`
OldEmail string `json:"old_email" pedantigo:"email,deprecated"`
// or with message:
LegacyID int `json:"legacy_id" pedantigo:"deprecated=Use 'id' field instead"`
}

Generated schema:

{
"properties": {
"old_email": {
"type": "string",
"deprecated": true,
"description": "This field is deprecated"
},
"legacy_id": {
"type": "integer",
"deprecated": true,
"description": "Use 'id' field instead"
}
}
}

Complete Schema Example

Here's a realistic example showing constraint mapping and metadata:

package main

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

type CreateUserRequest struct {
// Basic fields with constraints
Name string `json:"name" pedantigo:"required,min=2,max=100,title=Full Name,description=User's full name"`

// Email with format constraint
Email string `json:"email" pedantigo:"required,email,description=Primary email address"`

// Numeric with range
Age int `json:"age" pedantigo:"min=18,max=120,description=Age in years"`

// Enum field
Status string `json:"status" pedantigo:"required,oneof=active inactive suspended,description=Account status"`

// Optional field with description
Bio string `json:"bio,omitempty" pedantigo:"maxLength=500,description=User biography"`

// Tags/roles array
Tags []string `json:"tags,omitempty" pedantigo:"maxItems=10,unique,description=User interests and skills"`

// URL field
Website string `json:"website,omitempty" pedantigo:"url,description=User's personal website"`

// Deprecated field
OldUsername string `json:"old_username,omitempty" pedantigo:"deprecated=Use 'name' instead"`
}

func main() {
schema := pedantigo.Schema[CreateUserRequest]()

// Use the schema for API documentation
fmt.Printf("Title: %s\n", schema.Title)
fmt.Printf("Required: %v\n", schema.Required)

// Get as JSON
schemaJSON, _ := pedantigo.SchemaJSON[CreateUserRequest]()
fmt.Println(string(schemaJSON))
}

Generated JSON:

{
"type": "object",
"title": "CreateUserRequest",
"properties": {
"name": {
"type": "string",
"title": "Full Name",
"description": "User's full name",
"minLength": 2,
"maxLength": 100
},
"email": {
"type": "string",
"description": "Primary email address",
"format": "email"
},
"age": {
"type": "integer",
"description": "Age in years",
"minimum": 18,
"maximum": 120
},
"status": {
"type": "string",
"description": "Account status",
"enum": ["active", "inactive", "suspended"]
},
"bio": {
"type": "string",
"description": "User biography",
"maxLength": 500
},
"tags": {
"type": "array",
"description": "User interests and skills",
"items": { "type": "string" },
"maxItems": 10,
"uniqueItems": true
},
"website": {
"type": "string",
"description": "User's personal website",
"format": "uri"
},
"old_username": {
"type": "string",
"deprecated": true,
"description": "Use 'name' instead"
}
},
"required": ["name", "email", "age", "status"]
}

Nested Structs

Nested structs are automatically inlined in the default schema mode:

type Address struct {
Street string `json:"street" pedantigo:"required"`
City string `json:"city" pedantigo:"required"`
Zip string `json:"zip" pedantigo:"required,len=5"`
}

type User struct {
Name string `json:"name" pedantigo:"required"`
Address Address `json:"address" pedantigo:"required"`
}

schema := pedantigo.Schema[User]()

Generated schema (inlined):

{
"type": "object",
"properties": {
"name": { "type": "string" },
"address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"zip": {
"type": "string",
"minLength": 5,
"maxLength": 5
}
},
"required": ["street", "city", "zip"]
}
},
"required": ["name", "address"]
}

For OpenAPI-style composition using $ref and $defs, use SchemaOpenAPI() instead.

Use Cases

API Documentation

Generate OpenAPI/Swagger specs automatically:

type CreatePostRequest struct {
Title string `json:"title" pedantigo:"required,min=5,max=200,description=Post title"`
Content string `json:"content" pedantigo:"required,min=10,max=10000,description=Post content"`
Tags []string `json:"tags" pedantigo:"maxItems=10,unique,description=Post tags"`
}

schema := pedantigo.SchemaJSONOpenAPI[CreatePostRequest]()
// Include in OpenAPI spec components/schemas

Frontend Form Generation

Send schema to frontend for dynamic form building:

// HTTP handler
func handleGetSchema(w http.ResponseWriter, r *http.Request) {
schemaBytes, _ := pedantigo.SchemaJSON[CreatePostRequest]()

w.Header().Set("Content-Type", "application/json")
w.Write(schemaBytes)
}

// Frontend receives schema and generates form fields dynamically

LLM Structured Output

Provide schema to LLMs for structured generation:

type TranslationResult struct {
Original string `json:"original" pedantigo:"required,description=Original text"`
Translated string `json:"translated" pedantigo:"required,description=Translated text"`
Language string `json:"language" pedantigo:"required,oneof=en es fr de,description=Target language code"`
Confidence float64 `json:"confidence" pedantigo:"min=0,max=1,description=Translation confidence 0-1"`
}

schema := pedantigo.SchemaJSON[TranslationResult]()
// Pass to Claude, GPT, etc. for structured generation

Database Schema Generation

Use generated schema to validate before persisting:

// Unmarshal validates against constraints
user, err := pedantigo.Unmarshal[User](jsonData)
if err != nil {
return err // All validation done before database
}

// user is guaranteed valid
db.Insert(user)

Runtime Type Inspection

Query the schema at runtime:

schema := pedantigo.Schema[User]()

// Check which fields are required
requiredFields := schema.Required
fmt.Printf("Required fields: %v\n", requiredFields)

// Get property details
if emailProp, ok := schema.Properties["email"]; ok {
fmt.Printf("Email format: %s\n", emailProp.Format)
}

Performance Characteristics

Schema generation is optimized for speed:

OperationTimeNotes
First schema call~10msReflection + generation + caching
Cached schema call~100ns240x faster
Serializing to JSON~1msEfficient JSON marshaling
Retrieving from cache~100nssync.Map lookup

The 240x speedup means you can safely call Schema[T]() in hot paths after the first call.

Comparison with Validation

Schema describes what is valid (documentation). Validation checks if data is valid (enforcement).

Both use the same constraints, so they're always in sync:

// These use the same constraint definitions:
schema := pedantigo.Schema[User]() // For documentation
user, err := pedantigo.Unmarshal[User](data) // For validation

No manual schema maintenance needed - change a constraint, both schema and validation update automatically.

Limitations & Notes

  • Embedded types: Not supported in constraints (design choice for simplicity)
  • Custom types: Custom constraint types are included in schema but may need custom validator implementation
  • Nullable fields: Use *T (pointer) to make a field optional
  • Schema references: $ref only used in OpenAPI mode (SchemaOpenAPI), default mode inlines

Next Steps