schemavalidate

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package schemavalidate resolves entity JSON Schemas once and validates write payloads against them.

Resolution is done exactly once per schema, at construction. Measured against the largest shipped schema, resolving costs ~1.58ms while validating an already-resolved schema costs ~5.5us — a ~250x ratio, so re-resolving per request would add ~0.7s to a 500-record batch create (#314).

Cross-file references are restricted to plain siblings inside the schema directory. A schema may say "lead.json#/properties/contact"; it may not reach a subdirectory, a parent, or the network. Anything else is rejected loudly rather than being rewritten to a same-named local file, which would validate a document against unrelated constraints.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ArrayPaths

type ArrayPaths map[string]struct{}

ArrayPaths is the set of dotted attribute paths a schema declares as arrays.

It exists because dotted attribute names are ambiguous about arrays and the metadata cache cannot resolve that ambiguity: requirement.areas.city and contact.email are recorded identically, and the attribute generator computes array membership only to discard it. The schema is the only loaded source of truth, and this package already holds every schema resolved, so the set is derived here once at construction rather than re-walked per request.

Paths use the attribute-name convention: an array index is not part of a name, so array elements keep their parent's path. That is what makes requirement.areas the recorded path for items of requirement.areas, matching the attribute name requirement.areas.city.

func (ArrayPaths) CrossesBelow

func (p ArrayPaths) CrossesBelow(prefix, name string) bool

CrossesBelow reports whether name lies beneath an array declared *below* prefix, where prefix is the document position the caller is already at. Pass "" to ask about the whole name.

The position matters because a dotted key inside an array element is already past that array: within an element of propertyInterests, "snapshot.code" nests legally, even though the absolute name propertyInterests.snapshot.code does cross an array. Only arrays strictly between prefix and the name's own leaf can make nesting impossible, so paths at or above prefix are skipped.

Only proper prefixes count at the other end too. A name that *is* an array path — a literal "contact.phones" holding the array itself — describes the array rather than something under it, and nesting it is both correct and necessary for the value to be validated.

type Validator

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

Validator holds one resolved schema per schema ID. It is built once at startup and is safe for concurrent use: the map is never written after New returns, and jsonschema.Resolved is read-only during Validate.

func New

func New(registry forma.SchemaRegistry, schemaDir string) (*Validator, error)

New resolves every schema the registry knows about. It fails closed: any schema that cannot be resolved aborts construction and names the schema, so a broken $ref is a deploy-time error rather than a silent loss of validation at runtime (#314).

The resolve options are built once for the whole registry: they carry only the base URI and the sibling-restricted loader, both fixed by schemaDir.

func (*Validator) ArrayPaths

func (v *Validator) ArrayPaths(schemaID int16) ArrayPaths

ArrayPaths returns the array paths declared by the schema registered for schemaID, or nil when there is no such schema.

Nil is a safe answer rather than an error: it means "nothing known to be an array", which leaves callers with the behaviour they had before this set existed. A nil receiver is treated the same way, matching Validate, because callers may hold a *Validator that is nil when validation is unconfigured.

func (*Validator) Validate

func (v *Validator) Validate(schemaID int16, doc any) error

Validate checks doc against the schema registered for schemaID.

A violation, or a payload json.Marshal refuses to encode, wraps forma.ErrInvalidInput: both are caller input and must surface as 4xx. A missing resolved schema does not — that is a server configuration fault and must stay operator-visible (docs/error-handling.md). Neither does a failure to decode the marshaller's own output: marshalling already succeeded, so that is an internal fault, not something the caller handed in.

exactNumberInstance is the one honest gap. A literal that fits neither int64 nor float64 — {"score": 1e400}, which arrives intact because httpapi decodes with UseNumber and json.Marshal re-emits a json.Number verbatim — is caller input, yet it answers a plain error here and so a 500. That is a known misclassification, tracked in #402, not a decision this comment is defending.

doc is marshalled before validating. Native Go values do not carry their JSON types: time.Time presents as an object and fails a "type":"string" property until round-tripped, and two production call sites assign time.Now() to string-typed properties. The round-trip decodes with UseNumber and rewrites numbers via exactNumberInstance so constraint checks stay exact above 2^53.

A nil receiver is treated as "no schema resolved" rather than panicking: callers may hold a *Validator that is nil when validation is unconfigured.

Jump to

Keyboard shortcuts

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