# `Rupa.JsonSchema`
[🔗](https://github.com/zero-one-group/rupa/blob/v0.1.0/lib/rupa/json_schema.ex#L1)

JSON Schema draft 2020-12, in both directions.

`encode/1` is total: Rupa's vocabulary is a subset of JSON Schema's declarative one, so every
schema that validates has a document. `decode/1` is not, and cannot be — JSON Schema says far
more than Rupa does. What it will not do is fail quietly: a keyword Rupa has no answer for is
a named error with the path to it, never a silently dropped constraint.

## `encode/1` writes what the wire sees

It stages the schema first, so the document describes the document — `rename_all:`, `from:`
and `keys:` are already spent, and the property names are the wire keys rather than the field
names. A field carrying both `from:` and `to:` reads one key and writes another, and what the
document describes is the one it reads.

Staging also resolves refs, so a `$ref` that is not on a cycle is inlined and only genuine
recursion comes back out as `$defs` and `$ref`. The document says the same thing either way;
it is just longer.

## `decode/1` interns nothing

The objects it produces are named with strings, so no property name in a document ever becomes
an atom. That is the whole reason `Rupa.Schema` grew string field names, and it is what makes
it safe to decode a schema that arrived over the wire. `keys: :atom` is still there if you
want atoms out of a document you trust.

Two constructs in Rupa's own vocabulary cannot come back this way, because both would have to
intern an atom from the document to exist at all:

  * a **tagged union** — its decoded tag is an atom, so `oneOf` decodes to nothing
  * a **recursive ref** — `$defs` names are atoms, so a `$ref` on a cycle decodes to nothing

Both encode fine; neither decodes. A `$ref` that is *not* on a cycle is inlined instead, which
needs no name at all, so ordinary reuse through `$defs` works.

## Where Rupa and JSON Schema disagree

Documented rather than papered over, and measured: the official JSON-Schema-Test-Suite is
vendored under `test/fixtures/json-schema-suite/` and every case runs on every build. What
follows is the whole of what it reports.

**Rupa is more permissive on three, and `encode/1` emits the keyword anyway**, so a reader
that follows the spec is stricter than Rupa rather than looser:

  * **`format` asserts.** JSON Schema makes `format` an annotation by default; Rupa's formats
    validate and several convert. `Rupa.Schema.formats/0` is the fixed set.
  * **`pattern` is PCRE**, not ECMA-262, because it compiles with Erlang's `:re`.
  * **`minLength` and `maxLength` count grapheme clusters**, not code points — what a reader
    would count.

**Rupa is stricter on one**, and it is one decision seen from several angles: *a number is
compared by term, not by mathematical value*. JSON Schema says `1.0` is an `integer`, that
`{"const": 0}` matches `0.0`, and that an `enum` of `1` matches `1.0`. On the BEAM those are
different terms, and reconciling them would put a check on every decode in every schema to
serve the documents that write `1.0` and mean `1`.

Everything else the suite asks, on the keywords Rupa claims, Rupa answers the way the spec
says. A keyword it does not claim is a skip with a reason, never a quiet pass.

# `decode`

```elixir
@spec decode(term()) :: {:ok, Rupa.Schema.t()} | {:error, [Rupa.Error.t()]}
```

Turns a draft 2020-12 document into a schema, or says what it could not.

Takes the document as a decoded term, or as JSON text. The objects it produces are named with
strings, so nothing in the document becomes an atom.

    iex> {:ok, schema} = Rupa.JsonSchema.decode(~s({"type": "string", "minLength": 1}))
    iex> schema
    {:string, [min: 1]}

    iex> {:error, [error]} = Rupa.JsonSchema.decode(%{"allOf" => []})
    iex> {error.code, error.meta.keyword}
    {:unsupported_keyword, "allOf"}

A schema it hands back compiles: what the document says in a way Rupa refuses -- a `minLength`
above its `maxLength`, a `pattern` on a `date-time` -- is reported here, as the schema error
`Rupa.compile/2` would have given, rather than left for the compile to find.

    iex> {:error, [error]} = Rupa.JsonSchema.decode(%{"type" => "string", "minLength" => 5, "maxLength" => 2})
    iex> error.code
    :contradictory_bounds

# `decode!`

```elixir
@spec decode!(term()) :: Rupa.Schema.t()
```

`decode/1`, raising `Rupa.SchemaError` instead of returning errors.

    iex> Rupa.JsonSchema.decode!(%{"type" => "integer", "minimum" => 0})
    {:integer, [gte: 0]}

# `encode`

```elixir
@spec encode(term()) :: {:ok, map()} | {:error, [Rupa.Error.t()]}
```

Turns a schema into a draft 2020-12 document, as a decoded JSON term.

The only errors are the schema's own, because staging runs first.

    iex> Rupa.JsonSchema.encode!(%{name: Rupa.T.string(min: 1)})
    %{
      "$schema" => "https://json-schema.org/draft/2020-12/schema",
      "type" => "object",
      "properties" => %{"name" => %{"type" => "string", "minLength" => 1}},
      "required" => ["name"]
    }

# `encode!`

```elixir
@spec encode!(term()) :: map()
```

`encode/1`, raising `Rupa.SchemaError` instead of returning errors.

    iex> Rupa.JsonSchema.encode!(Rupa.T.boolean())
    %{"$schema" => "https://json-schema.org/draft/2020-12/schema", "type" => "boolean"}

---

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