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.Formatsregisters string callbacks returning nil on success. Non-string values do not invoke them. No standard format names, includingemail, are enabled implicitly by the reference's default AJV instance. Register each required format.CompileOptions.NumberFormatsregisters numeric callbacks. The default engine suppliesjson.Number, preserving the original numeric token. Defining the same name in both format maps is an error. Built-in engine format names such asregexcan be overridden through the adapter.CompileOptions.ExternalRefsmaps reference resource URLs to schema documents. Use absolute URLs as keys. Relative$refvalues use schema$idresolution. Unregistered references fail compilation; the loader never fetches files or URLs.CompileOptions.ExternalSchemasregisters schema values by$idin 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.Regexconfigures optional match timeout and backtracking stack limits for the default compiler. Matching resource failures returnRegexErrorrather than invalid-payload diagnostics. See regex behavior and limits.Options.QueryOptionsconfigures the existing JMESPath module. The default matches the reference's standard query functions. Passjmespath.WithPowertoolsFunctions()explicitly to enable extended decoding functions.Options.CompilerimplementsCompile(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
$defsconstraints 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
structuredCloneor 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. |