Defining Schemas
View SourceThis guide explains the different possible values to use as a schema with JSV.
Schema sources
Schemas given as input to JSV can come from two sources:
- Schemas given directly to the
JSV.build/2
function. These are expected to be maps or booleans. JSV will not parse JSON strings. - Schemas returned by resolvers. These should also be maps (but not booleans). The built-in resolvers will handle JSON deserialization automatically.
JSV is designed to work with raw schemas. Any map or boolean is a valid schema. For instance, it is possible to directly use a schema from a file:
root =
"my-schema.json"
|> File.read!()
|> JSON.decode!()
|> JSV.build()
Schema formats
Schemas can be either booleans or maps. The true
value is equivalent to an
empty JSON Schema {}
, while false
is a schema that will invalidate any
value. It is most often used as a sub-schema for additionalProperties
.
Maps can define keys and values as binaries or atoms. The following schemas are equivalent:
%{type: :boolean}
%{type: "boolean"}
%{"type" => "boolean"}
# You will rarely find this one in the wild!
%{"type" => :boolean}
This is because JSV will normalize the schemas before building a "root", the base data structure for data validation.
Mixing keys is not recommended. In the following example, JSV will build a schema that will successfully validate integers with a minimum of zero. However, the choice for the maximum value is not made by JSV.
%{:type => :integer, "minimum" => 0, "maximum" => 10, :maximum => 20}
Struct schemas
Schemas can be used to define structs with the JSV.defschema/1
macro.
For instance, with this module definition schema:
defmodule MyApp.UserSchema do
use JSV.Schema
defschema %{
type: :object,
properties: %{
name: %{type: :string, default: ""},
age: %{type: :integer, default: 0}
}
}
end
A struct will be defined with the appropriate default values:
iex> %MyApp.UserSchema{}
%MyApp.UserSchema{name: "", age: 0}
The module can be used as a schema to build a validator root and cast data to the corresponding struct:
iex> {:ok, root} = JSV.build(MyApp.UserSchema)
iex> data = %{"name" => "Alice"}
iex> JSV.validate(data, root)
{:ok, %MyApp.UserSchema{name: "Alice", age: 0}}
Casting to a struct can be disabled by passing cast: false
into the
options of JSV.validate/3
.
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!"}}
The module can also be used in other schemas:
%{
type: :object,
properties: %{
name: %{type: :string},
owner: MyApp.UserSchema
}
}
An alternative syntax can be used, by passing only the properties schemas as a list.
defmodule MyApp.UserSchema do
use JSV.Schema
defschema name: %{type: :string, default: ""},
age: %{type: :integer, default: 0}
end
In that case, properties that do not have a default value are automatically
required, and the type
of the schema is automatically set to object
. The
title
of the schema is set as the last segment of the module name.
Struct defining schemas are a special case of the generic cast mechanism built in JSV. Make sure to check that guide out as well.
Defining multiple schemas with defschema/3
The JSV.defschema/3
macro allows you to define a new module for a schema. It
can be used inside an enclosing "group" module, which is useful for organizing
related schemas together:
defmodule MyApp.Schemas do
use JSV.Schema
defschema User,
name: string(),
email: string(),
age: integer(default: 18)
defschema Address,
"Physical address information",
street: string(),
city: string(),
country: string(default: "US")
defschema Company,
"Company information with nested schemas",
name: string(),
address: Address,
employees: array_of(User)
end
This creates three separate modules: MyApp.Schemas.User
,
MyApp.Schemas.Address
, and MyApp.Schemas.Company
, each with their own struct
and JSON schema.
You can use these schemas independently:
user = %MyApp.Schemas.User{name: "Alice", email: "alice@example.com"}
address = %MyApp.Schemas.Address{street: "123 Main St", city: "Boston"}
{:ok, user_root} = JSV.build(MyApp.Schemas.User)
{:ok, company_root} = JSV.build(MyApp.Schemas.Company)
Using full schema maps
You can also use defschema/3
with complete schema maps instead of property lists:
defmodule MyApp.Schemas do
use JSV.Schema
defschema ApiResponse,
"Standard API response format",
%{
type: :object,
properties: %{
success: %{type: :boolean},
data: %{type: :object},
errors: %{type: :array, items: %{type: :string}}
},
required: [:success],
additionalProperties: false
}
end
When using full schema maps, the title and description from the macro parameters are not automatically applied to the schema, the map is used as-is. Only the description parameter is used for the module's documentation.
Self-referencing schemas
Schemas can reference themselves using __MODULE__
:
defmodule MyApp.Schemas do
use JSV.Schema
defschema Category,
"Hierarchical category structure",
name: string(),
parent: optional(__MODULE__)
end
This creates a MyApp.Schemas.Category
module that can have a parent of the
same type, allowing for hierarchical data structures.