# `Backplane.JsonSchema`

JSON Schema Draft 2020-12 and Draft 7 validator for Elixir.

Compile a schema once, then validate data against it repeatedly:

    iex> schema = %{"type" => "integer", "minimum" => 10}
    iex> {:ok, compiled} = Backplane.JsonSchema.compile(schema)
    iex> Backplane.JsonSchema.validate(compiled, 15)
    :ok
    iex> {:error, [error]} = Backplane.JsonSchema.validate(compiled, 5)
    iex> error.rule
    :minimum
    iex> Backplane.JsonSchema.format_error(error)
    "Value 5 is less than minimum 10"

With format assertion:

    iex> schema = %{"type" => "string", "format" => "email"}
    iex> {:ok, compiled} = Backplane.JsonSchema.compile(schema, format_assertion: true)
    iex> Backplane.JsonSchema.validate(compiled, "user@example.com")
    :ok
    iex> {:error, [error]} = Backplane.JsonSchema.validate(compiled, "not-an-email")
    iex> error.rule
    :format
    iex> Backplane.JsonSchema.format_error(error)
    ~s(Invalid email format: "not-an-email")

For compile-time schema embedding, see:

- `Backplane.JsonSchema.Schema` for the `compile!/2` macro
- `Backplane.JsonSchema.Sigil` for the `~X` sigil

`use Backplane.JsonSchema` imports the `~X` sigil as a convenience.

# `__using__`
*macro* 

Imports the `~X` sigil for compile-time schema literals.

For the explicit API, use `Backplane.JsonSchema.Sigil` directly.

# `bundle_fragment`

```elixir
@spec bundle_fragment(map() | boolean(), keyword()) ::
  {:ok, map() | boolean()} | {:error, Backplane.JsonSchema.Types.Error.t()}
```

Bundles a JSON Schema fragment from a containing document into a standalone raw schema.

This uses the same entrypoint and reference-context options as `compile_fragment/2`,
follows only the schema graph reachable from the entrypoint, mounts reachable
external resources under `$defs`, and returns a raw JSON Schema map or boolean
that can be compiled later with `compile/2`. External refs in unrelated containing-
document subtrees, inactive definitions, examples, and extension data are not loaded.

## Generated definitions

A map bundle may contain generated entries under `$defs` whose names start with
`jsonschex_`. These entries are required storage locations for containing-document
context, external resources, and reachable anchors. Their exact keys are internal,
may receive a numeric suffix to avoid a caller-owned definition, and are not a
stable API. Resources retain their reference identity through `$id` and anchors;
callers should compile the complete returned bundle and must not construct refs to
generated paths such as `#/$defs/jsonschex_external_1`.

Existing caller-owned `$defs` entries are preserved. If generated resources are
required, an existing `$defs` value must be a map; otherwise bundling returns an
`invalid_defs` error instead of replacing it.

When an anchor can only be discovered through opaque containing-document data,
a unique declaration is treated as the anchor target because Backplane.JsonSchema has no
domain-specific knowledge of containers such as OpenAPI components or extensions.
Multiple distinct locations declaring the same anchor are reported as an
`ambiguous_anchor` error. The loader is not invoked for any ambiguous candidate.

## Examples

    iex> document = %{
    ...>   "components" => %{"schemas" => %{"Name" => %{"type" => "string"}}},
    ...>   "schema" => %{"$ref" => "#/components/schemas/Name"}
    ...> }
    iex> {:ok, bundled} = Backplane.JsonSchema.bundle_fragment(document, entry: "#/schema")
    iex> {:ok, schema} = Backplane.JsonSchema.compile(bundled)
    iex> Backplane.JsonSchema.validate(schema, "Ada")
    :ok

# `compile`

Compiles a raw JSON Schema into a reusable `Schema` struct.

## Options

- `:loader` — `(uri -> {:ok, map()} | {:error, term()})` for remote `$ref` schemas
- `:base_uri` — Starting base URI for resolving relative references
- `:format_assertion` — Explicitly enable `format` validation (default: `false`); an active Draft 2020-12 format-assertion vocabulary also enables it.
- `:content_assertion` — Enable strict content vocabulary validation (default: `false`)
- `:draft` — `"draft7"` or `"draft2020_12"` (strings, not atoms). Recognized root `$schema` URIs select the draft automatically; a conflicting option returns `{:error, %Backplane.JsonSchema.Types.Error{rule: :draft_conflict}}`. Without either, Draft 2020-12 is the default. Custom `$schema` URIs do not select a draft.

See the [Loader guide](guide/loader.md) and
[Content and Format guide](guide/content_and_format.md) for details.

## Examples

    iex> {:ok, schema} = Backplane.JsonSchema.compile(%{"type" => "string"})
    iex> is_struct(schema, Backplane.JsonSchema.Types.Schema)
    true

    iex> {:ok, schema} = Backplane.JsonSchema.compile(%{"type" => "string", "format" => "email"}, format_assertion: true)
    iex> Backplane.JsonSchema.validate(schema, "test@example.com")
    :ok

# `compile_fragment`

```elixir
@spec compile_fragment(map() | boolean(), keyword()) ::
  {:ok, Backplane.JsonSchema.Types.Schema.t()}
  | {:error, Backplane.JsonSchema.Types.Error.t()}
```

Compiles a JSON Schema fragment from a containing document.

This is useful when a schema lives inside a larger resource, such as an
OpenAPI 3.1 document, and local references like `#/components/schemas/User`
must resolve against that containing document rather than against the fragment
map in isolation.

Draft 7 entry contexts follow ancestor `$id` resources: pointer lookup uses the
owning resource's raw schema, while discovery still scans the containing document.
Without a new resource boundary, the containing document remains the pointer root.

## Options

- `:entry` — JSON Pointer or URI-reference style entrypoint (`#/...`, `/...`, or `path-or-uri#/...`). If it includes a base URI/path and `:base_uri` is omitted, that base is used for relative reference resolution.
- `:base_uri` — optional starting base URI/path for resolving relative references when `:entry` is a document-local pointer
- `:loader` — optional loader for external resources. It may return `{:ok, schema}` or `{:ok, %{document: schema, base_uri: base_uri}}`; wrapper metadata uses atom keys only.
- `:format_assertion` — Explicitly enable `format` validation (default: `false`); an active Draft 2020-12 format-assertion vocabulary also enables it.
- `:content_assertion` — Enable strict content vocabulary validation (default: `false`)
- `:draft` — `"draft7"` or `"draft2020_12"` (strings, not atoms). A recognized `$schema` at the containing document's root selects the draft and must agree with this option. Otherwise the option selects the draft; if neither selects a draft, Draft 2020-12 is the default.

Use a document-local entry such as `"#/components/schemas/User"` when the
containing document's base is supplied separately with `:base_uri` or when no
relative external refs are reachable. Use a URI-reference entry such as
`"/api/openapi.yaml#/components/schemas/User"` when the entrypoint and base
URI/path can be represented together.

## Examples

    iex> document = %{
    ...>   "components" => %{"schemas" => %{"Name" => %{"type" => "string"}}},
    ...>   "schema" => %{"$ref" => "#/components/schemas/Name"}
    ...> }
    iex> {:ok, schema} = Backplane.JsonSchema.compile_fragment(document, entry: "#/schema")
    iex> Backplane.JsonSchema.validate(schema, "Ada")
    :ok
    iex> {:error, [%{rule: :type}]} = Backplane.JsonSchema.validate(schema, 123)

    iex> document = %{"schema" => %{"type" => "integer"}}
    iex> {:ok, schema} = Backplane.JsonSchema.compile_fragment(document, entry: "/api/openapi.yaml#/schema")
    iex> Backplane.JsonSchema.validate(schema, 42)
    :ok

# `format_error`

```elixir
@spec format_error(Backplane.JsonSchema.Types.Error.t()) :: String.t()
```

Formats a validation error into a human-readable string.

## Examples

    iex> error = %Backplane.JsonSchema.Types.Error{path: ["age", "user"], rule: :minimum, context: %Backplane.JsonSchema.Types.ErrorContext{contrast: 0, input: -5}}
    iex> Backplane.JsonSchema.format_error(error)
    "At /user/age: Value -5 is less than minimum 0"

    iex> {:error, error} = Backplane.JsonSchema.compile(%{"type" => "1"})
    iex> Backplane.JsonSchema.format_error(error)
    ~s(Keyword 'type' must be one of [string, integer, number, boolean, object, array, null], got: "1")

    iex> {:error, error} = Backplane.JsonSchema.compile(%{"minimum" => "five"})
    iex> Backplane.JsonSchema.format_error(error)
    ~s(Keyword 'minimum' must be a number, got: "five")

    iex> {:error, error} = Backplane.JsonSchema.compile(%{"multipleOf" => -3})
    iex> Backplane.JsonSchema.format_error(error)
    ~s(Keyword 'multipleOf' must be a strictly positive number, got: -3)

    iex> {:error, error} = Backplane.JsonSchema.compile(%{"minLength" => -1})
    iex> Backplane.JsonSchema.format_error(error)
    ~s(Keyword 'minLength' must be a non-negative integer, got: -1)

    iex> {:error, error} = Backplane.JsonSchema.compile(%{"uniqueItems" => "yes"})
    iex> Backplane.JsonSchema.format_error(error)
    ~s(Keyword 'uniqueItems' must be a boolean, got: "yes")

# `validate`

```elixir
@spec validate(Backplane.JsonSchema.Types.Schema.t(), term()) ::
  :ok | {:error, [Backplane.JsonSchema.Types.Error.t()]}
```

Validates data against a compiled schema.

Returns `:ok` or `{:error, [Error.t()]}`. Use `Backplane.JsonSchema.format_error/1` on
individual errors to produce human-readable messages.

## Examples

    iex> {:ok, schema} = Backplane.JsonSchema.compile(%{"type" => "integer"})
    iex> Backplane.JsonSchema.validate(schema, 42)
    :ok

    iex> {:ok, schema} = Backplane.JsonSchema.compile(%{"type" => "integer"})
    iex> {:error, errors} = Backplane.JsonSchema.validate(schema, "not an integer")
    iex> [error] = errors
    iex> error.rule
    :type
    iex> error.path
    []
    iex> Backplane.JsonSchema.format_error(error)
    "Expected type integer, got string"

---

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