JSV (jsv v0.7.1)

View Source

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. Schemas can be defined in Elixir code, read from files, fetched remotely, etc.
  2. Build a validation root with build/2 or build!/2.
  3. Validate the data.

Example

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

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.

Summary

Functions

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

Same as build/2 but raises on error.

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.

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

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

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

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

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

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

Types

raw_schema()

@type raw_schema() :: map() | boolean() | module()

Functions

build(raw_schema, opts \\ [])

@spec build(
  raw_schema(),
  keyword()
) :: {: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 (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 - Formats are validated according to the meta-schema vocabulary.
    • true - Enforces validation with the default validator modules.
    • false - Disables all format validation.
    • [Module1, Module2,...] – set those modules as validators. Disables the default format validator modules. The default validators can be included back in the list manually, see default_format_validator_modules/0.

    Formats are disabled by the default meta-schema

    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.

    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. In the standard Draft 2020-12 meta-schema, these URIs 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, opt: "hello"}
    }

    The default value is %{}.

build!(raw_schema, opts \\ [])

@spec build!(
  raw_schema(),
  keyword()
) :: JSV.Root.t()

Same as build/2 but raises on error.

default_format_validator_modules()

@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()

@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".

defcast(local_fun)

(macro)

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

Example

defmodule MyApp.Cast do
  import JSV

  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.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(tag, local_fun)

(macro)

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

Example

defmodule MyApp.Cast do
  import JSV

  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.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(tag, fun, block)

(macro)

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

See the custom cast functions guide 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, including an existing atom cast.

defmodule MyApp.Cast do
  import JSV

  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.cast/2:

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

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

iex> schema = JSV.Schema.string() |> JSV.Schema.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.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:

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

Make sure to read the custom cast functions guide!

defschema(schema)

(macro)

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

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.

The $id property of the schema will automatically be set, if not present, to "jsv:module:" <> Atom.to_string(__MODULE__). Because of this, module based schemas must avoid using relative references to a parent schema as the references will resolve to that generated $id.

Additional properties

Additional properties are allowed.

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 be invalid.

If the cast: false option is given to JSV.validate/3, the additional properties will be kept.

Example

Given the following module definition:

defmodule MyApp.UserSchema do
  require JSV

  JSV.defschema(%{
    type: :object,
    properties: %{
      name: %{type: :string, default: ""},
      age: %{type: :integer, default: 0}
    }
  })
end

We can get the struct with default values:

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

And we can use the module as a schema:

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

Additional properties are ignored:

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

Disabling struct casting with additional properties:

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!"}}

A module can reference another module:

defmodule MyApp.CompanySchema do
  require JSV

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

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

normalize_error(error)

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

validate(data, root, opts \\ [])

@spec validate(term(), JSV.Root.t(), keyword()) ::
  {: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

  • 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 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 (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 (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.