Building Schemas
View SourceTo validate data with schemas, JSV
turns the schemas into a custom data
structure made specifically for validation.
It does not validate data with raw schemas directly. That would be too slow and
would not work properly with advanced features of Draft 2020-12, such as
$dynamicRef
and $dynamicAnchor
.
Instead, raw schemas are processed by a set of different "vocabulary" modules
that are each specialized in some part of the validation. The result of this
processing is then collected as a JSV.Root
struct.
This guide covers the configuration and customization of this process to better serve your needs.
The build functions
The main build function is JSV.build/2
. It accepts a raw schema and a set of
options and returns the root.
There are variations around that function, which is very common in Elixir:
JSV.build/1
with default options, JSV.build!/1
, and JSV.build!/2
with or
without default options that raise errors instead of returning an error tuple.
Custom build modules
The build functions do not use macros or process-based techniques. We encourage you to wrap them and define your options in a single place:
defmodule MyApp.SchemaBuilder do
def build(raw_schema) do
JSV.build(raw_schema, build_opts())
end
def build!(raw_schema) do
JSV.build!(raw_schema, build_opts())
end
defp build_opts do
[resolver: MyApp.CustomSchemaResolver, formats: true]
end
end
Compile-time builds
Validation roots can be built at runtime, but it is recommended to build them during compilation, if possible, to avoid repeating the build step unnecessarily.
Building at runtime should be done when the JSON schema is not available during compilation.
For instance, if we have this function that should validate external data:
# DO NOT DO THIS
defp order_schema do
"priv/schemas/order.schema.json"
|> File.read!()
|> JSON.decode!()
|> JSV.build!()
end
def validate_order(order) do
case JSV.validate(order, order_schema()) do
{:ok, _} -> OrderHandler.handle_order(order)
{:error, _} = err -> err
end
end
The schema will be built each time the function is called. Building a schema is
actually pretty fast, but it is a waste of resources nevertheless. In this
example, it is obvious that you would not want to read from a file in every call
to validate_order
. But the schema fetching will generally be wrapped in a
custom function or a build module as suggested above.
Make sure those builds are called at compile-time:
# Do this instead
@order_schema "priv/schemas/order.schema.json"
|> File.read!()
|> JSON.decode!()
|> JSV.build!()
defp order_schema, do: @order_schema
def validate_order(order) do
case JSV.validate(order, order_schema()) do
{:ok, _} -> OrderHandler.handle_order(order)
{:error, _} = err -> err
end
end
Enable format validation
No format validation by default
By default, the https://json-schema.org/draft/2020-12/schema
meta schema
does not perform format validation. This is very counterintuitive, but it
basically means that the following code is correct:
root = JSV.build!(%{type: :string, format: :date})
{:ok, "not a date"} = JSV.validate("not a date", root)
The format
schema keyword is totally ignored. This is bad, but it is the spec!
To always enable format validation when building a root schema, provide the
formats: true
option to JSV.build/2
:
JSV.build(raw_schema, formats: true)
This is another reason to wrap JSV.build/2
with a custom builder module, so
you don't forget to enable those.
Note that format validation is determined at build time. There is no way to change whether it is performed once the root schema is built.
Enable format validation using vocabularies
You can also enable format validation by using the JSON Schema specification
semantics, though it is far simpler and less error-prone to use the :formats
option.
For format validation to be enabled, a schema should declare the
https://json-schema.org/draft/2020-12/vocab/format-assertion
vocabulary
instead of the https://json-schema.org/draft/2020-12/vocab/format-annotation
one that is included by default in the
https://json-schema.org/draft/2020-12/schema
meta schema.
1. Use a new meta schema with format-assertion
{
"$id": "custom://with-formats-on/",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/format-assertion": true
},
"$dynamicAnchor": "meta",
"allOf": [
{ "$ref": "https://json-schema.org/draft/2020-12/meta/core" },
{ "$ref": "https://json-schema.org/draft/2020-12/meta/format-assertion" }
]
}
This example is taken from the JSON Schema Test Suite codebase and does not include all the vocabularies, only the assertion for the formats and the core vocabulary. It will not validate anything other than formats.
2. Declare a schema using that meta schema to perform validation.
You will need a custom resolver to resolve the
given URL for the new $schema
property.
schema =
JSON.decode!("""
{
"$schema": "custom://with-formats-on/",
"type": "string",
"format": "date"
}
""")
root = JSV.build!(schema, resolver: ...)
3. Validate
Now it will work as expected. JSV.validate/2
returns an error tuple without
needing the formats: true
.
{:error, _} = JSV.validate("hello", root)
Reverse use-case
If one of your schemas is using such a meta schema and you want to disable the formats validation, then the following will work:
JSV.build(raw_schema, formats: false)