JSON Schema is a declarative language for defining the structure, content, and constraints of JSON documents. It specifies what fields a document must have, what types those fields should be, which values are acceptable, and how nested objects should be structured. By validating documents against a schema at system boundaries—API ingress, configuration loading, data import—you catch structural errors before they propagate into business logic, turning runtime surprises into explicit validation failures.
JSONPath is a query language for extracting values from JSON documents, analogous to XPath for XML. Where JSON Schema answers “is this document valid?”, JSONPath answers “give me these specific values from this document.” Together, they form a complete toolkit for working with JSON data: Schema enforces the contract, and JSONPath navigates the structure to extract what you need.
In a document database system, JSON Schema validation is used to enforce document structures at ingestion time—every document entering the database must conform to its collection’s schema. JSONPath queries power the extraction of nested fields for indexing, filtering, and projection. Sharing compatible schema definitions across implementations ensures that documents validated by one system are readable by another.
A JSON Schema document is itself JSON. It uses keywords like type, properties, required, items, and additionalProperties to describe the expected shape of a target document. Schemas can reference other schemas via $ref, enabling composition: a shared “address” schema can be reused across “user,” “company,” and “order” schemas without duplication.
A JSON Schema is a machine-readable API contract. Unlike documentation that can drift from reality, a schema is enforced at runtime. If a producer sends a document that violates the schema, the consumer rejects it immediately with a specific error pointing to the exact field and constraint that failed. This shifts error detection from “undefined behavior deep in the code” to “rejected at the gate with a clear message.”
JSON Schema provides a rich set of validation constraints: minimum/maximum for numbers, minLength/maxLength for strings, pattern for regex matching, enum for allowed values, format for semantic types (email, date-time, URI), and oneOf/anyOf/allOf for composing complex constraints. These keywords eliminate entire categories of validation code that would otherwise be scattered across application logic.
Instead of writing if typeof field !== "string" || field.length > 255 in every handler, you declare {"type": "string", "maxLength": 255} once in the schema. The validator library handles traversal, type checking, and error reporting. This is more maintainable (change the constraint in one place), more consistent (every consumer validates the same way), and more discoverable (the schema doubles as documentation).
JSONPath expressions select elements from a JSON document using dot notation and bracket syntax. The root element is $, child access is $.field or $['field'], array indexing is $[0], wildcards are $.* or $[*], recursive descent is $.., and filter expressions use $[?(@.price < 10)]. These expressions can extract deeply nested values without writing traversal code.
JSONPath treats a JSON document as a tree and lets you address nodes by path, just as a filesystem path addresses files. $.store.book[0].title navigates from root to the store object, into its book array, selects the first element, and returns its title. This is far more concise and less error-prone than manual traversal: doc["store"]["book"][0]["title"] with nil checks at every level.
Large schemas are composed from smaller, reusable pieces using $ref pointers. A $ref value like "#/$defs/address" references a schema defined in the same document’s $defs section. External references like "https://example.com/schemas/address.json" point to schemas hosted elsewhere. This enables a library of shared type definitions that multiple schemas reference, ensuring consistency across an organization’s API surface.
Schema validation traverses the target document and the schema in parallel, checking each node against its corresponding schema constraints. JSONPath evaluation traverses the document following the path expression, collecting matching nodes. Both operations are tree walks with different goals: validation produces a pass/fail result with error details, while JSONPath produces a set of matched values.
$ref pointers, precompute regex patterns, and build a validator tree./users/0/email) so the caller knows exactly which fields failed and why.// Go: JSON Schema validation using gojsonschema
package main
import (
"fmt"
"github.com/xeipuuv/gojsonschema"
)
func validateDocument(schemaJSON, documentJSON string) error {
schemaLoader := gojsonschema.NewStringLoader(schemaJSON)
documentLoader := gojsonschema.NewStringLoader(documentJSON)
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
if err != nil {
return fmt.Errorf("validation setup failed: %w", err)
}
if !result.Valid() {
for _, desc := range result.Errors() {
fmt.Printf("- %s: %s\n", desc.Field(), desc.Description())
}
return fmt.Errorf("document has %d validation errors", len(result.Errors()))
}
return nil
}
// Example schema for a database document
const documentSchema = `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "content", "metadata"],
"properties": {
"id": {
"type": "string",
"pattern": "^[a-f0-9]{32}$"
},
"content": {
"type": "string",
"minLength": 1,
"maxLength": 1048576
},
"metadata": {
"type": "object",
"required": ["source", "created_at"],
"properties": {
"source": { "type": "string" },
"created_at": { "type": "string", "format": "date-time" },
"tags": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true
}
},
"additionalProperties": true
},
"embeddings": {
"type": "array",
"items": { "type": "number" },
"minItems": 128,
"maxItems": 1536
}
},
"additionalProperties": false
}`
// Go: JSONPath queries for extracting nested values
package main
import (
"encoding/json"
"fmt"
"github.com/PaesslerAG/jsonpath"
)
func queryDocument(docJSON string, path string) (interface{}, error) {
var doc interface{}
if err := json.Unmarshal([]byte(docJSON), &doc); err != nil {
return nil, fmt.Errorf("parse JSON: %w", err)
}
result, err := jsonpath.Get(path, doc)
if err != nil {
return nil, fmt.Errorf("jsonpath %q: %w", path, err)
}
return result, nil
}
func main() {
doc := `{
"store": {
"book": [
{"title": "HNSW Indexing", "price": 29.99, "tags": ["search", "algorithms"]},
{"title": "LSM Trees in Practice", "price": 39.99, "tags": ["database", "storage"]},
{"title": "Raft Consensus", "price": 34.99, "tags": ["distributed", "consensus"]}
]
}
}`
// Get all book titles
titles, _ := queryDocument(doc, "$.store.book[*].title")
fmt.Println("Titles:", titles)
// Get the first book's tags
tags, _ := queryDocument(doc, "$.store.book[0].tags")
fmt.Println("First book tags:", tags)
// Get books with price under 35
cheap, _ := queryDocument(doc, "$.store.book[?(@.price < 35)]")
fmt.Println("Under $35:", cheap)
}
Every API endpoint that accepts JSON should validate the request body against a schema before processing it. This catches malformed requests at the handler boundary, producing clear 400 errors instead of obscure 500 errors deep in the application. In a document database, insert and update endpoints validate the incoming document against the collection’s schema before writing to storage.
Application configuration files (JSON, YAML converted to JSON) are validated at startup against a schema that defines required fields, allowed values, and type constraints. This catches typos ("levle": "debug" instead of "level": "debug") and missing required fields before the application starts serving traffic. The schema itself documents every configuration option and its constraints.
When a document database indexes a document, it uses JSONPath expressions to extract specific fields for different index types. A full-text search index extracts $.content, a faceted filter index extracts $.metadata.tags[*], and a date-range index extracts $.metadata.created_at. These extraction paths are configured per collection, not hardcoded, making the indexing system generic across document structures.
As data models evolve, schemas version alongside them. A v2 schema might add a required field with a default value, or deprecate an old field by making it optional. Validation can be run against multiple schema versions to classify documents and apply appropriate migration logic. This is how a document database handles rolling upgrades without requiring downtime or batch migration of all existing documents.
Parsing and compiling a JSON Schema involves resolving references, compiling regex patterns, and building a validator tree. This is expensive and should be done once at startup, not on every validation call. Cache compiled schemas by their URI or version identifier. In In a document database, each collection’s compiled schema is loaded when the collection is opened and reused for all subsequent document validations.
Some validators can coerce values to the expected type: the string "42" becomes the number 42 if the schema says "type": "number". This is useful for form data or query parameters that are always strings. However, for API contracts and database storage, strict validation (reject type mismatches) is safer because coercion can mask bugs in upstream producers.
Validate JSON documents at system boundaries: API ingress, file loading, message consumption. Once a document passes validation, internal code can trust its structure without redundant checks. This principle reduces defensive programming clutter while maintaining safety. The schema is the gatekeeper; code behind the gate operates on trusted data.
Validation errors should include a JSON Pointer (RFC 6901) to the failing field: /metadata/tags/2 tells the caller exactly which element in which nested structure failed. Without this, error messages like “string expected” force the caller to guess which of dozens of string fields is wrong. Good error reporting is the difference between a five-second fix and a thirty-minute debugging session.
// Zig: Minimal JSON Schema type validation
const std = @import("std");
const json = std.json;
const SchemaType = enum {
string,
number,
integer,
boolean,
array,
object,
null_type,
};
const ValidationError = struct {
path: []const u8,
message: []const u8,
};
fn validateType(
value: json.Value,
expected: SchemaType,
path: []const u8,
errors: *std.ArrayList(ValidationError),
) !void {
const matches = switch (expected) {
.string => value == .string,
.number => value == .float or value == .integer,
.integer => value == .integer,
.boolean => value == .bool,
.array => value == .array,
.object => value == .object,
.null_type => value == .null,
};
if (!matches) {
try errors.append(.{
.path = path,
.message = "type mismatch",
});
}
}
fn validateRequired(
obj: json.ObjectMap,
required_fields: []const []const u8,
path: []const u8,
errors: *std.ArrayList(ValidationError),
) !void {
for (required_fields) |field| {
if (obj.get(field) == null) {
// Build path like "/metadata/source"
var field_path = std.ArrayList(u8).init(std.heap.page_allocator);
try field_path.appendSlice(path);
try field_path.append('/');
try field_path.appendSlice(field);
try errors.append(.{
.path = field_path.items,
.message = "required field missing",
});
}
}
}
additionalProperties: false rejects any field not listed in properties. This is safe for closed APIs but breaks forward compatibility—a producer that adds a new optional field will be rejected by consumers using the old schema. Use it for strict validation at storage time, but be cautious at API boundaries where producers and consumers evolve independently.