Packages
jsv
0.7.2
0.21.2
0.21.1
0.21.0
0.20.0
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.3
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.0
0.15.2
0.15.1
0.15.0
0.14.0
0.13.1
0.13.0
0.12.0
0.11.5
0.11.4
0.11.3
retired
0.11.2
0.11.1
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
A JSON Schema Validator with complete support for the latest specifications.
Current section
Files
Jump to
Current section
Files
lib/jsv/vocabulary/v202012/validation.ex
defmodule JSV.Vocabulary.V202012.Validation do
alias JSV.Codec
alias JSV.Helpers.Math
alias JSV.Validator
use JSV.Vocabulary, priority: 300
@moduledoc """
Implementation for the
`https://json-schema.org/draft/2020-12/vocab/validation` vocabulary.
"""
@impl true
def init_validators(_) do
[]
end
take_keyword :type, t, vds, builder, _ do
{:ok, [{:type, valid_type!(t)} | vds], builder}
end
take_keyword :maximum, maximum, acc, builder, _ do
take_number(:maximum, maximum, acc, builder)
end
take_keyword :exclusiveMaximum, exclusive_maximum, acc, builder, _ do
take_number(:exclusiveMaximum, exclusive_maximum, acc, builder)
end
take_keyword :minimum, minimum, acc, builder, _ do
take_number(:minimum, minimum, acc, builder)
end
take_keyword :exclusiveMinimum, exclusive_minimum, acc, builder, _ do
take_number(:exclusiveMinimum, exclusive_minimum, acc, builder)
end
take_keyword :minItems, min_items, acc, builder, _ do
take_integer(:minItems, min_items, acc, builder)
end
take_keyword :maxItems, max_items, acc, builder, _ do
take_integer(:maxItems, max_items, acc, builder)
end
take_keyword :required, required when is_list(required), acc, builder, _ do
{:ok, [{:required, required} | acc], builder}
end
take_keyword :multipleOf, zero when zero in [0, 0.0, -0.0], _acc, _builder, _ do
{:error, "mutipleOf zero is not allowed"}
end
take_keyword :multipleOf, multiple_of, acc, builder, _ do
take_number(:multipleOf, multiple_of, acc, builder)
end
take_keyword :const, const, acc, builder, _ do
{:ok, [{:const, const} | acc], builder}
end
take_keyword :maxLength, max_length, acc, builder, _ do
take_integer(:maxLength, max_length, acc, builder)
end
take_keyword :minLength, min_length, acc, builder, _ do
take_integer(:minLength, min_length, acc, builder)
end
take_keyword :minProperties, min_properties, acc, builder, _ do
take_integer(:minProperties, min_properties, acc, builder)
end
take_keyword :maxProperties, max_properties, acc, builder, _ do
take_integer(:maxProperties, max_properties, acc, builder)
end
take_keyword :enum, enum, acc, builder, _ do
{:ok, [{:enum, enum} | acc], builder}
end
take_keyword :pattern, pattern, acc, builder, _ do
case Regex.compile(pattern) do
{:ok, re} -> {:ok, [{:pattern, re} | acc], builder}
{:error, _} -> {:error, {:invalid_pattern, pattern}}
end
end
take_keyword :uniqueItems, unique?, acc, builder, _ do
if unique? do
{:ok, [{:uniqueItems, true} | acc], builder}
else
{:ok, acc, builder}
end
end
take_keyword :dependentRequired, dependent_required, acc, builder, _ do
{:ok, [{:dependentRequired, dependent_required} | acc], builder}
end
# minContains/maxContains is handled by the Applicator module IF the validation vocabulary is
# enabled
ignore_any_keyword()
# ---------------------------------------------------------------------------
@impl true
def finalize_validators([]) do
:ignore
end
def finalize_validators(list) do
list
end
# -----------------------------------------------------------------------------
defp valid_type!(list) when is_list(list) do
Enum.map(list, &valid_type!/1)
end
defp valid_type!("array") do
:array
end
defp valid_type!("object") do
:object
end
defp valid_type!("null") do
:null
end
defp valid_type!("boolean") do
:boolean
end
defp valid_type!("string") do
:string
end
defp valid_type!("integer") do
:integer
end
defp valid_type!("number") do
:number
end
@impl true
def validate(data, vds, vctx) do
Validator.reduce(vds, data, vctx, &validate_keyword/3)
end
@doc false
@spec validate_keyword({atom, Validator.validator()}, term, Validator.context()) :: Validator.result()
def validate_keyword({:type, ts}, data, vctx) when is_list(ts) do
found =
Enum.find_value(ts, fn t ->
case validate_type(data, t) do
true -> {:ok, data}
false -> nil
{:swap, new_data} -> {:ok, new_data}
end
end)
case found do
{:ok, data} -> {:ok, data, vctx}
nil -> {:error, Validator.with_error(vctx, :type, data, type: ts)}
end
end
def validate_keyword({:type, t}, data, vctx) do
case validate_type(data, t) do
true -> {:ok, data, vctx}
false -> {:error, Validator.with_error(vctx, :type, data, type: t)}
{:swap, new_data} -> {:ok, new_data, vctx}
end
end
def validate_keyword({:maximum, n}, data, vctx) when is_number(data) do
case data <= n do
true -> {:ok, data, vctx}
false -> {:error, Validator.with_error(vctx, :maximum, data, n: n)}
end
end
with_decimal do
def validate_keyword({:maximum, n}, %Decimal{} = data, vctx) do
case Decimal.lte?(data, to_decimal(n)) do
true -> {:ok, data, vctx}
false -> {:error, Validator.with_error(vctx, :maximum, data, n: n)}
end
end
end
pass validate_keyword({:maximum, _})
def validate_keyword({:exclusiveMaximum, n}, data, vctx) when is_number(data) do
case data < n do
true -> {:ok, data, vctx}
false -> {:error, Validator.with_error(vctx, :exclusiveMaximum, data, n: n)}
end
end
with_decimal do
def validate_keyword({:exclusiveMaximum, n}, %Decimal{} = data, vctx) do
case Decimal.lt?(data, to_decimal(n)) do
true -> {:ok, data, vctx}
false -> {:error, Validator.with_error(vctx, :exclusiveMaximum, data, n: n)}
end
end
end
pass validate_keyword({:exclusiveMaximum, _})
def validate_keyword({:minimum, n}, data, vctx) when is_number(data) do
case data >= n do
true -> {:ok, data, vctx}
false -> {:error, Validator.with_error(vctx, :minimum, data, n: n)}
end
end
with_decimal do
def validate_keyword({:minimum, n}, %Decimal{} = data, vctx) do
case Decimal.gte?(data, to_decimal(n)) do
true -> {:ok, data, vctx}
false -> {:error, Validator.with_error(vctx, :minimum, data, n: n)}
end
end
end
pass validate_keyword({:minimum, _})
def validate_keyword({:exclusiveMinimum, n}, data, vctx) when is_number(data) do
case data > n do
true -> {:ok, data, vctx}
false -> {:error, Validator.with_error(vctx, :exclusiveMinimum, data, n: n)}
end
end
with_decimal do
def validate_keyword({:exclusiveMinimum, n}, %Decimal{} = data, vctx) do
case Decimal.gt?(data, to_decimal(n)) do
true -> {:ok, data, vctx}
false -> {:error, Validator.with_error(vctx, :exclusiveMinimum, data, n: n)}
end
end
end
pass validate_keyword({:exclusiveMinimum, _})
def validate_keyword({:maxItems, max}, data, vctx) when is_list(data) do
len = length(data)
if len <= max do
{:ok, data, vctx}
else
{:error, Validator.with_error(vctx, :maxItems, data, maxItems: max, len: len)}
end
end
pass validate_keyword({:maxItems, _})
def validate_keyword({:minItems, min}, data, vctx) when is_list(data) do
len = length(data)
if len >= min do
{:ok, data, vctx}
else
{:error, Validator.with_error(vctx, :minItems, data, minItems: min, len: len)}
end
end
pass validate_keyword({:minItems, _})
def validate_keyword({:multipleOf, n}, data, vctx) when is_number(data) do
case Math.fractional_is_zero?(data / n) do
true -> {:ok, data, vctx}
false -> {:error, Validator.with_error(vctx, :multipleOf, data, multipleOf: n)}
end
rescue
# Rescue infinite division (huge numbers divided by float, too large invalid
# floats)
_ in ArithmeticError -> {:error, Validator.with_error(vctx, :arithmetic_error, data, context: "multipleOf")}
end
with_decimal do
def validate_keyword({:multipleOf, n}, %Decimal{} = data, vctx) do
data
|> Decimal.div(to_decimal(n))
|> Decimal.integer?()
|> case do
true -> {:ok, data, vctx}
false -> {:error, Validator.with_error(vctx, :multipleOf, data, multipleOf: n)}
end
end
end
pass validate_keyword({:multipleOf, _})
def validate_keyword({:required, required_keys}, data, vctx) when is_map(data) do
case required_keys -- Map.keys(data) do
[] -> {:ok, data, vctx}
missing -> {:error, Validator.with_error(vctx, :required, data, required: missing)}
end
end
pass validate_keyword({:required, _})
def validate_keyword({:dependentRequired, dependent_required}, data, vctx) do
validate_dependent_required(dependent_required, data, vctx)
end
def validate_keyword({:maxLength, max}, data, vctx) when is_binary(data) do
len = String.length(data)
if len <= max do
{:ok, data, vctx}
else
{:error, Validator.with_error(vctx, :maxLength, data, maxLength: max, len: len)}
end
end
pass validate_keyword({:maxLength, _})
def validate_keyword({:minLength, min}, data, vctx) when is_binary(data) do
len = String.length(data)
if len >= min do
{:ok, data, vctx}
else
{:error, Validator.with_error(vctx, :minLength, data, minLength: min, len: len)}
end
end
pass validate_keyword({:minLength, _})
def validate_keyword({:const, const}, data, vctx) do
# 1 == 1.0 should be true according to JSON Schema specs
if data == const do
{:ok, data, vctx}
else
{:error, Validator.with_error(vctx, :const, data, const: const)}
end
end
def validate_keyword({:enum, enum}, data, vctx) do
# validate 1 == 1.0 or 1.0 == 1
if Enum.any?(enum, &(&1 == data)) do
{:ok, data, vctx}
else
{:error, Validator.with_error(vctx, :enum, data, enum: enum)}
end
end
def validate_keyword({:pattern, re}, data, vctx) when is_binary(data) do
if Regex.match?(re, data) do
{:ok, data, vctx}
else
{:error, Validator.with_error(vctx, :pattern, data, pattern: re.source)}
end
end
pass validate_keyword({:pattern, _})
def validate_keyword({:uniqueItems, true}, data, vctx) when is_list(data) do
data
|> Enum.with_index()
|> Enum.reduce({[], %{}}, fn {item, index}, {duplicate_indices, seen} ->
case Map.fetch(seen, item) do
{:ok, seen_index} -> {[{index, seen_index} | duplicate_indices], seen}
:error -> {duplicate_indices, Map.put(seen, item, index)}
end
end)
|> case do
{[], _} -> {:ok, data, vctx}
{duplicates, _} -> {:error, Validator.with_error(vctx, :uniqueItems, data, duplicates: Map.new(duplicates))}
end
end
pass validate_keyword({:uniqueItems, true})
def validate_keyword({:minProperties, n}, data, vctx) when is_map(data) do
case map_size(data) do
size when size < n -> {:error, Validator.with_error(vctx, :minProperties, data, minProperties: n, size: size)}
_ -> {:ok, data, vctx}
end
end
pass validate_keyword({:minProperties, _})
def validate_keyword({:maxProperties, n}, data, vctx) when is_map(data) do
case map_size(data) do
size when size > n -> {:error, Validator.with_error(vctx, :maxProperties, data, maxProperties: n, size: size)}
_ -> {:ok, data, vctx}
end
end
pass validate_keyword({:maxProperties, _})
# ---------------------------------------------------------------------------
# Shared to support "dependencies" compatibility
@doc false
@spec validate_dependent_required(Validator.validator(), term, Validator.context()) :: Validator.result()
def validate_dependent_required(dependent_required, data, vctx) when is_map(data) do
all_keys = Map.keys(data)
Validator.reduce(dependent_required, data, vctx, fn
{parent_key, required_keys}, data, vctx when is_map_key(data, parent_key) ->
case required_keys -- all_keys do
[] ->
{:ok, data, vctx}
missing ->
{:error, Validator.with_error(vctx, :dependentRequired, data, parent: parent_key, missing: missing)}
end
{_, _}, data, vctx ->
{:ok, data, vctx}
end)
end
def validate_dependent_required(_other, data, vctx) do
{:ok, data, vctx}
end
defp validate_type(data, :array) do
is_list(data)
end
with_decimal do
defp validate_type(%Decimal{}, :object) do
false
end
end
defp validate_type(data, :object) do
is_map(data)
end
defp validate_type(data, :null) do
data === nil
end
defp validate_type(data, :boolean) do
is_boolean(data)
end
defp validate_type(data, :string) do
is_binary(data)
end
defp validate_type(data, :integer) when is_float(data) do
Math.fractional_is_zero?(data) && {:swap, trunc(data)}
end
defp validate_type(data, :integer) when is_integer(data) do
true
end
defp validate_type(data, :number) when is_number(data) do
true
end
with_decimal do
defp validate_type(%Decimal{} = data, :integer) do
if Decimal.integer?(data) do
{:swap, Decimal.to_integer(data)}
else
false
end
end
defp validate_type(%Decimal{}, :number) do
true
end
end
defp validate_type(_, t) when t in [:number, :integer] do
false
end
# ---------------------------------------------------------------------------
def format_error(:type, args, _) do
%{type: type} = args
types_format = type |> List.wrap() |> Enum.map_intersperse(" or ", &Atom.to_string/1)
"value is not of type #{types_format}"
end
def format_error(:minimum, %{n: n}, data) do
"value #{data} is lower than minimum #{n}"
end
def format_error(:exclusiveMinimum, %{n: n}, data) do
"value #{data} is not higher than exclusive minimum #{n}"
end
def format_error(:maximum, %{n: n}, data) do
"value #{data} is higher than maximum #{n}"
end
def format_error(:exclusiveMaximum, %{n: n}, data) do
"value #{data} is not lower than exclusive maximum #{n}"
end
def format_error(:minLength, %{len: len, minLength: min_length}, _data) do
"value length must be at least #{min_length} but is #{len}"
end
def format_error(:maxLength, %{len: len, maxLength: max_length}, _data) do
"value length must be at most #{max_length} but is #{len}"
end
def format_error(:const, %{const: const}, _data) do
"value should be #{Codec.encode!(const)}"
end
def format_error(:required, %{required: required}, _data) do
case required do
[single] -> "property #{quote_prop(single)} is required"
_ -> "properties #{required |> Enum.map("e_prop/1) |> verbose_list("and")} are required"
end
end
def format_error(:multipleOf, %{multipleOf: multiple_of}, data) do
"value #{data} is not a multiple of #{multiple_of}"
end
def format_error(:pattern, %{pattern: pattern}, _data) do
"value does not conform to pattern /#{pattern}/"
end
def format_error(:maxItems, %{len: len, maxItems: max_items}, _data) do
"value should have at most #{max_items} items, got #{len}"
end
def format_error(:minItems, %{len: len, minItems: min_items}, _data) do
"value should have at least #{min_items} items, got #{len}"
end
def format_error(:minProperties, %{size: size, minProperties: min_properties}, _data) do
"value must have at least #{min_properties} properties, got #{size}"
end
def format_error(:maxProperties, %{size: size, maxProperties: max_properties}, _data) do
"value must have at most #{max_properties} properties, got #{size}"
end
def format_error(:enum, %{enum: enum}, _data) do
"value must be one of the enum values: #{enum |> Enum.map(&inspect/1) |> verbose_list("or")}"
end
def format_error(:dependentRequired, %{parent: parent, missing: missing}, _data) do
case missing do
[single] ->
"property #{quote_prop(single)} is required when property #{quote_prop(parent)} is present"
_ ->
"properties #{missing |> Enum.map("e_prop/1) |> verbose_list("and")} are required when property #{quote_prop(parent)} is present"
end
end
def format_error(:uniqueItems, %{duplicates: duplicates}, _data) do
printout =
Enum.map(duplicates, fn {dup_index, seen_index} ->
"values at indices #{seen_index} and #{dup_index} are equal"
end)
"value must contain unique items but #{verbose_list(printout, "and")}"
end
@impl true
def format_error(:arithmetic_error, %{context: context}, data) do
"could not valiade #{inspect(data)}, got arithmetic error in context #{quote_prop(context)}"
end
defp verbose_list([single], _) do
single
end
defp verbose_list([_ | _] = list, operator) do
[last | [_ | _] = rest] = :lists.reverse(list)
rest = :lists.reverse(rest)
[Enum.intersperse(rest, ", "), " ", operator, " ", last]
end
defp quote_prop(val) do
["'", val, "'"]
end
end