JSV.Schema (jsv v0.8.1)

View Source

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])

Summary

Schema Definition Utilities

Defines or merges into a JSON Schema with allOf: schemas..

Defines or merges into a JSON Schema with anyOf: schemas..

Defines or merges into a JSON Schema with type: :array and items: item_schema..

Defines or merges into a JSON Schema with type: :boolean..

Defines or merges into a JSON Schema with type: :string and format: :date..

Defines or merges into a JSON Schema with type: :string and format: :"date-time"..

Defines or merges into a JSON Schema with type: :string and format: :email..

Defines or merges into a JSON Schema with format: format.

Defines or merges into a JSON Schema with type: :integer..

Defines or merges into a JSON Schema with items: item_schema.

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

Defines or merges into a JSON Schema with type: :integer and maximum: -1..

Defines or merges into a JSON Schema with type: :string and minLength: 1..

Defines or merges into a JSON Schema with type: :integer and minimum: 0..

Defines or merges into a JSON Schema with type: :number..

Defines or merges into a JSON Schema with type: :object.

Defines or merges into a JSON Schema with oneOf: schemas..

Defines or merges into a JSON Schema with type: :integer and minimum: 1..

Defines or merges into a JSON Schema with properties: properties.

Defines or merges into a JSON Schema with type: :object and properties: properties..

Defines or merges into a JSON Schema with $ref: ref.

Defines a JSON Schema with required: keys or adds the given keys if the base schema already has a :required definition.

Defines or merges into a JSON Schema with type: :string..

Defines or merges into a JSON Schema with type: :string and format: format..

Defines or merges into a JSON Schema with type: :string and format: :uri..

Defines or merges into a JSON Schema with type: :string and format: :uuid..

Schema Casts

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

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_atom"]..

Defines or merges into a JSON Schema with type: :string, enum: enum and jsv-cast: ["Elixir.JSV.Cast", "string_to_atom"].

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_boolean"]..

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_existing_atom"]..

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_float"]..

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_integer"]..

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_number"]..

Functions

Returns a new empty schema.

Returns a new schema with the given key/values.

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

Alias for merge/2.

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

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

Schema Definition Utilities

all_of(base \\ nil, schemas)

@spec all_of(base(), [schema()]) :: schema()

Defines or merges into a JSON Schema with allOf: schemas..

any_of(base \\ nil, schemas)

@spec any_of(base(), [schema()]) :: schema()

Defines or merges into a JSON Schema with anyOf: schemas..

array_of(base \\ nil, item_schema)

@spec array_of(base(), schema()) :: schema()

Defines or merges into a JSON Schema with type: :array and items: item_schema..

boolean(base \\ nil)

@spec boolean(base()) :: schema()

Defines or merges into a JSON Schema with type: :boolean..

date(base \\ nil)

@spec date(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and format: :date..

datetime(base \\ nil)

@spec datetime(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and format: :"date-time"..

email(base \\ nil)

@spec email(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and format: :email..

format(base \\ nil, format)

@spec format(base(), term()) :: schema()

Defines or merges into a JSON Schema with format: format.

Does not set the type: :string on the schema. Use string_of/2 for a shortcut. .

integer(base \\ nil)

@spec integer(base()) :: schema()

Defines or merges into a JSON Schema with type: :integer..

items(base \\ nil, item_schema)

@spec items(base(), schema()) :: schema()

Defines or merges into a JSON Schema with items: item_schema.

Does not set the type: :array on the schema. Use array_of/2 for a shortcut. .

merge(base, values)

@spec merge(base(), overrides()) :: schema()

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

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}

neg_integer(base \\ nil)

@spec neg_integer(base()) :: schema()

Defines or merges into a JSON Schema with type: :integer and maximum: -1..

non_empty_string(base \\ nil)

@spec non_empty_string(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and minLength: 1..

non_neg_integer(base \\ nil)

@spec non_neg_integer(base()) :: schema()

Defines or merges into a JSON Schema with type: :integer and minimum: 0..

number(base \\ nil)

@spec number(base()) :: schema()

Defines or merges into a JSON Schema with type: :number..

object(base \\ nil)

@spec object(base()) :: schema()

Defines or merges into a JSON Schema with type: :object.

See props/2 to define the properties as well. .

one_of(base \\ nil, schemas)

@spec one_of(base(), [schema()]) :: schema()

Defines or merges into a JSON Schema with oneOf: schemas..

pos_integer(base \\ nil)

@spec pos_integer(base()) :: schema()

Defines or merges into a JSON Schema with type: :integer and minimum: 1..

properties(base \\ nil, properties)

@spec properties(
  base(),
  properties()
) :: schema()

Defines or merges into a JSON Schema with properties: properties.

Does not set the type: :object on the schema. Use props/2 for a shortcut. .

props(base \\ nil, properties)

@spec props(
  base(),
  properties()
) :: schema()

Defines or merges into a JSON Schema with type: :object and properties: properties..

ref(base \\ nil, ref)

@spec ref(base(), String.t()) :: schema()

Defines or merges into a JSON Schema with $ref: ref.

A struct-based schema module name is not a valid reference. Modules should be passed directly where a schema (and not a $ref) is expected.

Example

For instance to define a user property, this is valid:

props(user: UserSchema)

The following is invalid:

# Do not do this
props(user: ref(UserSchema))

.

required(base \\ nil, key_or_keys)

@spec required(base(), [atom() | binary()]) :: t()

Defines a JSON Schema with required: keys or adds the given keys if the base schema already has a :required definition.

Existing required keys are preserved.

Examples

iex> JSV.Schema.required(%{}, [:a, :b])
%{required: [:a, :b]}

iex> JSV.Schema.required(%{required: nil}, [:a, :b])
%{required: [:a, :b]}

iex> JSV.Schema.required(%{required: [:c]}, [:a, :b])
%{required: [:a, :b, :c]}

iex> JSV.Schema.required(%{required: [:a]}, [:a])
%{required: [:a, :a]}

Use merge/2 to replace existing required keys.

iex> JSV.Schema.merge(%{required: [:a, :b, :c]}, required: [:x, :y, :z])
%{required: [:x, :y, :z]}

string(base \\ nil)

@spec string(base()) :: schema()

Defines or merges into a JSON Schema with type: :string..

string_of(base \\ nil, format)

@spec string_of(base(), term()) :: schema()

Defines or merges into a JSON Schema with type: :string and format: format..

uri(base \\ nil)

@spec uri(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and format: :uri..

uuid(base \\ nil)

@spec uuid(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and format: :uuid..

Schema Casts

cast(base \\ nil, mod_tag)

@spec cast(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.cast([MyApp.Cast, :a_cast_function])
%JSV.Schema{"jsv-cast": ["Elixir.MyApp.Cast", "a_cast_function"]}

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

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

string_to_atom(base \\ nil)

@spec string_to_atom(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_atom"]..

string_to_atom_enum(base \\ nil, enum)

@spec string_to_atom_enum(base(), [atom()]) :: schema()

Defines or merges into a JSON Schema with type: :string, enum: enum and jsv-cast: ["Elixir.JSV.Cast", "string_to_atom"].

Accepts a list of atoms and validates that a given value is a string representation of one of the given atoms.

On validation, a cast will be made to return the original atom value.

This is useful when dealing with enums that are represented as atoms in the codebase, such as Oban job statuses or other Ecto enum types.

iex> schema = JSV.Schema.props(status: JSV.Schema.string_to_atom_enum([:executing, :pending]))
iex> root = JSV.build!(schema)
iex> JSV.validate(%{"status" => "pending"}, root)
{:ok, %{"status" => :pending}}

Does not support nil

This function sets the string type on the schema. If nil is given in the enum, the corresponding valid JSON value will be the "nil" string rather than null .

string_to_boolean(base \\ nil)

@spec string_to_boolean(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_boolean"]..

string_to_existing_atom(base \\ nil)

@spec string_to_existing_atom(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_existing_atom"]..

string_to_float(base \\ nil)

@spec string_to_float(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_float"]..

string_to_integer(base \\ nil)

@spec string_to_integer(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_integer"]..

string_to_number(base \\ nil)

@spec string_to_number(base()) :: schema()

Defines or merges into a JSON Schema with type: :string and jsv-cast: ["Elixir.JSV.Cast", "string_to_number"]..

Types

base()

@type base() :: map() | [{atom() | binary(), term()}] | struct() | nil

overrides()

@type overrides() :: map() | [{atom() | binary(), term()}]

properties()

@type properties() ::
  [{property_key(), schema()}] | %{optional(property_key()) => schema()}

property_key()

@type property_key() :: atom() | binary()

schema()

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

schema_data()

@type schema_data() ::
  %{optional(binary()) => schema_data()}
  | [schema_data()]
  | number()
  | binary()
  | boolean()
  | nil

t()

@type t() :: %JSV.Schema{
  "$anchor": term(),
  "$comment": term(),
  "$defs": term(),
  "$dynamicAnchor": term(),
  "$dynamicRef": term(),
  "$id": term(),
  "$ref": term(),
  "$schema": 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(),
  "jsv-cast": 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()
}

Functions

new()

@spec new() :: t()

Returns a new empty schema.

new(schema)

@spec new(t() | overrides()) :: t()

Returns a new schema with the given key/values.

normalize(term)

@spec normalize(term()) ::
  %{optional(binary()) => 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 schema/0 function.

Examples

defmodule Elixir.ASchemaExportingModule do
  def 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"

override(base, values)

This function is deprecated. Use `merge/2`.
@spec override(base(), overrides()) :: schema()

Alias for merge/2.

schema_module?(module)

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

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

to_map(schema)

@spec to_map(t()) :: %{optional(atom()) => term()}

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