[@@deriving jsonschema] generates a JSON Schema
(draft 2020-12) from an OCaml type. It is the
ppx_deriving_jsonschema
deriver, integrated into jsonkit and registered through the same PPX as
[@@deriving json].
The generated schema is compatible with the wire format produced by the other json derivers:
- jsonkit (
[@@deriving json]) - ppx_deriving_yojson
- ppx_yojson_conv
The deriver ships inside the jsonkit PPX, so there is nothing extra to
install — enabling jsonkit-melange.ppx (Melange) or jsonkit.ppx
(native) makes [@@deriving jsonschema] available. The generated code relies on
a small runtime that the PPX wires in automatically as a ppx_runtime_library:
| Build | PPX | Runtime |
|---|---|---|
| Melange/JS | jsonkit-melange.ppx |
jsonkit-melange |
| Native | jsonkit.ppx |
jsonkit |
Both runtimes expose the schema support at the same path, Jsonkit.Jsonschema,
so sources shared between a native and a Melange build need no per-backend
conditionals.
A native dune stanza looks like:
(library
(name my_types)
(preprocess
(pps jsonkit.ppx)))and a Melange one:
(library
(name my_types)
(modes melange)
(preprocess
(pps jsonkit-melange.ppx)))type address = {
street: string;
city: string;
zip: string;
} [@@deriving jsonschema]
type t = {
name: string;
age: int;
email: string option;
address: address;
} [@@deriving jsonschema]
let schema = Jsonkit.Jsonschema.make t_jsonschemaSuch a type will be turned into a JSON schema like this:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"address": {
"type": "object",
"properties": {
"zip": { "type": "string" },
"city": { "type": "string" },
"street": { "type": "string" }
},
"required": [ "zip", "city", "street" ],
"additionalProperties": true
},
"email": { "type": "string" },
"age": { "type": "integer" },
"name": { "type": "string" }
},
"required": [ "address", "age", "name" ],
"additionalProperties": true
}For each type with [@@deriving jsonschema], the deriver generates a
<type>_jsonschema value of type Jsonkit.Jsonschema.t.
Wrap it with Jsonkit.Jsonschema.make to get a complete,
self-contained schema document (it adds the $schema header and any
$defs/metadata).
The deriver emits primitive helpers (e.g. int_jsonschema, string_jsonschema)
unqualified, so bring a primitives module into scope:
open Jsonkit.Primitives
type t = {
a: int;
b: string;
} [@@deriving jsonschema]Jsonkit.Primitives describes the encoding jsonkit's own
[@@deriving json] produces — notably int64 as a string, and result as a
tagged array. If the values you are describing are encoded by yojson-style
derivers instead, where int64 is a plain JSON number, open
Jsonkit.Jsonschema.Yojson_primitives.
As we support the ppx to be used with both jsonkit and yojson, we provide two primitives modules: Jsonkit and Yojson.
| OCaml type | JSON schema |
|---|---|
char |
{ "type": "string", "minLength": 1, "maxLength": 1 } |
int |
{ "type": "integer" } |
int64 |
{ "type": "string", "description": "int64 is represented as a string" } |
float |
{ "type": "number" } |
bool |
{ "type": "boolean" } |
string |
{ "type": "string" } |
list, array |
{ "type": "array", "items": { "type": "..." } } |
'a option |
{ "type": ["...", "null"] } |
unit |
{ "type": "null" } |
result('a, 'b) |
{
"anyOf": [
{
"type": "array",
"prefixItems": [ { "const": "Error" }, { "type": "..." } ],
"unevaluatedItems": false,
"minItems": 1
},
{
"type": "array",
"prefixItems": [ { "const": "Ok" }, { "type": "..." } ],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
} |
| OCaml type | JSON schema |
|---|---|
char |
{ "type": "string", "minLength": 1, "maxLength": 1 } |
int, int64 |
{ "type": "number" } |
float |
{ "type": "number" } |
bool |
{ "type": "boolean" } |
string |
{ "type": "string" } |
list, array |
{ "type": "array", "items": { "type": "..." } } |
'a option |
{ "type": ["...", "null"] } |
unit |
{ "type": "null" } |
Type 'a ref is treated as 'a.
type t = {
name : string ref;
} [@@deriving jsonschema]{ "type": "string" }Option types are converted to { "type": ["...", "null"] } and added to the required list.
type t = {
name : string option;
} [@@deriving jsonschema]{
"type": "object",
"properties": {
"name": {
"type": [ "string", "null" ]
},
},
"required": [ "name" ],
"additionalProperties": true
}To make a field optional, use the [@@jsonschema.option] attribute:
type t = {
name : string option; [@jsonschema.option]
} [@@deriving jsonschema]{
"type": "object",
"properties": {
"name": {
"type": [ "string", "null" ]
},
},
"required": [],
"additionalProperties": true
}('ok, 'err) result is converted to an anyOf with two array variants, matching the standard variant encoding:
type result_value = (int, string) result [@@deriving jsonschema]{
"anyOf": [
{
"type": "array",
"prefixItems": [ { "const": "Error" }, { "type": "integer" } ],
"unevaluatedItems": false,
"minItems": 1
},
{
"type": "array",
"prefixItems": [ { "const": "Ok" }, { "type": "string" } ],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}OCaml lists and arrays are converted to { "type": "array", "items": { "type": "..." } }.
Tuples are converted to { "type": "array", "prefixItems": [...] }.
type t = int * string [@@deriving jsonschema]{
"type": "array",
"prefixItems": [ { "type": "integer" }, { "type": "string" } ],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}By default, constructors in variants are represented as a list with one string, which is the name of the contructor. Constructors with arguments are represented as lists, the first element being the constructor name, the rest being its arguments. It reproduces the representation of ppx_deriving_yojson and ppx_yojson_conv. For example:
type t =
| Typ
| Class of string
[@@deriving jsonschema]{
"anyOf": [
{
"type": "array",
"prefixItems": [ { "const": "Typ" } ],
"unevaluatedItems": false,
"minItems": 1,
"maxItems": 1
},
{
"type": "array",
"prefixItems": [ { "const": "Class" }, { "type": "string" } ],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}Note that the implicit tuple in a polymorphic variant is flattened. This can be disabled using the ~polymorphic_variant_tuple flag.
type a = [ `A of int * string * bool ] [@@deriving jsonschema]{
"anyOf": [
{
"type": "array",
"prefixItems": [
{ "const": "A" },
{ "type": "integer" },
{ "type": "string" },
{ "type": "boolean" }
],
"unevaluatedItems": false,
"minItems": 4,
"maxItems": 4
}
]
}type b = [ `B of int * string * bool ] [@@deriving jsonschema ~polymorphic_variant_tuple]{
"anyOf": [
{
"type": "array",
"prefixItems": [
{ "const": "B" },
{
"type": "array",
"prefixItems": [
{ "type": "integer" },
{ "type": "string" },
{ "type": "boolean" }
],
"unevaluatedItems": false,
"minItems": 3,
"maxItems": 3
}
],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}If the JSON variant names differ from OCaml conventions, it is possible to specify the corresponding JSON string explicitly using [@name "constr"], for example:
type t =
| Typ [@name "type"]
| Class of string [@name "class"]
[@@deriving jsonschema]{
"anyOf": [
{
"type": "array",
"prefixItems": [ { "const": "type" } ],
"unevaluatedItems": false,
"minItems": 1,
"maxItems": 1
},
{
"type": "array",
"prefixItems": [ { "const": "class" }, { "type": "string" } ],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}A [@@jsonschema.compact_variants] attribute on the type declaration collapses payload-free constructors to a bare { "const": "..." }; constructors with arguments keep the standard array encoding. It preserves the encoding of constructors with a payload.
type t =
| A
| B
| C of int
[@@deriving jsonschema] [@@jsonschema.compact_variants]{
"anyOf": [
{ "const": "A" },
{ "const": "B" },
{
"type": "array",
"prefixItems": [ { "const": "C" }, { "type": "integer" } ],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}It works the same way on polymorphic variants: payload-free constructors collapse to { "const": "..." }, while constructors with arguments keep the array encoding.
type t =
[ `Aaa
| `Bbb
| `Ccc of int
]
[@@deriving jsonschema] [@@jsonschema.compact_variants]{
"anyOf": [
{ "const": "Aaa" },
{ "const": "Bbb" },
{
"type": "array",
"prefixItems": [ { "const": "Ccc" }, { "type": "integer" } ],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}Records are converted to { "type": "object", "properties": {...}, "required": [...], "additionalProperties": true }.
The fields of type option are not included in the required list.
By default, additional properties are allowed in objects, matching the JSON
derivers which ignore unknown object keys. To reject unknown keys and generate
a strict schema, use the disallow_extra_fields attribute:
type company = {
name : string;
employees : int;
}
[@@deriving jsonschema]
[@@jsonschema.disallow_extra_fields]This annotation will generate a schema with "additionalProperties": false,
rejecting fields not defined in the record:
{
"type": "object",
"properties": {
"name": { "type": "string" },
"employees": { "type": "integer" }
},
"required": [ "name", "employees" ],
"additionalProperties": false
}[@@jsonschema.allow_extra_fields] is still accepted for backwards
compatibility but is now a no-op (allowing extra fields is the default);
combining it with disallow_extra_fields on the same type is an error.
When the JSON object keys differ from the ocaml field names, users can specify the corresponding JSON key implicitly using [@key "field"], for example:
type t = {
typ : float [@key "type"];
class_ : float [@key "CLASS"];
}
[@@deriving jsonschema]Inline records in variants also allow additional fields by default. Use the [@jsonschema.disallow_extra_fields] attribute on a constructor with an inline record to reject unknown keys for that record:
type inline_record_with_strict_fields =
| User of { name : string; email : string } [@jsonschema.disallow_extra_fields]
| Guest of { ip : string }
[@@deriving jsonschema]This will generate a schema that rejects additional fields for the User variant's record but allows them for the Guest variant:
{
"anyOf": [
{
"type": "array",
"prefixItems": [
{ "const": "User" },
{
"type": "object",
"properties": {
"email": { "type": "string" },
"name": { "type": "string" }
},
"required": [ "email", "name" ],
"additionalProperties": false
}
],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
},
{
"type": "array",
"prefixItems": [
{ "const": "Guest" },
{
"type": "object",
"properties": { "ip": { "type": "string" } },
"required": [ "ip" ],
"additionalProperties": true
}
],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}Rather than inlining the definition of a type it is possible to use a json schema $ref using the [@ref "name"] attribute. In such a case, the type definition must be passed to Jsonkit.Jsonschema.make as a parameter.
type address = {
street : string;
city : string;
zip : string;
}
[@@deriving jsonschema]
type t = {
name : string;
age : int;
email : string option;
home_address : address; [@ref "shared_address"]
work_address : address; [@ref "shared_address"]
retreat_address : address; [@ref "shared_address"]
}
[@@deriving jsonschema]
let schema =
Jsonkit.Jsonschema.make
~definitions:[("shared_address", address_jsonschema)]
t_jsonschemaWould produce the following schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"shared_address": {
"type": "object",
"properties": {
"zip": { "type": "string" },
"city": { "type": "string" },
"street": { "type": "string" }
},
"required": [ "zip", "city", "street" ]
}
},
"type": "object",
"properties": {
"retreat_address": { "$ref": "#/$defs/shared_address" },
"work_address": { "$ref": "#/$defs/shared_address" },
"home_address": { "$ref": "#/$defs/shared_address" },
"email": { "type": "string" },
"age": { "type": "integer" },
"name": { "type": "string" }
},
"required": [
"retreat_address", "work_address", "home_address", "age", "name"
]
}Recursive types are automatically detected and handled using JSON Schema's $defs and $ref mechanism.
type tree =
| Leaf
| Node of { value : int; left : tree; right : tree }
[@@deriving jsonschema]{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"tree": {
"anyOf": [
{
"type": "array",
"prefixItems": [ { "const": "Leaf" } ],
"unevaluatedItems": false,
"minItems": 1,
"maxItems": 1
},
{
"type": "array",
"prefixItems": [
{ "const": "Node" },
{
"type": "object",
"properties": {
"right": { "$ref": "#/$defs/tree" },
"left": { "$ref": "#/$defs/tree" },
"value": { "type": "integer" }
},
"required": [ "right", "left", "value" ],
"additionalProperties": true
}
],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}
},
"$ref": "#/$defs/tree"
}Mutually recursive types (defined with and) are also supported:
type expr =
| Literal of int
| Binary of expr * expr
| Block of stmt list
and stmt =
| ExprStmt of expr
| IfStmt of { cond : expr; then_ : stmt; else_ : stmt option }
[@@deriving jsonschema]This generates expr_jsonschema containing all definitions in $defs:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"expr": {
"anyOf": [
{
"type": "array",
"prefixItems": [ { "const": "Literal" }, { "type": "integer" } ],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
},
{
"type": "array",
"prefixItems": [
{ "const": "Binary" },
{ "$ref": "#/$defs/expr" },
{ "$ref": "#/$defs/expr" }
],
"unevaluatedItems": false,
"minItems": 3,
"maxItems": 3
},
{
"type": "array",
"prefixItems": [
{ "const": "Block" },
{ "type": "array", "items": { "$ref": "#/$defs/stmt" } }
],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
},
"stmt": {
"anyOf": [
{
"type": "array",
"prefixItems": [
{ "const": "ExprStmt" }, { "$ref": "#/$defs/expr" }
],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
},
{
"type": "array",
"prefixItems": [
{ "const": "IfStmt" },
{
"type": "object",
"properties": {
"else_": { "$ref": "#/$defs/stmt" },
"then_": { "$ref": "#/$defs/stmt" },
"cond": { "$ref": "#/$defs/expr" }
},
"required": [ "then_", "cond" ],
"additionalProperties": true
}
],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}
},
"$ref": "#/$defs/expr"
}The secondary type stmt_jsonschema is also a self-contained schema, with the same $defs but $ref pointing to its own type:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"expr": { ... },
"stmt": { ... }
},
"$ref": "#/$defs/stmt"
}[@@jsonschema.description] (REF)
Add a description to a type or a field. It can be used on a type, a field, a variant constructor, or directly on a core type (e.g. a variant payload).
type t = {
name : string [@jsonschema.description "The user's full name"];
} [@@deriving jsonschema]{
"type": "object",
"properties": {
"name": { "description": "The user's full name", "type": "string" }
},
"required": [ "name" ],
"additionalProperties": true
}When the ~ocaml_doc flag is passed to the deriver, an OCaml doc comment ((** ... *), stored in the AST as an ocaml.doc attribute) is used as the description when [@jsonschema.description] is not provided. The explicit attribute takes precedence. Without the flag, doc comments are ignored.
type t = {
name : string; (** The user's full name *)
} [@@deriving jsonschema ~ocaml_doc]
(** A user object *){
"description": "A user object",
"type": "object",
"properties": {
"name": { "description": "The user's full name", "type": "string" }
},
"required": [ "name" ],
"additionalProperties": true
}[@@jsonschema.format] (REF)
Add a format annotation to a string-typed field. It can be used on a type, a field, or directly on a core type (e.g. a variant payload). Only applies to string and bytes types.
type t = {
name : string [@jsonschema.format "date-time"];
} [@@deriving jsonschema]{
"type": "object",
"properties": {
"name": { "format": "date-time", "type": "string" }
},
"required": [ "name" ],
"additionalProperties": true
}[@@jsonschema.maximum] (REF)
Add a maximum value to a type or a field. It can be used on a type, a field, or a variant payload. Only applies to numeric types (int, int32, nativeint, float).
type t = {
score : int [@jsonschema.maximum 100];
} [@@deriving jsonschema]{
"type": "object",
"properties": {
"score": { "maximum": 100, "type": "integer" }
},
"required": [ "score" ],
"additionalProperties": true
}[@@jsonschema.minimum] (REF)
Add a minimum value to a type or a field. It can be used on a type, a field, or a variant payload. Only applies to numeric types (int, int32, nativeint, float).
type t = {
score : int [@jsonschema.minimum 0];
} [@@deriving jsonschema]{
"type": "object",
"properties": {
"score": { "minimum": 0, "type": "integer" }
},
"required": [ "score" ],
"additionalProperties": true
}[@jsonschema.default] (REF)
Set a default value for a record field. Fields with a default are excluded from required.
Primitive literals (int, int32, nativeint, float, string, bytes, bool) and their option, list, tuple, and array variants are serialized automatically.
For non-primitive types (custom variants, records, etc.) a <type>_to_json function must be in scope — e.g. via [@@deriving json] from jsonkit. (<type> -> Js.Json.t at melange and <type> -> Yojson.Basic.t at native)
type status = Active | Inactive [@@deriving jsonschema]
let status_to_json = function
| Active -> `String "Active"
| Inactive -> `String "Inactive"
type t = {
score : int option; [@jsonschema.default 0]
label : string; [@jsonschema.default "unlabelled"]
is_admin : bool; [@jsonschema.default false]
tags : string list; [@jsonschema.default ["general"]]
status : status; [@jsonschema.default Active]
} [@@deriving jsonschema]{
"type": "object",
"properties": {
"score": { "default": 0, "type": [ "integer", "null" ] },
"label": { "default": "unlabelled", "type": "string" },
"is_admin": { "default": false, "type": "boolean" },
"tags": { "default": [ "general" ], "type": "array", "items": { "type": "string" } },
"status": { "default": [ "Active" ], "anyOf": [ ... ] }
},
"required": [],
"additionalProperties": true
}A composite annotation that bundles multiple schema attributes into a single record expression. Supported fields: description, format, maximum, minimum. Type-sensitive fields (format, maximum, minimum) are validated against the annotated type.
Can be used on core types, label declarations, and type declarations.
type t = {
score : int [@jsonschema.attrs { maximum = 100; minimum = 0; description = "Score out of 100" }];
created_at : string [@jsonschema.attrs { format = "date-time"; description = "Creation timestamp" }];
} [@@deriving jsonschema]{
"type": "object",
"properties": {
"score": { "minimum": 0, "maximum": 100, "description": "Score out of 100", "type": "integer" },
"created_at": { "format": "date-time", "description": "Creation timestamp", "type": "string" }
},
"required": [ "score", "created_at" ],
"additionalProperties": true
}It can also be applied directly to a core type in a variant payload:
type t =
| Score of (int [@jsonschema.attrs { maximum = 100; minimum = 0; description = "Percentage" }])
[@@deriving jsonschema]{
"anyOf": [
{
"type": "array",
"prefixItems": [
{ "const": "Score" },
{ "minimum": 0, "maximum": 100, "description": "Percentage", "type": "integer" }
],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}