package ppx_deriving_jsonschema
Install
dune-project
Dependency
Authors
Maintainers
Sources
sha256=c5a2f8c1d906c1890bcde81aa149cc38e479878eb6c1c45a8455d03a95a07c2e
sha512=c0e661717244bf62132a21ef975db2720597b1d8013b0221c1d7db5adfc14928caf675c0f2fd89869ee6f4f96527599e556a0c123cd9dc7f65cad8f81de322ef
Description
ppx_deriving_jsonschema is a ppx rewriter that generates jsonschema from ocaml types
README
ppx_deriving_jsonschema
ppx_deriving_jsonschema is a PPX syntax extension that generates JSON schema from OCaml types.
The conversion aims to be compatible with the existing json derivers:
- https://github.com/melange-community/melange-json
- https://github.com/ocaml-ppx/ppx_deriving_yojson
- https://github.com/janestreet/ppx_yojson_conv
Installation
opam install ppx_deriving_jsonschema[@@deriving jsonschema]
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 = Ppx_deriving_jsonschema_runtime.json_schema 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" ]
},
"email": { "type": "string" },
"age": { "type": "integer" },
"name": { "type": "string" }
},
"required": [ "address", "age", "name" ]
}Usage
To generate jsonschema for a type, add the [@@deriving jsonschema] attribute to the type declaration.
open Ppx_deriving_jsonschema_runtime.Primitives.Melange_json
type t = {
a: int;
b: string;
} [@@deriving jsonschema]Conversion rules
Primitives
As we support the ppx to be used with both melange-json and yojson, we provide two primitives modules: Melange_json and Yojson.
Melange_json
| OCaml type | JSON schema |
|---|---|
char |
|
int |
|
int64 |
|
float |
|
bool |
|
string |
|
list, array |
|
'a option |
|
unit |
|
result('a, 'b) |
|
Yojson
| OCaml type | JSON schema |
|---|---|
char |
|
int, int64 |
|
float |
|
bool |
|
string |
|
list, array |
|
'a option |
|
unit |
|
Ref
Type 'a ref is treated as 'a.
type t = {
name : string ref;
} [@@deriving jsonschema]{ "type": "string" }Option
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": false
}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": false
}Result
('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
}
]
}List and arrays
OCaml lists and arrays are converted to { "type": "array", "items": { "type": "..." } }.
Tuples
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
}Variants and polymorphic variants
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
}
]
}A ~variant_as_string flag is exposed to obtain a more natural representation "anyOf": [{ "const": "..." }, ...]. This representation does not support payloads. For example:
type t =
| Typ
| Class of string
[@@deriving jsonschema ~variant_as_string]{ "anyOf": [ { "const": "Typ" }, { "const": "Class" } ] }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 ~variant_as_string]{ "anyOf": [ { "const": "type" }, { "const": "class" } ] }A [@@jsonschema.compact_variants] attribute offers a middle ground. Unlike ~variant_as_string, it only collapses payload-free constructors to a bare { "const": "..." }; constructors with arguments keep the standard array encoding. It therefore supports payloads.
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
Records are converted to { "type": "object", "properties": {...}, "required": [...], "additionalProperties": false }.
The fields of type option are not included in the required list.
By default, additionalProperties are not allowed in objects. To allow additionalProperties, use the allow_extra_fields attribute:
type company = {
name : string;
employees : int;
}
[@@deriving jsonschema]
[@@jsonschema.allow_extra_fields]This annotation will generate a schema with "additionalProperties": true, allowing for additional fields not defined in the record:
{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": [ "name", "age" ],
"additionalProperties": true
}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
You can use the [@jsonschema.allow_extra_fields] attribute on a constructor with an inline record to allow additional fields in that record:
type inline_record_with_extra_fields =
| User of { name : string; email : string } [@jsonschema.allow_extra_fields]
| Guest of { ip : string }
[@@deriving jsonschema]This will generate a schema that allows additional fields for the User variant's record but not for the Guest variant:
{
"anyOf": [
{
"type": "array",
"prefixItems": [
{ "const": "User" },
{
"type": "object",
"properties": {
"email": { "type": "string" },
"name": { "type": "string" }
},
"required": [ "email", "name" ],
"additionalProperties": true
}
],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
},
{
"type": "array",
"prefixItems": [
{ "const": "Guest" },
{
"type": "object",
"properties": { "ip": { "type": "string" } },
"required": [ "ip" ],
"additionalProperties": false
}
],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}References
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 Ppx_deriving_jsonschema_runtime.json_schema 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 =
Ppx_deriving_jsonschema_runtime.json_schema
~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
Recursive types are automatically detected and handled using JSON Schema's $defs and $ref mechanism.
Self-referential types
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": false
}
],
"unevaluatedItems": false,
"minItems": 2,
"maxItems": 2
}
]
}
},
"$ref": "#/$defs/tree"
}Mutually recursive types
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": false
}
],
"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"
}Annotations
[@@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": false
}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": false
}[@@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": false
}[@@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": false
}[@@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": false
}[@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 melange-json. (<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": false
}[@jsonschema.attrs]
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": false
}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
}
]
}
Dependencies (5)
-
server-reason-react
>= "0.4.1" -
melange
>= "4.0.0" -
ppxlib
> "0.36.0" -
dune
>= "3.16" -
ocaml
>= "5.0.0"
Dev Dependencies (9)
-
odoc
with-doc -
odoc-driver
with-doc -
ocaml-lsp-server
with-dev-setup -
ocamlformat
= "0.29.0" & with-dev-setup -
conf-python-3
with-test -
conf-npm
with-test -
yojson
with-test -
melange-json-native
>= "2.0.0" & with-test -
melange-json
>= "2.0.0" & with-test
Used by
None
Conflicts
None