Skip to content

JSON Schema Validation: API and behavior notes

Start with the JSON Schema Validation guide for examples and results. This page covers detailed contracts and less common options. Contributor provenance and verification scope are recorded in compatibility and the linked implementation plans.

Usage

schema, err := validation.Compile(ctx, json.RawMessage(`{
  "type": "object",
  "required": ["id"],
  "properties": {"id": {"type": "string", "minLength": 1}}
}`), validation.Options{Envelope: "body"})
if err != nil {
    return err
}
value, err := schema.Validate(ctx, event)

Validate(ctx, payload, schema, options) compiles for one call. Compile retains a schema for concurrent warm-invocation reuse. Schema documents and format/reference registrations are captured during compilation; changing the original maps afterward does not change the compiled validator. Callbacks must be safe for concurrent use. Successful standalone validation returns the original payload when there is no envelope; JMESPath returns its extracted representation. No defaults, coercion or removal of additional fields are enabled by default.

WrapHandler[I,T,R](inbound, outbound, handler) accepts the raw event type I, validates and converts it to T, invokes the business handler, and validates its successful result R. Input is snapshotted through JSON before validation, including typed events containing slices or pointers. Only inbound validation applies an envelope. Business results with errors and panic identity are preserved. The wrapper reuses the shared invocation context. See the native Lambda example.

As in the reference middleware, a literal boolean false schema passed through Compile is skipped by the wrapper. Standalone validation against that same compiled schema rejects all values. A nil wrapper schema disables that stage. Go's wrapper accepts precompiled schemas; compilation failures are therefore normally handled during initialization, before invocation. It does not emulate the TypeScript decorator's un-cloned input variant.

Validate a Lambda event and response

The maintained example compiles input and output schemas once, then wraps a typed handler. Input {"id":"ORD-123","amount":42} returns "ORD-123"; an invalid input fails before business code runs, and an invalid successful result fails outbound validation.

// Package main validates a typed Lambda event and response using JSON Schema.
package main

import (
    "context"
    jsonv1 "encoding/json"
    "log"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/rambow-cloud/powertools-lambda-go/validation"
)

type order struct {
    ID     string `json:"id"`
    Amount int    `json:"amount"`
}

func main() {
    inbound, err := validation.Compile(context.Background(), jsonv1.RawMessage(`{"type":"object","required":["id","amount"],"properties":{"id":{"type":"string","minLength":1},"amount":{"type":"integer","minimum":1}},"additionalProperties":false}`), validation.Options{})
    if err != nil {
        log.Fatal(err)
    }
    outbound, err := validation.Compile(context.Background(), jsonv1.RawMessage(`{"type":"string","minLength":1}`), validation.Options{})
    if err != nil {
        log.Fatal(err)
    }
    handler := validation.WrapHandler[jsonv1.RawMessage](inbound, outbound, func(_ context.Context, input order) (string, error) { return input.ID, nil })
    lambda.Start(handler)
}

Extension points

  • CompileOptions.Formats registers string callbacks returning nil on success. Non-string values do not invoke them. No standard format names, including email, are enabled implicitly by the reference's default AJV instance. Register each required format.
  • CompileOptions.NumberFormats registers numeric callbacks. The default engine supplies json.Number, preserving the original numeric token. Defining the same name in both format maps is an error. Built-in engine format names such as regex can be overridden through the adapter.
  • CompileOptions.ExternalRefs maps reference resource URLs to schema documents. Use absolute URLs as keys. Relative $ref values use schema $id resolution. Unregistered references fail compilation; the loader never fetches files or URLs.
  • CompileOptions.ExternalSchemas registers schema values by $id in slice order after URL-keyed references. Use absolute document IDs for the verified contract. Nested relative IDs and cross-document pointer aliases retain this registration order; URL-keyed references use lexical URL order. See resource registration for remaining identity/error boundaries.
  • CompileOptions.Regex configures optional match timeout and backtracking stack limits for the default compiler. Matching resource failures return RegexError rather than invalid-payload diagnostics. See regex behavior and limits.
  • Options.QueryOptions configures the existing JMESPath module. The default matches the reference's standard query functions. Pass jmespath.WithPowertoolsFunctions() explicitly to enable extended decoding functions.
  • Options.Compiler implements Compile(context.Context, any, CompileOptions) (Validator, error). A validator returns validation issues separately from operational errors. A nil issue slice means success; a non-nil slice, including an empty one, means rejection. An injected compiler owns its schema dialect, mutation behavior, concurrency and extension semantics.

The default backend is the pure-Go jsonschema v6.0.3 engine. Original documents receive Draft 7 structural validation; a private shared assertion core and compiler vocabulary control Draft 7 conditional reachability. The adapter rejects unknown keywords/formats, widens nullable types without widening enum/const, evaluates reference siblings and splits constraints that the engine would otherwise short-circuit. Source locations are retained for diagnostics. Root Commons and Parser gain no third-party dependencies from this module. See condition graphs for the engine configuration and dialect boundaries.

Optional Draft 2020-12 compiler

Draft202012Compiler is available as an opt-in compiler from v1.2.0. Select it through the existing compiler interface:

schema, err := validation.Compile(ctx, json.RawMessage(`{
    "$schema":"https://json-schema.org/draft/2020-12/schema",
    "type":"array","prefixItems":[{"type":"integer"},{"type":"string"}],"items":false
}`), validation.Options{Compiler: validation.Draft202012Compiler{}})

Here json is encoding/json. [1,"ok"] passes; [1,"ok",true] fails. The executable compiler example shows the complete local call. Omitting Options.Compiler retains Draft 7.

The adapter uses the already pinned jsonschema/v6 v6.0.3 engine, its standard 2020-12 metaschema and this project's ECMAScript Unicode regex adapter. It covers prefixItems, dependency keywords, evaluated properties/items and dynamic references. It rejects unsupported keywords and declarations of another dialect, including in registered documents and schema-valued definitions. Annotations such as default, examples and enum values remain data. No defaults, coercion or property removal are enabled.

Compilation snapshots input and registered documents. ExternalRefs keys must be absolute resource URLs without fragments; ExternalSchemas requires a nonempty $id. Nested relative IDs and named/dynamic anchors are indexed for registered references; duplicate resource identities fail. Unregistered HTTP or file resources cannot load. String/numeric format callbacks retain the existing CompileOptions interface and must be concurrency-safe. Known engine formats and callbacks are asserted; unknown format names follow the engine's annotation behavior. The native reserved regex format cannot be overridden. Content keywords remain annotations. Regex limits apply; regex resource failures remain operational errors. Cancellation is checked before/after synchronous engine work, not an interruption guarantee.

Validation rejection uses SchemaValidationError with independent leaf Issues. InstancePath is an escaped JSON Pointer; SchemaPath is the engine's absolute schema URI plus keyword path. Params.absoluteKeywordLocation repeats that location. Keyword uses the native keyword path, with falseSchema for literal false schemas; Message is the native English diagnostic. Issues are ordered by instance path, schema path, keyword and message, rather than AJV evaluation order. This profile does not promise AJV parameter keys, parent-error trees or messages. Compilation errors still use SchemaCompilationError and preserve their cause.

The 56-case injected-Ajv2020 corpus retains complete Powertools the upstream baseline/AJV v8.20.0 reference results. Fifty-four verdicts agree; two measured native differences are explicit, not normalization:

Input and constraint Pinned Ajv2020 Native compiler
[1,"bad"], integer contains, unevaluatedItems:false Accepts Rejects the unmatched item
1.2, multipleOf:0.1 Rejects through JavaScript division Accepts through exact JSON-number arithmetic

The existing Draft 7 corpus and error-order contract are unchanged. Additional native tests cover ownership, 48 overlapping callers, reentrant formats, cancellation, JSON/regex failures and registered-only loading. Local Handler/Parser, inbound/outbound and envelope composition are covered separately. See the implementation issue.

Optional Draft 2019-09 compiler

Draft201909Compiler is also available from v1.2.0, through Options{Compiler: validation.Draft201909Compiler{}}. Registration, snapshots, native diagnostics, formats, regex limits and cancellation follow the 2020-12 compiler contract. Each adapter selects and validates its own metaschema and vocabulary. Their shared private implementation does not select a dialect automatically or expand default Draft 7.

Use tuple items with additionalItems, rather than 2020-12 prefixItems and the changed items. Recursive extension uses $recursiveAnchor:true and $recursiveRef:"#"; 2020-12 dynamic keywords are rejected. See the executable Draft201909Compiler example.

The 58-case pinned injected-Ajv2019 corpus records 55 agreeing verdicts and three explicit differences. With integer contains and unevaluatedItems:false, Ajv2019 accepts [1,2] and [1,"bad"]; the native 2019-09 engine rejects both because contains does not mark array locations evaluated. The earlier draft left this interaction unspecified; the 2020-12 release notes describe the subsequent change. The third difference is exact fractional multipleOf, as in the 2020-12 native profile. Original reference results and reasons are retained separately; no AJV verdict/message equivalence is promised.

Native acceptance includes nested tuple resource/vocabulary checks, owned schema/payload/diagnostic reuse with 32 overlapping calls, default/dialect separation, recursive-reference fixtures, and local Handler/Parser, envelope, outbound and exact-number cases. The full 2020-12 and default Draft 7 suites remain regression gates after factoring the shared implementation.

Errors and current boundaries

SchemaCompilationError has the reference message Failed to compile schema and unwraps its compilation cause. SchemaValidationError carries Issues with instance path, schema path, keyword, message, params and an optional property name. An empty property name remains distinguishable from an absent field. Wrapper messages distinguish Inbound schema validation failed and Outbound schema validation failed. Query and operational errors remain distinct for standalone calls; wrappers retain operational errors as their cause.

Serialize issues with encoding/json/v2. An empty propertyName remains present, while an absent name is omitted. For the default compiler's oneOf diagnostics, Params.passingSchemas is JSON null when no branch matched and an index array when multiple branches matched; these are explicit domain values.

The initial reference fixture records complete ordered diagnostics for 502 cases; the Go comparison does not sort away mismatches. Covered cases include scalar/object/array constraints, multiple simultaneous failures, nullable/enum behavior, logical combinations, reference siblings, invalid schemas, custom string/numeric formats, external references and extraction. Its generator uses the actual Powertools utility and an AJV instance with the same allErrors: true setting; only AJV's warning logger is disabled during fixture generation. An additional 6,085 regex cases use the default reference utility and cover JavaScript Unicode patterns and all 1,683 supported Unicode property aliases, bringing the total to 6,587.

An additional 1,207 applicator cases bring the Validation corpus to 7,794. Conditionals, dependencies, property names, tuple limits, nested diagnostic ordering and oneOf branch retention now have scoped reference coverage. See applicator diagnostics and object-order semantics. The fixture reader compares every reference field, including fields absent from the Go issue type, without sorting expected errors.

Another 1,271 cases cover mixed-type rule groups, nullable arrays, numeric/string formats, nested $id resources in $defs, relative/absolute references, chains, external schemas and recursion. The combined corpus contains 9,065 cases. Displayed reference paths are scoped separately from canonical engine locations; see type groups and references.

An additional 3,047 strict-pattern cases bring the combined corpus to 12,112. Strict property/pattern overlap uses legacy UTF-16 semantics while payload matching retains Unicode behavior. See strict-schema behavior and remaining setup differences.

The schema-setup corpus adds 658 cases, for 12,770 total. Original document structure is checked independently of reachable compilation. Unknown keywords, unknown formats, nullable restrictions and regex syntax in unused definitions do not fail compilation unless reached, while Draft 7 structural errors remain errors. Definitions reached through pointers, aliases or nested IDs use the same checks. Pattern matchers are skipped only when their assertions are always valid and additional-property rules do not require them. See VALIDATION_STRICT.md for the exact scope and remaining differences.

Another 3,433 keyword cases bring the total to 16,203. Referenced $defs fragments now preserve empty combinator behavior, fractional/negative size limits, non-string required diagnostics and default floating-point multipleOf semantics. The latter intentionally follows AJV's parseInt comparison, including 0.3 failing multipleOf: 0.1 and exponential-notation quotients failing the comparison. See VALIDATION_KEYWORDS.md for examples and remaining numeric boundaries.

The 5,764-case schema-shape corpus brought the total to 21,967. Reachable keyword-type checks remain distinct from original metaschema structure. Scoped primitive fragments, annotation-only schemas, conditional compilation/ignored conditions and mixed property dependencies have reference coverage. Private compilation edges validate unselected constant branches without adding runtime callback calls. See VALIDATION_SHAPES.md; its remaining ignored-reference resolution cases are covered by the subsequent graph milestone below.

The 1,104-case graph corpus brings the total to 23,071. Ignored conditions do not resolve inactive references, active references retain their original conditional targets, and nested dialect declarations remain annotations. Global nested-resource indexing preserves relative scopes and original diagnostics. Ordered ExternalSchemas registrations support the verified cross-document alias replacement behavior; ExternalRefs uses lexical URL order. See VALIDATION_GRAPHS.md for scope and remaining identity/setup errors.

Remaining compatibility work is explicit:

  • JavaScript Unicode regex adaptation now supports lookarounds, backreferences, strict lexical forms and pinned property tables. Exhaustive regex/capture/quantifier and malformed-string parity remains open; see VALIDATION_REGEX.md.
  • Mixed-type grouping, nested IDs and recursive/reference paths have scoped coverage; exhaustive combinations, anchors, duplicate-ID/setup behavior and special JavaScript object names still need further coverage. Go maps use deterministic JSON encoding order; use raw JSON to retain source declaration order. Strict property/pattern overlap now uses a separate legacy matcher; exhaustive legacy capture/native-string behavior remains open.
  • Exhaustive AJV strict-mode diagnostics, schema setup errors, arbitrary fragment/conditional reachability, dialect registration, format object/regular-expression forms, custom keyword plugins and injected-instance options require a complete export/options audit. Go wraps invalid external registration in SchemaCompilationError; the reference can throw an ordinary Error before its compilation catch. Referenced $defs constraints not covered by Draft 7 structural validation still require numeric/keyword edge coverage.
  • JSON numbers retain Go precision. JavaScript floating-point arithmetic, multiple-of rounding, malformed Unicode and non-JSON native values require explicit cross-language boundary tests.
  • Input snapshots are JSON snapshots, not arbitrary JavaScript structuredClone or a reflection-based Go deep clone. Functions, channels and cyclic values fail before the handler. Standalone validation never mutates the caller's payload by default.
  • Context cancellation is checked around compilation/validation, but synchronous engine traversal cannot be interrupted midway. Performance and pathological-schema resource limits are not yet measured.
  • Parser/Event Handler composition, complete middleware/decorator mapping, public release and live AWS acceptance remain separate gates.

JSON snapshots and typed handler conversion use encoding/json/v2. Duplicate members and invalid Unicode are rejected; typed fields match JSON names case-sensitively. Numeric callbacks and snapshots retain exact json.Number tokens. Use explicit application field tags and current JSON collection and omission rules.

Public contract map

TypeScript public entry Go equivalent Remaining boundary
Root validate Validate, reusable Compile and Schema.Validate Complete AJV keyword and diagnostic parity
middleware.validator WrapHandler JSON snapshot instead of arbitrary structured clone; compilation usually occurs before invocation; clone failures currently receive an inbound stage error
decorator.validator Typed handler wrapping An explicit un-cloned decorator-equivalent path remains open
errors.SchemaCompilationError SchemaCompilationError with Unwrap Complete native setup-cause and timing mapping
errors.SchemaValidationError SchemaValidationError with structured Issues and operational Unwrap Complete AJV cause payload and keyword metadata
payload, schema, envelope Function arguments and Options.Envelope Runtime-specific native values and malformed queries
formats Formats, NumberFormats Regex/object/async format forms and custom keyword behavior
externalRefs URL-keyed ExternalRefs, ordered ExternalSchemas Complete duplicate-ID/relative-document-ID/setup errors
ajv Compiler and Validator interfaces Explicit adapters for AJV option families rather than a claim of engine equivalence

This map was inspected against the installed package's export manifest, declarations and implementations. The package exposes four public subpaths: root, middleware, decorator and errors. Its types.d.ts is referenced by the declarations but is not a separately exported package subpath.

CGO is disabled for all development, tests and builds. Functional concurrency tests do not replace a race-detector run; the latter is intentionally excluded by repository policy.

Objects and lifecycle

The finite default boundary audit records actual default AJV behavior, exact diagnostics and explicit native registration/JSON/warning differences. Exhaustive and optional dialect gates remain independent.

Object How to use it
Schema document Describe the accepted payload with JSON Schema.
Compiled schema Use Compile once for repeated calls rather than compiling on each Validate call.
Invocation context Pass cancellation and deadlines into validation and wrappers.
Lambda wrapper Optionally validate both the event and successful response.