# `JSV.Schema`
[🔗](https://github.com/lud/jsv/blob/v0.18.3/lib/jsv/schema.ex#L1)

This module defines a struct where all the supported keywords of the JSON
schema specification are defined as keys. Text editors that can predict the
struct keys will make autocompletion available when writing schemas.

### Using in build

The `%JSV.Schema{}` struct can be given to `JSV.build/2`:

    schema = %JSV.Schema{type: :integer}
    JSV.build(schema, options())

Because Elixir structs always contain all their defined keys, writing a schema
as `%JSV.Schema{type: :integer}` is actually defining the following:

    %JSV.Schema{
      type: :integer,
      "$id": nil
      additionalItems: nil,
      additionalProperties: nil,
      allOf: nil,
      anyOf: nil,
      contains: nil,
      # etc...
    }

For that reason, when giving a `%JSV.Schema{}` struct to `JSV.build/2`, any `nil` value is
ignored. The same behaviour can be defined for other struct by implementing
the `JSV.Normalizer.Normalize` protocol. Mere maps will keep their `nil`
values.

Note that `JSV.build/2` does not require `%JSV.Schema{}` structs, any map with binary or
atom keys is accepted.

This is also why the `%JSV.Schema{}` struct does not define the `const` keyword, because
`nil` is a valid value for that keyword but there is no way to know if the
value was omitted or explicitly defined as `nil`. To circumvent that you may
use the `enum` keyword or just use a regular map instead of this module's
struct:

    %JSV.Schema{enum: [nil]}
    # OR
    %{const: nil}

### Functional helpers

This module also exports a small range of utility functions to ease writing
schemas in a functional way.

This is mostly useful when generating schemas dynamically, or for shorthands.

For instance, instead of writing the following:

    %Schema{
      type: :object,
      properties: %{
        name: %Schema{type: :string, description: "the name of the user", minLength: 1},
        age: %Schema{type: :integer, description: "the age of the user"}
      },
      required: [:name, :age]
    }

One can write:

    %Schema{
      type: :object,
      properties: %{
        name: string(description: "the name of the user", minLength: 1),
        age: integer(description: "the age of the user")
      },
      required: [:name, :age]
    }

This is also useful when building schemas dynamically, as the helpers are
pipe-able one into another:

    new()
    |> props(
      name: string(description: "the name of the user", minLength: 1),
      age: integer(description: "the age of the user")
    )
    |> required([:name, :age])

# `attributes`

```elixir
@type attributes() ::
  %{required(binary() | atom()) =&gt; term()} | [{atom() | binary(), term()}]
```

# `merge_base`

```elixir
@type merge_base() :: attributes() | [{atom() | binary(), term()}] | struct() | nil
```

# `schema`

```elixir
@type schema() :: true | false | map()
```

# `schema_data`

```elixir
@type schema_data() ::
  %{optional(binary()) =&gt; schema_data()}
  | [schema_data()]
  | number()
  | binary()
  | boolean()
  | nil
```

# `t`

```elixir
@type t() :: %JSV.Schema{
  &quot;$anchor&quot;: term(),
  &quot;$comment&quot;: term(),
  &quot;$defs&quot;: term(),
  &quot;$dynamicAnchor&quot;: term(),
  &quot;$dynamicRef&quot;: term(),
  &quot;$id&quot;: term(),
  &quot;$ref&quot;: term(),
  &quot;$schema&quot;: term(),
  additionalItems: term(),
  additionalProperties: term(),
  allOf: term(),
  anyOf: term(),
  contains: term(),
  contentEncoding: term(),
  contentMediaType: term(),
  contentSchema: term(),
  default: term(),
  dependencies: term(),
  dependentRequired: term(),
  dependentSchemas: term(),
  deprecated: term(),
  description: term(),
  else: term(),
  enum: term(),
  examples: term(),
  exclusiveMaximum: term(),
  exclusiveMinimum: term(),
  format: term(),
  if: term(),
  items: term(),
  &quot;jsv-cast&quot;: term(),
  maxContains: term(),
  maxItems: term(),
  maxLength: term(),
  maxProperties: term(),
  maximum: term(),
  minContains: term(),
  minItems: term(),
  minLength: term(),
  minProperties: term(),
  minimum: term(),
  multipleOf: term(),
  not: term(),
  oneOf: term(),
  pattern: term(),
  patternProperties: term(),
  prefixItems: term(),
  properties: term(),
  propertyNames: term(),
  readOnly: term(),
  required: term(),
  then: term(),
  title: term(),
  type: term(),
  unevaluatedItems: term(),
  unevaluatedProperties: term(),
  uniqueItems: term(),
  writeOnly: term()
}
```

# `__using__`
*macro* 

Use this module to define module-based schemas or schemas with the helpers
API.

* Imports struct and cast definitions from `JSV`.
* Imports the `JSV.Schema.Helpers` module with the `string`, `integer`,
  `enum`, _etc._ helpers.

### Example

    defmodule MySchema do
      use JSV.Schema

      defschema %{
        type: :object,
        properties: %{
          foo: string(description: "Some foo!"),
          bar: integer(minimum: 100) |> with_cast(__MODULE__,:hashid),
          sub: props(sub_foo: string(), sub_bar: integer()) pp
        }
      }

      defcast hashid(bar) do
        {:ok, Hashids.decode!(bar, cipher())}
      end
    end

# `combine`

```elixir
@spec combine(attributes(), attributes()) :: schema()
```

Merges two sets of attributes into a single map. Attributes can be a keyword
list or a map.

# `from_module`

```elixir
@spec from_module(module()) :: schema()
```

Calls the `json_schema/0` function on the given module.

# `merge`

```elixir
@spec merge(merge_base(), attributes()) :: schema()
```

Merges the given key/values into the base schema. The merge is shallow and
will overwrite any pre-existing key.

This function is defined to work with the `JSV.Schema.Composer` API.

The resulting schema is always a map or a struct but the actual type depends
on the given base. It follows the followng rules:

* **When the base type is a map or a struct, it is preserved**
  - If the base is a `%JSV.Schema{}` struct, the `values` are merged in.
  - If the base is another struct, the `values` a merged in. It will fail if
    the struct does not define the overriden keys. No invalid struct is
    generated.
  - If the base is a mere map, it is **not** turned into a `%JSV.Schema{}` struct and the
    `values` are merged in.

* **Otherwise the base is cast to a `%JSV.Schema{}` struct**
  - If the base is `nil`, the function returns a `%JSV.Schema{}` struct with the given
    `values`.
  - If the base is a keyword list, the list will be turned into a `%JSV.Schema{}` struct
  and then the `values` are merged in.

## Examples

    iex> JSV.Schema.merge(%JSV.Schema{description: "base"}, %{type: :integer})
    %JSV.Schema{description: "base", type: :integer}

    defmodule CustomSchemaStruct do
      defstruct [:type, :description]
    end

    iex> JSV.Schema.merge(%CustomSchemaStruct{description: "base"}, %{type: :integer})
    %CustomSchemaStruct{description: "base", type: :integer}

    iex> JSV.Schema.merge(%CustomSchemaStruct{description: "base"}, %{format: :date})
    ** (KeyError) struct CustomSchemaStruct does not accept key :format

    iex> JSV.Schema.merge(%{description: "base"}, %{type: :integer})
    %{description: "base", type: :integer}

    iex> JSV.Schema.merge(nil, %{type: :integer})
    %JSV.Schema{type: :integer}

    iex> JSV.Schema.merge([description: "base"], %{type: :integer})
    %JSV.Schema{description: "base", type: :integer}

# `new`

```elixir
@spec new() :: t()
```

Returns a new empty schema.

# `new`

```elixir
@spec new(t() | attributes()) :: t()
```

Returns a new schema with the given key/values.

# `normalize`

```elixir
@spec normalize(term()) ::
  %{optional(binary()) =&gt; schema_data()}
  | [schema_data()]
  | number()
  | binary()
  | boolean()
  | nil
```

Normalizes a JSON schema with the help of `JSV.Normalizer.normalize/3` with
the following customizations:

* `JSV.Schema` structs pairs where the value is `nil` will be removed.
  `%JSV.Schema{type: :object, properties: nil, allOf: nil, ...}` becomes
  `%{"type" => "object"}`.
* Modules names that export a schema will be converted to a raw schema with a
  reference to that module that can be resolved automatically by
  `JSV.Resolver.Internal`.
* Other atoms will be checked to see if they correspond to a module name that
  exports a `json_schema/0` function.

### Examples

    defmodule Elixir.ASchemaExportingModule do
      def json_schema, do: %{}
    end

    iex> JSV.Schema.normalize(ASchemaExportingModule)
    %{"$ref" => "jsv:module:Elixir.ASchemaExportingModule"}

    defmodule AModuleWithoutExportedSchema do
      def hello, do: "world"
    end

    iex> JSV.Schema.normalize(AModuleWithoutExportedSchema)
    "Elixir.AModuleWithoutExportedSchema"

# `normalize_collect`

```elixir
@spec normalize_collect(
  term(),
  keyword()
) :: %{optional(binary()) =&gt; schema_data()} | atom()
```

Behaves like `normalize/1` but all nested module-based schemas are collected
into `$defs` so the result is a self contained schema, whereas the default
normalization function returns references for `JSV.Resolver.Internal`.

Schemas are collected using their title for the key under `$defs`. If multiple
schemas use the same title, the title is suffixed with `_1`, `_2` and so on.

This function does not support schemas with pre-existing `$defs`, it will
ignore them and keep them nested. If such schemas are present and use `$ref`
to their own definitions, the schema returned from this function may not be
valid. To prevent this, schemas with definitions should define an `$id` and
use this in `$ref` references.

### Options

- `:as_root` - boolean, when `true` and used in combination with a
  module-based schema, that module's schema will be kept as the root schema
  instead of being wrapped in a definition. This will overwrite any `$defs`
  present in the schema.

# `schema_module?`

```elixir
@spec schema_module?(atom()) :: boolean()
```

Returns whether the given atom is a module with a `schema/0` exported
function.

# `to_map`

```elixir
@spec to_map(t()) :: %{optional(atom()) =&gt; term()}
```

Returns the given `%JSV.Schema{}` as a map without keys containing
a `nil` value.

# `with_cast`

```elixir
@spec with_cast(merge_base(), [atom() | binary() | integer(), ...]) :: schema()
```

Includes the cast function in a schema. The cast function must be given as a
list with two items:

* A module, as atom or string
* A tag, as atom, string or integer.

Atom arguments will be converted to string.

### Examples

    iex> JSV.Schema.with_cast([MyApp.Cast, :a_cast_function])
    %JSV.Schema{"jsv-cast": ["Elixir.MyApp.Cast", "a_cast_function"]}

    iex> JSV.Schema.with_cast([MyApp.Cast, 1234])
    %JSV.Schema{"jsv-cast": ["Elixir.MyApp.Cast", 1234]}

    iex> JSV.Schema.with_cast(["some_erlang_module", "custom_tag"])
    %JSV.Schema{"jsv-cast": ["some_erlang_module", "custom_tag"]}

---

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