# `PhoenixKit.API.Validator`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.64.1/lib/phoenix_kit/api/validator.ex#L1)

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 whole

`message` 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`.

# `error`

```elixir
@type error() :: %{
  path: String.t(),
  message: String.t(),
  limit: term() | nil,
  allowed: [term()] | nil
}
```

# `annotation?`

```elixir
@spec annotation?(String.t()) :: boolean()
```

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-`.

# `check_schema`

```elixir
@spec check_schema(term(), String.t()) :: :ok | {:error, [String.t()]}
```

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.

# `close_schema`

```elixir
@spec close_schema(map(), map()) :: map()
```

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.

# `validate`

```elixir
@spec validate(map(), term()) :: {:ok, term()} | {:error, [error()]}
```

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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
