validation

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package validation validates events and responses against JSON Schemas, optionally after unwrapping the payload of an event source with a JMESPath envelope: the Go side of Powertools for AWS Lambda (Python)'s validation utility.

It is schema-document validation. To decode events into typed Go models with a Validate method of their own, see the parser package; the two combine.

Validating in the handler

Validate checks an event against a schema and returns the validated data, with the schema defaults filled in. A *SchemaValidationError says what is wrong, in the words of Powertools Python:

var orderSchema = validation.MustCompile(orderSchemaJSON)

func handle(ctx context.Context, event jsontext.Value) (events.APIGatewayProxyResponse, error) {
	if _, err := validation.Validate(event, orderSchema, validation.WithEnvelope(validation.APIGatewayREST)); err != nil {
		return events.APIGatewayProxyResponse{StatusCode: 400, Body: err.Error()}, nil
	}
	...
}

ValidateAs validates and decodes the result into a Go type.

Validating with a decorator

Validator wraps a handler: it validates the event before the handler runs and the response after it, as Python's @validator does. The returned handler takes the raw event; the wrapped one receives the payload the envelope selects, defaults included, decoded into its own event type:

lambda.Start(validation.Validator(handle,
	validation.Inbound(orderSchema, validation.WithEnvelope(validation.EventBridge)),
	validation.Outbound(responseSchema),
))

Schemas

A schema is raw JSON ([]byte, jsontext.Value, or a string such as one read with go:embed), a value that marshals to JSON (a map[string]any), or a *Schema from Compile. Compile once, at package level or in init: Validate also accepts schema documents and caches what it compiles, except when the options carry functions (custom formats, loaders, a regexp engine), which Go cannot compare; compile those schemas with Compile.

The "$schema" keyword picks the draft: 4, 6, 7, 2019-09 and 2020-12 are supported. A schema without it is read as DefaultDraft, Draft 7, which is what Powertools Python applies (fastjsonschema's fallback generator is labelled 2019-09 but implements draft-07). WithDefaultDraft changes it.

Formats are asserted for every draft, as in Python; WithFormatAssertion turns them off. WithFormat and WithFormatRegexp add custom formats, which apply to strings only.

References

A "$ref" to a part of the same schema always resolves. Other documents come from WithResource, which preloads a document under a URL, or from a loader for the scheme of their URL: WithLoader (Python's handlers, with a context) or WithHTTPLoader. Nothing is fetched otherwise: a reference without a document or a loader fails the compilation with ErrNoLoader. Give a schema that references documents by relative URLs an "$id", the base they resolve against.

Envelopes

WithEnvelope takes a JMESPath expression, which can call the Powertools functions powertools_json, powertools_base64 and powertools_base64_gzip (see the jmespath package), and the custom functions given to it, next to the Powertools ones (Python's jmespath_options replace them). The constants of this package are Python's built-in envelopes. A failing envelope is a *jmespath.EnvelopeError, matched by errors.Is(err, jmespath.ErrInvalidEnvelope).

Errors

  • *SchemaValidationError (errors.Is ErrSchemaValidation): the data breaks the schema. Its fields are those of Python's exception: Message, ValidationMessage, Name, Path, Value, Definition, Rule and RuleDefinition. It unwraps to the engine's *jsonschema.ValidationError, whose BasicOutput lists every failure.
  • *InvalidSchemaFormatError (errors.Is ErrInvalidSchema): the schema does not compile.
  • ErrInvalidEvent: the event is not JSON.

Environment variables

None. POWERTOOLS_DEBUG shows what the package does in the debug log.

Differences from Powertools Python

  • Schemas compile once and are reused across invocations; Python compiles on every call. A schema error surfaces when Compile runs, or, for Validator, on the invocations until the schema compiles.
  • No remote "$ref" is fetched unless a loader is set; fastjsonschema fetches http and https URLs by default.
  • Validate returns the data with defaults as a new value; it never changes the caller's data in place. The decorator hands the defaults to the handler through its decoded event, and fills those of the outbound schema into the response, as Python's in-place update does.
  • The decorator's handler receives the payload decoded into its own type, and the returned handler takes the raw event (jsontext.Value).
  • Patterns are Go regular expressions (RE2): no lookarounds or backreferences unless WithRegexpEngine plugs in another engine. A format pattern is anchored at its start only, like Python's re.match.
  • A schema of 2019-09 or 2020-12 is validated by that draft's rules; fastjsonschema applies draft-07 rules to them.
  • Every envelope failure is a *jmespath.EnvelopeError; Python wraps only lexer, type and Unicode errors.
  • When several values break the schema, Python reports the first in its keyword order; Go reports the first by location in the data, then in the schema, which may name a different one of the same failures.
  • A tuple default is not appended after a missing position that has no default (fastjsonschema shifts it to the wrong index).
  • Numbers keep their precision: the validated data holds json.Number values. A number selected by an envelope goes through JMESPath as a float64.

Index

Examples

Constants

View Source
const (
	APIGatewayREST            = jmespath.APIGatewayREST
	APIGatewayHTTP            = jmespath.APIGatewayHTTP
	SQS                       = jmespath.SQS
	SNS                       = jmespath.SNS
	EventBridge               = jmespath.EventBridge
	CloudWatchEventsScheduled = jmespath.CloudWatchEventsSchedule
	KinesisDataStream         = jmespath.KinesisDataStream
	CloudWatchLogs            = jmespath.CloudWatchLogs
)

The built-in envelopes of Powertools Python's validation.envelopes: the JMESPath expressions that select the payload of the common event sources, for WithEnvelope. SQS, Kinesis and CloudWatch Logs select a list, so their schema describes an array of payloads; SNS selects the message of the first record only, as in Python.

View Source
const DefaultDraft = Draft7

DefaultDraft is the draft of schemas that do not declare "$schema". Powertools Python's engine, fastjsonschema, labels its fallback generator 2019-09, but that generator implements the draft-07 keywords (plus the uuid and duration formats, which every draft accepts here): Draft 7 is what Python actually applies.

View Source
const MaxRemoteSchemaSize = 8 << 20

MaxRemoteSchemaSize bounds the documents the HTTP loader reads, so that a misbehaving server cannot exhaust the function's memory.

Variables

View Source
var (
	// ErrSchemaValidation is matched by every *SchemaValidationError.
	ErrSchemaValidation = errors.New("failed schema validation")
	// ErrInvalidSchema is matched by every *InvalidSchemaFormatError.
	ErrInvalidSchema = errors.New("invalid schema")
	// ErrInvalidEvent reports data that is not JSON, or does not marshal to
	// it.
	ErrInvalidEvent = errors.New("event is not valid JSON")
	// ErrNoLoader reports a "$ref" to a remote document whose URL scheme
	// has no loader (see WithLoader and WithHTTPLoader).
	ErrNoLoader = errors.New("no loader for the URL scheme")
	// ErrFetch reports a remote document the HTTP loader could not fetch.
	ErrFetch = errors.New("fetch remote schema")
)

Sentinel errors, which the error types of this package match with errors.Is.

Functions

func Validate

func Validate(event, schema any, opts ...Option) (any, error)

Validate checks event against schema and returns the validated data: Powertools Python's validate.

The event is raw JSON ([]byte, jsontext.Value) or a value that marshals to JSON (an aws-lambda-go event, a map[string]any); a Go string is a JSON string. The schema is a *Schema from Compile, or a schema document, which is compiled on first use and cached. WithEnvelope selects the part of the event to validate.

The returned data is the selected part of the event as a decoded JSON value, with the defaults of the schema filled in (see WithDefaults). Numbers are json.Number values of encoding/json, so that none loses precision; ValidateAs decodes the data into a Go type instead.

A value that breaks the schema is a *SchemaValidationError, a schema that cannot be compiled an *InvalidSchemaFormatError, an envelope that cannot be applied a *jmespath.EnvelopeError.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/alessandrolattao/powertools-lambda-go/validation"
)

// userSchema is compiled once, when the function initializes.
var userSchema = validation.MustCompile(`{
	"type": "object",
	"required": ["user_id", "project"],
	"properties": {
		"user_id": {"type": "string", "format": "uuid"},
		"project": {"type": "string", "enum": ["powertools", "other"]},
		"ip": {"type": "string", "format": "ipv4"},
		"role": {"type": "string", "default": "reader"}
	}
}`)

func main() {
	event := []byte(`{"user_id":"0d44b083-8206-4a3a-aa95-5d392a99be4a","project":"lambda"}`)

	_, err := validation.Validate(event, userSchema)
	if verr, ok := errors.AsType[*validation.SchemaValidationError](err); ok {
		fmt.Println(verr.Rule, verr.Path)
		fmt.Println(verr)
	}
}
Output:
enum [data project]
Failed schema validation. Error: data.project must be one of ['powertools', 'other'], Path: ['data', 'project'], Data: lambda
Example (Envelope)
package main

import (
	"encoding/json/jsontext"
	"fmt"

	"github.com/aws/aws-lambda-go/events"

	"github.com/alessandrolattao/powertools-lambda-go/validation"
)

// userSchema is compiled once, when the function initializes.
var userSchema = validation.MustCompile(`{
	"type": "object",
	"required": ["user_id", "project"],
	"properties": {
		"user_id": {"type": "string", "format": "uuid"},
		"project": {"type": "string", "enum": ["powertools", "other"]},
		"ip": {"type": "string", "format": "ipv4"},
		"role": {"type": "string", "default": "reader"}
	}
}`)

func main() {
	event := events.EventBridgeEvent{
		DetailType: "UserCreated",
		Source:     "my.app",
		Detail:     jsontext.Value(`{"user_id":"0d44b083-8206-4a3a-aa95-5d392a99be4a","project":"powertools"}`),
	}

	detail, err := validation.Validate(event, userSchema, validation.WithEnvelope(validation.EventBridge))
	if err != nil {
		panic(err)
	}
	fields, ok := detail.(map[string]any)
	if !ok {
		panic("not an object")
	}
	fmt.Println(fields["role"])
}
Output:
reader

func ValidateAs

func ValidateAs[T any](event, schema any, opts ...Option) (T, error)

ValidateAs is Validate followed by decoding the validated data, defaults included, into a T.

Example
package main

import (
	"fmt"

	"github.com/alessandrolattao/powertools-lambda-go/validation"
)

// userSchema is compiled once, when the function initializes.
var userSchema = validation.MustCompile(`{
	"type": "object",
	"required": ["user_id", "project"],
	"properties": {
		"user_id": {"type": "string", "format": "uuid"},
		"project": {"type": "string", "enum": ["powertools", "other"]},
		"ip": {"type": "string", "format": "ipv4"},
		"role": {"type": "string", "default": "reader"}
	}
}`)

func main() {
	type user struct {
		UserID  string `json:"user_id"`
		Project string `json:"project"`
		Role    string `json:"role"`
	}

	u, err := validation.ValidateAs[user](
		[]byte(`{"user_id":"0d44b083-8206-4a3a-aa95-5d392a99be4a","project":"powertools"}`), userSchema)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", u)
}
Output:
{UserID:0d44b083-8206-4a3a-aa95-5d392a99be4a Project:powertools Role:reader}

func Validator

func Validator[E, R any](handler middleware.Handler[E, R], opts ...ValidatorOption) middleware.Handler[jsontext.Value, R]

Validator validates the events and the responses of handler against JSON Schemas: Powertools Python's @validator decorator.

The returned handler takes the raw event. It unwraps the event with the envelope of Inbound, validates it against the inbound schema, fills in the schema defaults, and decodes the result into the handler's event type E: the handler receives the payload, as in Python. E can be a struct, a map[string]any, a slice for envelopes that select a list (SQS, Kinesis, CloudWatch Logs), or jsontext.Value. After the handler returns, the response is validated against the outbound schema.

Schemas given as documents are compiled once, when Validator is called; a schema that fails to compile, for example because a remote reference is unreachable, is compiled again on the next invocation, which fails with the *InvalidSchemaFormatError until it compiles.

Example
package main

import (
	"context"
	"encoding/json/jsontext"
	"fmt"

	"github.com/alessandrolattao/powertools-lambda-go/validation"
)

func main() {
	type order struct {
		ID    string  `json:"id"`
		Total float64 `json:"total"`
	}
	type receipt struct {
		OrderID string `json:"orderId"`
		Status  string `json:"status"`
	}

	handle := func(_ context.Context, o order) (receipt, error) {
		return receipt{OrderID: o.ID, Status: "accepted"}, nil
	}

	// lambda.Start(h) in a function; called directly here.
	h := validation.Validator(handle,
		validation.Inbound(`{"type":"object","required":["id","total"],"properties":{"total":{"minimum":0}}}`,
			validation.WithEnvelope(validation.APIGatewayREST)),
		validation.Outbound(`{"type":"object","required":["orderId","status"]}`),
	)

	ok, err := h(context.Background(), jsontext.Value(`{"body":"{\"id\":\"o-1\",\"total\":12.5}"}`))
	fmt.Println(ok, err)

	_, err = h(context.Background(), jsontext.Value(`{"body":"{\"id\":\"o-2\",\"total\":-1}"}`))
	fmt.Println(err)
}
Output:
{o-1 accepted} <nil>
Failed schema validation. Error: data.total must be bigger than or equal to 0, Path: ['data', 'total'], Data: -1

Types

type Draft

type Draft int

Draft is a JSON Schema specification version. A schema's "$schema" keyword picks its draft; the default draft applies to schemas without one.

const (
	Draft4    Draft = 4
	Draft6    Draft = 6
	Draft7    Draft = 7
	Draft2019 Draft = 2019
	Draft2020 Draft = 2020
)

The drafts the validator implements.

func (Draft) String

func (d Draft) String() string

String returns the draft's name, as JSON Schema spells it.

type InvalidSchemaFormatError

type InvalidSchemaFormatError struct {
	// Schema is the JSON text of the schema, when it could be produced.
	Schema string
	// Formats lists the names of the custom formats.
	Formats []string
	// Err is the cause.
	Err error
}

InvalidSchemaFormatError reports a schema that cannot be compiled: it is not valid JSON, breaks its metaschema, references a document no loader can provide, or declares a format pattern that does not compile.

func (*InvalidSchemaFormatError) Error

func (e *InvalidSchemaFormatError) Error() string

func (*InvalidSchemaFormatError) Is

func (*InvalidSchemaFormatError) Is(target error) bool

Is makes every InvalidSchemaFormatError match ErrInvalidSchema.

func (*InvalidSchemaFormatError) Unwrap

func (e *InvalidSchemaFormatError) Unwrap() error

Unwrap returns the cause.

type Loader

type Loader func(ctx context.Context, url string) (any, error)

Loader returns the JSON Schema document at url, for a "$ref" to a document the schema does not embed. The document is raw JSON ([]byte, jsontext.Value or string) or a value that marshals to JSON, such as a map[string]any. The context is that of the invocation that compiles the schema, or context.Background() for Compile and Validate.

type Option

type Option func(*options)

Option configures how a schema is compiled and how data is validated.

Options that shape the compiled schema (formats, loaders, resources, the default draft, the regexp engine) are ignored when the schema given to Validate is already a compiled *Schema.

func WithDefaultDraft

func WithDefaultDraft(d Draft) Option

WithDefaultDraft sets the draft of schemas that do not declare "$schema". It defaults to DefaultDraft.

func WithDefaults

func WithDefaults(enabled bool) Option

WithDefaults turns the injection of schema defaults on or off. It is on by default, like fastjsonschema's use_default: a property missing from the data, whose schema declares a "default", is added to the validated data with that value.

func WithEnvelope

func WithEnvelope(expression string, opts ...jmespath.Option) Option

WithEnvelope selects, with a JMESPath expression, the part of the event to validate, as Powertools Python's envelope parameter does. The expression can call the Powertools functions (powertools_json, powertools_base64, powertools_base64_gzip) and the custom functions in opts. The built-in envelopes are the constants of this package.

func WithFormat

func WithFormat(name string, check func(value string) bool) Option

WithFormat adds a custom format: check receives every string value whose schema names the format and reports whether it conforms. Values that are not strings always conform. A custom format replaces a built-in format of the same name, except "regex".

Example
package main

import (
	"fmt"
	"regexp"

	"github.com/alessandrolattao/powertools-lambda-go/validation"
)

func main() {
	accountID := regexp.MustCompile(`^\d{12}$`)
	schema := validation.MustCompile(`{"properties":{"accountid":{"type":"string","format":"awsaccountid"}}}`,
		validation.WithFormat("awsaccountid", accountID.MatchString))

	_, err := schema.Validate([]byte(`{"accountid":"200984112386"}`))
	fmt.Println(err)
	_, err = schema.Validate([]byte(`{"accountid":"1234"}`))
	fmt.Println(err)
}
Output:
<nil>
Failed schema validation. Error: data.accountid must be awsaccountid, Path: ['data', 'accountid'], Data: 1234

func WithFormatAssertion

func WithFormatAssertion(enabled bool) Option

WithFormatAssertion turns the checking of the "format" keyword on or off. It is on by default for every draft, as fastjsonschema's use_formats, although the 2019-09 and 2020-12 specifications make formats annotations only.

func WithFormatRegexp

func WithFormatRegexp(name, pattern string) Option

WithFormatRegexp adds a custom format defined by a regular expression. As in Python, where the format is applied with re.match, the expression is anchored at the start of the value only: add "$" to anchor its end too.

func WithHTTPLoader

func WithHTTPLoader(client *http.Client) Option

WithHTTPLoader resolves a "$ref" to an http or https URL with a GET through client, which fastjsonschema does by default and this package only on request. A nil client means one with a 10 second timeout. A loader set with WithLoader for the same scheme wins.

func WithLoader

func WithLoader(scheme string, load Loader) Option

WithLoader resolves a "$ref" to a remote document whose URL has the given scheme ("https", "s3", "urn") through load: the Go side of Python's handlers parameter. No remote document is fetched unless a loader for its scheme is set.

Example
package main

import (
	"context"
	"fmt"

	"github.com/alessandrolattao/powertools-lambda-go/validation"
)

func main() {
	const child = `{"type":"object","properties":{"project":{"type":"string"}},"required":["project"]}`
	parent := `{"type":"object","properties":{"ParentSchema":{"$ref":"https://example.com/schemas/child.json"}}}`

	schema, err := validation.Compile(parent, validation.WithLoader("https", func(context.Context, string) (any, error) {
		// Fetch from S3, a parameter, the file system: here, a constant.
		return child, nil
	}))
	if err != nil {
		panic(err)
	}
	_, err = schema.Validate([]byte(`{"ParentSchema":{}}`))
	fmt.Println(err)
}
Output:
Failed schema validation. Error: data.ParentSchema must contain ['project'] properties, Path: ['data', 'ParentSchema'], Data: {}

func WithRegexpEngine

func WithRegexpEngine(compile func(pattern string) (Regexp, error)) Option

WithRegexpEngine replaces Go's regexp package for the "pattern" and "patternProperties" keywords and the "regex" format. Go's engine is RE2, without lookarounds and backreferences: an engine such as github.com/dlclark/regexp2 accepts the patterns Python's re module does.

func WithResource

func WithResource(url string, schema any) Option

WithResource makes the schema document available at url to the "$ref"s of the schema, without fetching it: the way to split a schema into documents that ship with the function. The document is raw JSON or a value that marshals to JSON.

type Regexp

type Regexp interface {
	// MatchString reports whether s contains a match.
	MatchString(s string) bool
	// String returns the source of the expression.
	String() string
}

Regexp is a compiled regular expression, as a custom regexp engine returns it.

type Schema

type Schema struct {
	// contains filtered or unexported fields
}

Schema is a compiled JSON Schema. It is safe for concurrent use: compile it once, at package level or in init, and validate every invocation against it.

func Compile

func Compile(schema any, opts ...Option) (*Schema, error)

Compile compiles a JSON Schema. The schema is raw JSON ([]byte, jsontext.Value or string) or a value that marshals to one, such as a map[string]any or a bool. A failure is an *InvalidSchemaFormatError.

func MustCompile

func MustCompile(schema any, opts ...Option) *Schema

MustCompile is Compile for schemas known to be valid; it panics otherwise.

func (*Schema) Validate

func (s *Schema) Validate(event any, opts ...Option) (any, error)

Validate checks event against the schema; see the Validate function. The options that shape compilation are ignored.

type SchemaValidationError

type SchemaValidationError struct {
	// Message is the full message, in Python's format: "Failed schema
	// validation. Error: <ValidationMessage>, Path: <Path>, Data: <Value>".
	Message string
	// ValidationMessage says what is wrong, in fastjsonschema's words:
	// "data.property[1] must be smaller than or equal to 42".
	ValidationMessage string
	// Name is the location of the value in the data: "data.property[1]".
	Name string
	// Path is the location as a list: ["data", "property", "1"].
	Path []string
	// Value is the value that breaks the schema.
	Value any
	// Definition is the schema the value breaks: {"type": "number",
	// "maximum": 42}.
	Definition any
	// Rule is the keyword the value breaks: "maximum".
	Rule string
	// RuleDefinition is the value of that keyword: 42.
	RuleDefinition any
	// Err is the engine's error.
	Err error
}

SchemaValidationError reports data that breaks the schema, with the fields of Powertools Python's exception of the same name. The engine collects every failure; this error describes the first one, as Python's fail-fast engine does, and unwraps to the engine's *jsonschema.ValidationError, whose BasicOutput lists them all.

func (*SchemaValidationError) Error

func (e *SchemaValidationError) Error() string

func (*SchemaValidationError) Is

func (*SchemaValidationError) Is(target error) bool

Is makes every SchemaValidationError match ErrSchemaValidation.

func (*SchemaValidationError) Unwrap

func (e *SchemaValidationError) Unwrap() error

Unwrap returns the engine's error.

type ValidatorOption

type ValidatorOption func(*validatorConfig)

ValidatorOption configures Validator.

func Inbound

func Inbound(schema any, opts ...Option) ValidatorOption

Inbound validates the event against schema before the handler runs: Python's inbound_schema, with opts in place of inbound_formats, inbound_handlers and inbound_provider_options. WithEnvelope among opts is Python's envelope: it selects the part of the event to validate, and that part is what the handler receives. A nil schema validates nothing, so that Inbound(nil, WithEnvelope(...)) only unwraps the event.

func Outbound

func Outbound(schema any, opts ...Option) ValidatorOption

Outbound validates the handler's response against schema: Python's outbound_schema, with opts in place of outbound_formats, outbound_handlers and outbound_provider_options. The defaults of the schema are filled into the response, as Python does to a dict response. WithEnvelope among opts validates a part of the response only (for example "powertools_json(body)" for an API Gateway response); the response then keeps no defaults.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL