A small JSON Schema validator for the input of API tools.
Core is installed in every PhoenixKit application, so it does not take a JSON
Schema dependency for this. The validator implements exactly the subset the
tools use. Any other validating keyword (const, exclusiveMinimum,
multipleOf, uniqueItems, oneOf, anyOf, allOf, not, $ref,
patternProperties, minProperties and so on) would be ignored by validate/2,
so check_schema/2 refuses a schema that uses one; PhoenixKit.API.Tool
runs it on every tool, so a schema mistake fails at registration, not as a
500 on one request.
Annotations constrain nothing and are accepted and ignored: description,
title, default, examples and format, and also $schema,
$id, $comment, example, deprecated, readOnly, writeOnly and any key
that starts with a lowercase x-. format is not enforced: a tool that needs
a shape checked declares a pattern.
| Keyword | Applies to | Error limit / allowed |
|---|---|---|
type (one name or a list) | any | |
enum | any | allowed |
required, properties, additionalProperties | objects | |
minimum, maximum (inclusive) | numbers | limit |
minLength, maxLength (characters) | strings | limit |
pattern (unanchored, ECMA-262 style) | strings | limit |
minItems, maxItems, items | arrays | limit |
type is one of "object", "array", "string", "integer", "number",
"boolean", "null". A schema with an unsupported type, or a pattern that
is not a valid regular expression, is a bug in the tool and raises
ArgumentError (validate/2 compiles every pattern of the schema once, up
front). A pattern follows JSON Schema (ECMA-262), not PCRE defaults: $
does not match before a trailing newline, and \d, \w and [[:alpha:]]
are ASCII only, while . still matches one code point. Booleans are valid
sub-schemas (true accepts anything, false accepts nothing).
Input and schema
Input is what Jason decodes: maps with string keys, lists, strings, numbers,
booleans and nil. A schema is a JSON-shaped map. Its keys may be strings or
atoms (they are normalised to strings first), so %{type: "object"} works.
Atom values of enum are compared as strings. A number member matches a
number that is equal to it (1 and 1.0); a list or an object member matches
strictly, so [1.0] does not match [1].
A float with no fractional part is accepted for "integer" and comes back as an
integer (JSON has one number type, and 2.0 is an integer in JSON Schema). The
validated value is otherwise the input unchanged. A tool that must not accept
an absurdly large integer declares maximum (1.0e300 is an integral float).
Errors
Every problem is reported, not just the first. Each error names the offending
value by path:
"name" a property of the input
"items[2].quantity" a property of the third element of `items`
"[0]" the first element when the input itself is an array
"" the input as a wholemessage is for people, limit carries the bound that was broken and allowed
the permitted values, so a client can show them without parsing text.
At most 100 errors are returned. The input is untrusted and can be megabytes
large (20 000 unknown keys against a closed schema, a list of 100 000 wrong
elements). String lengths are counted without building a list, patterns are
compiled once per call, and error lists are built by prepending (never
errors ++ more per key). The walk stops as soon as it has collected 100
errors, and the answer is {:error, first_100}. A result of exactly 100
errors may therefore be incomplete. Error paths shorten a key longer than 64
bytes, so an error response is not as large as the body that caused it.
A string that is not valid UTF-8 never matches a pattern.
Summary
Functions
Whether keyword only annotates a schema: check_schema/2 accepts it and
validate/2 ignores it. True for the annotations listed in the moduledoc and
for any key that starts with a lowercase x-.
Checks that a tool's schema only uses what validate/2 enforces.
Adds executor control fields (dry_run, confirm_token, idempotency_key) to a
tool's input schema.
Validates input against schema.
Types
Functions
Whether keyword only annotates a schema: check_schema/2 accepts it and
validate/2 ignores it. True for the annotations listed in the moduledoc and
for any key that starts with a lowercase x-.
Checks that a tool's schema only uses what validate/2 enforces.
Returns :ok or {:error, messages}. A message names the offending keyword
("input_schema.properties.sku.format is not supported ..."). It refuses a
validating keyword validate/2 would silently ignore, an unsupported type, a
bound that is not a number (a length or item count that is not a non-negative
integer), a pattern that is not a valid regular expression, and a sub-schema
that is neither an object nor a boolean. Annotations (description, title,
default, examples, format ...) and x- extensions pass. root is the label used
at the start of every message.
Adds executor control fields (dry_run, confirm_token, idempotency_key) to a
tool's input schema.
extra_properties maps field names to their schemas. They are merged into the
schema's properties, replacing a property of the same name (a module never
declares them), and the schema is closed with additionalProperties: false, so
every other unknown field is still refused. The result has string keys.
Validates input against schema.
Returns {:ok, value} (the input, with integral floats turned into integers
where the schema says "integer") or {:error, errors} with the violations,
at most 100 of them.