JSV.Schema (jsv v0.5.0)

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. This is not the case with other strucs or maps.

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"},
    age: %Schema{type: :integer, description: "the age of the user"}
  },
  required: [:name, :age]
}

One can write:

%Schema{}
|> Schema.props(
  name: Schema.string(description: "the name of the user"),
  age: Schema.integer(description: "the age of the user")
)
|> Schema.required([:name, :age])

Summary

Functions

Returns a schema with type: :boolean.

Returns a schema with type: :integer.

Returns a schema with type: :array and items: item_schema.

Returns a new JSV.Schema struct with the given key/values.

Returns the given term with all atoms converted to binaries except for special cases.

Returns a schema with type: :number.

Returns a schema with type: :object.

Updates the given JSV.Schema struct with the given key/values.

Returns a schema with the type: :object and the given properties.

Returns a schema with "$ref": ref.

Adds the given key or keys in the base schema :required property. Previous values are preserved.

Returns a schema with type: :string.

Returns the given schema as a map without keys containing a nil value.

Types

base()

@type base() :: prototype() | nil

prototype()

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

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-struct": 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

boolean(base \\ nil)

@spec boolean(base()) :: t()

Returns a schema with type: :boolean.

integer(base \\ nil)

@spec integer(base()) :: t()

Returns a schema with type: :integer.

items(base \\ nil, item_schema)

@spec items(base(), map() | boolean()) :: t()

Returns a schema with type: :array and items: item_schema.

new(schema)

@spec new(prototype()) :: t()

Returns a new JSV.Schema struct with the given key/values.

normalize(term)

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

Returns the given term with all atoms converted to binaries except for special cases.

Note that this function accepts any data and not actually a %JSV.Schema{} or a raw schema.

  • JSV.Schema structs pairs where the value is nil will be completely removed. %JSV.Schema{type: :object} becomes %{"type" => "object"} whereas the struct contains many more keys.
  • Structs will be simply converted to map with Map.from_struct/1, not JSON encoder protocol will be used.
  • true, false and nil will be kept as-is in all places except map keys.
  • true, false and nil as map keys will be converted to string.
  • Other atoms will be checked to see if they correspond to a module name that exports a schema/0 function.

In any case, the resulting function will alway contain no atom other than true, false or nil.

Examples

iex> JSV.Schema.normalize(%JSV.Schema{title: :"My Schema"})
%{"title" => "My Schema"}

iex> JSV.Schema.normalize(%{name: :joe})
%{"name" => "joe"}

iex> JSV.Schema.normalize(%{"name" => :joe})
%{"name" => "joe"}

iex> JSV.Schema.normalize(%{"name" => "joe"})
%{"name" => "joe"}

iex> JSV.Schema.normalize(%{true: false})
%{"true" => false}

iex> JSV.Schema.normalize(%{specials: [true, false, nil]})
%{"specials" => [true, false, nil]}

iex> map_size(JSV.Schema.normalize(%JSV.Schema{}))
0

iex> JSV.Schema.normalize(1..10)
%{"first" => 1, "last" => 10, "step" => 1}

iex> defmodule :some_module_with_schema do
iex>   def schema, do: %{}
iex> end
iex> JSV.Schema.normalize(:some_module_with_schema)
%{"$ref" => "jsv:module:some_module_with_schema"}

iex> defmodule :some_module_without_schema do
iex>   def hello, do: "world"
iex> end
iex> JSV.Schema.normalize(:some_module_without_schema)
"some_module_without_schema"

number(base \\ nil)

@spec number(base()) :: t()

Returns a schema with type: :number.

object(base \\ nil)

@spec object(base()) :: t()

Returns a schema with type: :object.

See props/2 to define the properties as well.

override(base, overrides)

@spec override(base(), prototype()) :: t()

Updates the given JSV.Schema struct with the given key/values.

Accepts nil as the base schema, which is equivalent to new(overrides).

props(base \\ nil, properties)

@spec props(base(), map() | [{atom() | binary(), term()}]) :: t()

Returns a schema with the type: :object and the given properties.

ref(base \\ nil, ref)

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

Returns a schema with "$ref": ref.

required(base \\ nil, key_or_keys)

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

Adds the given key or keys in the base schema :required property. Previous values are preserved.

string(base \\ nil)

@spec string(base()) :: t()

Returns a schema with type: :string.

to_map(schema)

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

Returns the given schema as a map without keys containing a nil value.