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

JSV is a JSON Schema Validator.

This module is the main facade for the library.

To start validating schemas you will need to go through the following steps:

1. [Obtain a schema](guides/schemas/defining-schemas.md). Schemas can be
   defined in Elixir code, read from files, fetched remotely, _etc_.
1. [Build a validation root](guides/build/build-basics.md) with `build/2` or
   `build!/2`.
1. [Validate the data](guides/validation/validation-basics.md).

## Example

Here is an example of the most simple way of using the library:

```elixir
schema = %{
  type: :object,
  properties: %{
    name: %{type: :string}
  },
  required: [:name]
}

root = JSV.build!(schema)

case JSV.validate(%{"name" => "Alice"}, root) do
  {:ok, data} ->
    {:ok, data}

  # Errors can be turned into JSON compatible data structure to send them as an
  # API response or for logging purposes.
  {:error, validation_error} ->
    {:error, JSON.encode!(JSV.normalize_error(validation_error))}
end
```

If you want to explore the different capabilities of the library, please refer
to the guides provided in this documentation.

# `build_context`

```elixir
@opaque build_context()
```

# `build_opt`

```elixir
@type build_opt() ::
  {:resolver, atom() | {module(), [term()]} | [atom() | {module(), [term()]}]}
  | {:default_meta, binary()}
  | {:formats, boolean() | nil | [atom()]}
  | {:vocabularies, %{optional(binary()) =&gt; atom() | {module(), [term()]}}}
```

# `native_schema`

```elixir
@type native_schema() :: boolean() | map() | module() | normal_schema()
```

A schema in native JSV/Elixir terms: maps with atoms, structs, and module.

# `normal_schema`

```elixir
@type normal_schema() ::
  boolean() | %{required(binary()) =&gt; normal_schema() | [normal_schema()]}
```

A schema in a JSON-decoded form: Only maps with binary keys and
binary/number/boolean/nil values, or a boolean.

The name refers to the process of _normalization_. A `t:native_schema/0` can
be turned into a `t:normal_schema/0` with the help of
`JSV.Schema.normalize/1`.

# `validate_opt`

```elixir
@type validate_opt() ::
  {:cast, boolean()} | {:cast_formats, boolean()} | {:key, term()}
```

# `build`

```elixir
@spec build(native_schema(), [build_opt()]) ::
  {:ok, JSV.Root.t()} | {:error, Exception.t()}
```

Builds the schema as a `JSV.Root` schema for validation.

### Options

* `:resolver` - The `JSV.Resolver` behaviour implementation module to
  retrieve schemas identified by an URL.

  Accepts a `module`, a `{module, options}` tuple or a
  list of those forms.

  The options can be any term and will be given to the
  `resolve/2` callback of the module.

  The `JSV.Resolver.Embedded` and `JSV.Resolver.Internal`
  will be automatically appended to support module-based
  schemas and meta-schemas.

  The default value is `[]`.

* `:default_meta` (`t:String.t/0`) - The meta schema to use for resolved schemas that do not define a `"$schema"` property. The default value is `"https://json-schema.org/draft/2020-12/schema"`.

* `:formats` - Controls the validation of strings with the `"format"` keyword.

  * `nil` - Format validation is enabled if to the meta-schema uses the format assertion vocabulary.
  * `true` - Enforces validation with the default validator modules.
  * `false` - Disables all format validation.
  * `[Module1, Module2,...]` (A list of modules) - Format validation is enabled and
     will use those modules as validators instead of the default format validator modules.
     The default format validator modules can be included back in the list manually,
     see `default_format_validator_modules/0`.

  > #### Formats are disabled by the default meta-schema {: .warning}
  >
  > The default value for this option is `nil` to respect
  > the JSON Schema specification where format validation
  > is enabled via vocabularies.
  >
  > The default meta-schemas for the latest drafts (example: `https://json-schema.org/draft/2020-12/schema`)
  > do not enable format validation.
  >
  > You'll probably want this option to be set to `true`
  > or a list of your own modules.

  Worth noting, while this option does support providing your own formats,
  the [official specification](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.2.3)
  recommends against it:

  > Vocabularies do not support specifically declaring different value sets for keywords.
  > Due to this limitation, and the historically uneven implementation of this keyword,
  > it is RECOMMENDED to define additional keywords in a custom vocabulary rather than
  > additional format attributes if interoperability is desired.

  The default value is `nil`.

* `:vocabularies` - Allows to redefine modules implementing vocabularies.

  This option accepts a map with vocabulary URIs as keys and implementations as values.
  The URIs are not fetched by JSV and does not need to point to anything specific.
  For instance, vocabulary URIs in the standard Draft 2020-12 meta-schema point to
  human-readable documentation.

  The given implementations will only be used if the meta-schema used to build a validation root
  actually declare those URIs in their `$vocabulary` keyword.

  For instance, to redefine how the `type` keyword and other validation keywords are handled,
  one should pass the following map:

      %{
        "https://json-schema.org/draft/2020-12/vocab/validation" => MyCustomModule
      }

  Modules must implement the `JSV.Vocabulary` behaviour.

  Implementations can also be passed options by wrapping them in a tuple:

      %{
       "https://json-schema.org/draft/2020-12/vocab/validation" => {MyCustomModule, foo: "bar"}
      }

  The default value is `%{}`.

# `build!`

```elixir
@spec build!(native_schema(), [build_opt()]) :: JSV.Root.t()
```

Same as `build/2` but raises on error. Errors are not normalized into a
`JSV.BuildError` as `build/2` does.

# `default_format_validator_modules`

```elixir
@spec default_format_validator_modules() :: [module()]
```

Returns the list of format validator modules that are used when a schema is
built with format validation enabled and the `:formats` option to `build/2` is
`true`.

# `default_meta`

```elixir
@spec default_meta() :: binary()
```

Returns the default meta schema used when the `:default_meta` option is not
set in `build/2`.

Currently returns "https://json-schema.org/draft/2020-12/schema".

# `error_schema`

```elixir
@spec error_schema() :: module()
```

Returns the schema representing errors returned by `normalize_error/1`.

Because errors can be nested, the schema is recursive, so this function
returns a module based schema (a module name).

# `normalize_error`

```elixir
@spec normalize_error(
  JSV.ValidationError.t() | JSV.Validator.context() | [JSV.Validator.Error.t()],
  keyword()
) :: map()
```

Returns a JSON compatible represenation of a `JSV.ValidationError` struct.

See `JSV.ErrorFormatter.normalize_error/2` for options.

When used without the `:atoms` keys option, a normalized error will correspond
to the JSON schema returned by `error_schema/0`.

# `resolver_chain`

```elixir
@spec resolver_chain(
  resolvers :: module() | {module(), term()} | [{module(), term()}]
) :: [
  {module(), term()}
]
```

Normalizes a resolver implementation to a list of `{module, options}` and
appends the default resolvers if they are not already present in the list.

### Examples

    iex> JSV.resolver_chain(MyModule)
    [{MyModule, []}, {JSV.Resolver.Embedded, []}, {JSV.Resolver.Internal, []}]

    iex> JSV.resolver_chain([JSV.Resolver.Embedded, MyModule])
    [{JSV.Resolver.Embedded, []}, {MyModule, []}, {JSV.Resolver.Internal, []}]

    iex> JSV.resolver_chain([{JSV.Resolver.Embedded, []}, {MyModule, %{foo: :bar}}])
    [{JSV.Resolver.Embedded, []}, {MyModule, %{foo: :bar}}, {JSV.Resolver.Internal, []}]

# `validate`

```elixir
@spec validate(term(), JSV.Root.t(), [validate_opt()]) ::
  {:ok, term()} | {:error, Exception.t()}
```

Validates and casts the data with the given schema. The schema must be a
`JSV.Root` struct generated with `build/2`.

> #### This function returns cast data {: .info}
>
>
> * If the `:cast_formats` option is enabled, string values may be transformed
>   in other data structures. Refer to the "Formats" section of the
>   [Validation guide](validation-basics.html#formats) for more information.
> * The JSON Schema specification states that `123.0` is a valid integer. This
>   function will return `123` instead. This may return invalid data for
>   floats with very large integer parts. As always when dealing with JSON and
>   big decimal or extremely precise numbers, use strings.

### Options

* `:cast` (`t:boolean/0`) - Enables calling generic cast functions on validation.

  This is based on the `jsv-cast` JSON Schema custom keyword
  and is typically used by `defschema/1`.

  While it is on by default, some specific casting features are enabled
  separately, see option `:cast_formats`.

  The default value is `true`.

* `:cast_formats` (`t:boolean/0`) - When enabled, format validators will return casted values,
  for instance a `Date` struct instead of the date as string.

  It has no effect when the schema was not built with formats enabled.

  The default value is `false`.

* `:key` (`t:term/0`) - When specified, the validation will start in the schema at the given key
  instead of using the root schema.

  The key must have been built and returned by `build_key!/2`. The validation
  does not accept to validate any Ref or pointer in the schema.

  This is useful when validating with a JSON document that contains schemas but
  is not itself a schema.

# `validate!`

```elixir
@spec validate!(term(), JSV.Root.t(), keyword()) :: term()
```

# `defcast`
*macro* 

Enables a casting function in the current module, identified by its function
name.

### Example

```elixir
defmodule MyApp.Cast do
  use JSV.Schema

  defcast :to_integer

  defp to_integer(data) when is_binary(data) do
    case Integer.parse(data) do
      {int, ""} -> {:ok, int}
      _ -> {:error, "invalid"}
    end
  end

  defp to_integer(_) do
    {:error, "invalid"}
  end
end
```

    iex> schema = JSV.Schema.string() |> JSV.Schema.with_cast(["Elixir.MyApp.Cast", "to_integer"])
    iex> root = JSV.build!(schema)
    iex> JSV.validate("1234", root)
    {:ok, 1234}

See `defcast/3` for more information.

# `defcast`
*macro* 

Enables a casting function in the current module, identified by a custom tag.

### Example

```elixir
defmodule MyApp.Cast do
  use JSV.Schema

  defcast "to_integer_if_string", :to_integer

  defp to_integer(data) when is_binary(data) do
    case Integer.parse(data) do
      {int, ""} -> {:ok, int}
      _ -> {:error, "invalid"}
    end
  end

  defp to_integer(_) do
    {:error, "invalid"}
  end
end
```

    iex> schema = JSV.Schema.string() |> JSV.Schema.with_cast(["Elixir.MyApp.Cast", "to_integer_if_string"])
    iex> root = JSV.build!(schema)
    iex> JSV.validate("1234", root)
    {:ok, 1234}

See `defcast/3` for more information.

# `defcast`
*macro* 

Defines a casting function in the calling module, and enables it for casting
data during validation.

See the [custom cast functions guide](cast-functions.html) to learn more about
defining your own cast functions.

This documentation assumes the following module is defined. Note that
`JSV.Schema` provides several [predefined cast
functions](JSV.Schema.html#schema-casters), including an [existing atom
cast](JSV.Schema.html#string_to_existing_atom/0).

```elixir
defmodule MyApp.Cast do
  use JSV.Schema

  defcast to_existing_atom(data) do
    {:ok, String.to_existing_atom(data)}
  rescue
    ArgumentError -> {:error, "bad atom"}
  end

  def accepts_anything(data) do
    {:ok, data}
  end
end
```

This macro will define the `to_existing_atom/1` function in the calling
module, and enable it to be referenced in the `jsv-cast` schema custom
keyword.

    iex> MyApp.Cast.to_existing_atom("erlang")
    {:ok, :erlang}

    iex> MyApp.Cast.to_existing_atom("not an existing atom")
    {:error, "bad atom"}

It will also define a zero arity function to get the cast information ready to
be included in a schema:

    iex> MyApp.Cast.to_existing_atom()
    ["Elixir.MyApp.Cast", "to_existing_atom"]

This is accepted by `JSV.Schema.with_cast/2`:

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

With a `jsv-cast` property defined in a schema, data will be cast when the
schema is validated:

    iex> schema = JSV.Schema.string() |> JSV.Schema.with_cast(MyApp.Cast.to_existing_atom())
    iex> root = JSV.build!(schema)
    iex> JSV.validate("noreply", root)
    {:ok, :noreply}

    iex> schema = JSV.Schema.string() |> JSV.Schema.with_cast(MyApp.Cast.to_existing_atom())
    iex> root = JSV.build!(schema)
    iex> {:error, %JSV.ValidationError{}} = JSV.validate(["Elixir.NonExisting"], root)

It is not mandatory to use the schema definition helpers. Raw schemas can
contain cast pointers too:

    iex> schema = %{
    ...>   "type" => "string",
    ...>   "jsv-cast" => ["Elixir.MyApp.Cast", "to_existing_atom"]
    ...> }
    iex> root = JSV.build!(schema)
    iex> JSV.validate("noreply", root)
    {:ok, :noreply}

Note that for security reasons the cast pointer does not allow to call any
function from the schema definition. A cast function MUST be enabled by
`defcast/1`, `defcast/2` or `defcast/3`.

The `MyApp.Cast` example module above defines a `accepts_anything/1` function,
but the following schema will fail:

    iex> schema = %{
    ...>   "type" => "string",
    ...>   "jsv-cast" => ["Elixir.MyApp.Cast", "accepts_anything"]
    ...> }
    iex> root = JSV.build!(schema)
    iex> {:error, %JSV.ValidationError{errors: [%JSV.Validator.Error{kind: :"bad-cast"}]}} = JSV.validate("anything", root)

Finally, you can customize the name present in the `jsv-cast` property by
using a custom tag:

```elixir
defcast "my_custom_tag", a_function_name(data) do
  # ...
end
```

Make sure to read the [custom cast functions guide](cast-functions.html)!

# `defschema`
*macro* 

Defines a struct in the calling module where the struct keys are the
properties of the schema.

The given schema must define the `type` keyword as `object` and must define a
`properties` map. That map can be empty to define a struct without any key.
Properties keys must be given as atoms.

If a default value is given in a property schema, it will be used as the
default value for the corresponding struct key. Otherwise, the default value
will be `nil`. A default value is _not_ validated against the property schema
itself.

    defmodule MyApp.UserSchema do
      import JSV

      defschema %{
        type: :object,
        properties: %{
          name: %{type: :string, default: ""},
          age: %{type: :integer, default: 123}
        }
      }
    end

    iex> %MyApp.UserSchema{}
    %MyApp.UserSchema{name: "", age: 123}

    iex> {:ok, root} = JSV.build(MyApp.UserSchema)
    iex> JSV.validate(%{"name" => "Alice"}, root)
    {:ok, %MyApp.UserSchema{name: "Alice", age: 123}}

The `required` keyword is supported and must use atom keys as well.

    defmodule MyApp.WithRequired do
      import JSV

      defschema %{
        type: :object,
        properties: %{
          name: %{type: :string},
          age: %{type: :integer, default: 123}
        },
        required: [:name]
      }
    end

    iex> %MyApp.WithRequired{name: "Alice"}
    %MyApp.WithRequired{name: "Alice", age: 123}

### Property List Syntax

Alternatively, you can use a keyword list to define the properties where each
property is defined as `{key, schema}`. The following rules apply:

- All properties without a `default` value are automatically marked as
  required and are enforced at the struct level.
- The resulting schema will have `type: :object` set automatically.
- The `title` of the schema is set as the last segment of the module name.

This provides a more concise way to define simple object schemas.

    defmodule MyApp.UserKW do
      use JSV.Schema

      defschema name: string(default: ""),
                age: integer(default: 123)
    end

    iex> %MyApp.UserKW{}
    %MyApp.UserKW{name: "", age: 123}

### Additional properties

Additional properties are allowed by default.

If your schema does not define `additionalProperties: false`, the validation
will accept a map with additional properties, but the keys will not be added
to the resulting struct as it would make an invalid struct.

    iex> {:ok, root} = JSV.build(MyApp.UserSchema)
    iex> data = %{"name" => "Alice", "extra" => "hello!"}
    iex> JSV.validate(data, root)
    {:ok, %MyApp.UserSchema{name: "Alice", age: 123}}

If the `cast: false` option is given to `JSV.validate/3`, structs will not be
created. In that case, the additional properties will be kept.

    iex> {:ok, root} = JSV.build(MyApp.UserSchema)
    iex> data = %{"name" => "Alice", "extra" => "hello!"}
    iex> JSV.validate(data, root, cast: false)
    {:ok, %{"name" => "Alice", "extra" => "hello!"}}

It is also possible to collect additional properties in a new struct key by
defining the `@additional_properties` attribute above the `defschema`
expression. This property will have a default value of `%{}` (the empty map).

    defmodule MyApp.UserSchemaWithAdds do
      import JSV

      @additional_properties :adds
      defschema %{
        type: :object,
        properties: %{
          name: %{type: :string, default: ""},
          age: %{type: :integer, default: 123}
        }
      }
    end

    iex> {:ok, root} = JSV.build(MyApp.UserSchemaWithAdds)
    iex> data = %{"name" => "Alice", "extra" => "hello!"}
    iex> JSV.validate(data, root)
    {:ok, %MyApp.UserSchemaWithAdds{name: "Alice", age: 123, adds: %{"extra" => "hello!"}}}

### Ignoring struct keys

Some keys can be defined in the schema but not included in the struct by using
the `@def` module attribute. This is helpful when a property uses
`const` but your code rather depends on the struct type.

Keys listed in `@skip_keys` will still be validated according to the schema!

    defmodule MyApp.UserEvent do
      use JSV.Schema

      @skip_keys [:message_type]
      defschema message_type: const("user_event"),
                user_id: integer(),
                event: string()
    end

    iex> {:ok, root} = JSV.build(MyApp.UserEvent)
    iex> data = %{"message_type" => "user_event", "user_id" => 123, "event" => "login"}
    iex> {:ok, result} = JSV.validate(data, root)
    iex> result
    %MyApp.UserEvent{user_id: 123, event: "login"}

### Module references

A module can reference another module in its properties.

    defmodule MyApp.CompanySchema do
      import JSV

      defschema %{
        type: :object,
        properties: %{
          name: %{type: :string},
          owner: MyApp.UserSchema
        }
      }
    end

    iex> root = JSV.build!(MyApp.CompanySchema)
    iex> data = %{"name" => "Schemas Inc.", "owner" => %{"name" => "Alice", "age" => 999}}
    iex> JSV.validate(data, root)
    {:ok, %MyApp.CompanySchema{
      name: "Schemas Inc.",
      owner: %MyApp.UserSchema{
        name: "Alice",
        age: 999
      }
    }}

# `defschema`
*macro* 

Defines a new module with a JSON Schema struct.

This macro is similar to `defschema/1` but it also takes a module name and
defines a nested module in the context where it is called. An optional
description can be given, used as the `@moduledoc` and the description when a
keyword list of properties is given.

The module's struct will automatically `@derive` `Jason.Encoder` and
`JSON.Encoder` if those modules are found during compilation.

### Title and Description Behavior

When passing properties as a keyword list instead of a schema, the `title` and
`description` parameters are automatically applied to the generated schema:

- `title` is set from the module name (without outer module prefix if any)
- `description` is set from the description parameter

When passing a full schema map, the title and description from the parameters
are not applied - the schema map is used as-is. Only the `description`
parameter is used as the module's `@moduledoc`.

### Examples

Basic module definition with keyword list:

    defschema User,
      name: string(),
      age: integer(default: 0)

Module with description using keyword list:

    defschema User,
              "A user in the system",
              name: string(),
              age: integer(default: 0)

Module with full schema map:

    defschema User,
              "User schema",
              %{
                type: :object,
                title: "Custom Title",
                description: "Custom Desc",
                properties: %{
                  name: %{type: :string},
                  age: %{type: :integer, default: 18}
                },
                required: [:name]
              }

## Usage

The created module can be used like any struct:

    %User{name: "Alice", age: 25}

And as a JSON Schema for validation:

    {:ok, root} = JSV.build(User)
    JSV.validate(%{"name" => "Bob"}, root)
    #=> {:ok, %User{name: "Bob", age: 0}}

## Module References

Modules can reference other modules in their properties:

    defschema Address,
              street: string(),
              city: string()

    defschema User,
              name: string(),
              address: Address

Use `__MODULE__` for self-references:

    defschema Category,
              name: string(),
              parent: optional(__MODULE__)

## Inherited Module Attributes

This macro reads `@skip_keys` and `@additional_properties` from the caller
module and applies them to the generated nested module.

Those attributes are consumed from the caller when `defschema/3` is expanded.
If you define multiple schemas with `defschema/3` in the same parent module,
you must redeclare those attributes before each schema that needs them.

    defmodule Parent do
      use JSV.Schema

      @skip_keys [:kind]
      @additional_properties :ext
      defschema User,
                name: string(),
                kind: const("user")

      # Attributes above were consumed by the previous defschema/3 call.
      # Redeclare them if they should apply to this schema too.
      @skip_keys [:kind]
      @additional_properties :ext
      defschema Team,
                name: string(),
                kind: const("team")
    end

# `build_add`

```elixir
@spec build_add(build_context(), native_schema()) ::
  {:ok, JSV.Key.t(), normal_schema(), build_context()} | {:error, Exception.t()}
```

Same as `build_add!/2` but rescues
errors and returns a result tuple.

# `build_add!`

```elixir
@spec build_add!(build_context(), native_schema()) ::
  {JSV.Key.t(), normal_schema(), build_context()}
```

Adds a schema to the build context.

# `build_init`

```elixir
@spec build_init([build_opt()]) :: {:ok, build_context()} | {:error, Exception.t()}
```

Same as `build_init!/1` but rescues
errors and returns a result tuple.

# `build_init!`

```elixir
@spec build_init!([build_opt()]) :: build_context()
```

Initializes a build context for controlled builds.

See `build/2` for options.

# `build_key`

```elixir
@spec build_key(build_context(), JSV.Ref.ns() | JSV.Ref.t()) ::
  {:ok, JSV.Key.t(), build_context()} | {:error, Exception.t()}
```

Same as `build_key!/2` but rescues
errors and returns a result tuple.

# `build_key!`

```elixir
@spec build_key!(build_context(), JSV.Ref.ns() | JSV.Ref.t()) ::
  {JSV.Key.t(), build_context()}
```

Builds the given reference or root schema.

Returns the build context as well as a key, which is a pointer to the built
schema.

# `to_root`

```elixir
@spec to_root(build_context(), JSV.Key.t()) ::
  {:ok, JSV.Root.t()} | {:error, Exception.t()}
```

Same as `to_root!/2` but rescues
errors and returns a result tuple.

# `to_root!`

```elixir
@spec to_root!(build_context(), JSV.Key.t()) :: JSV.Root.t()
```

Returns a root with all the validators from the build context and the given
`root_key`. That key is used as the default entrypoint for validation when no
`:key` option is passed to `validate/2`.

---

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