Skip to content

Commit

Permalink
fill readme
Browse files Browse the repository at this point in the history
  • Loading branch information
Khady committed Sep 22, 2024
1 parent 978987a commit 4e31f1c
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 28 deletions.
149 changes: 148 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,156 @@
# ppx_deriving_jsonschema

`ppx_deriving_jsonschema` is a ppx rewriter that generates JSON schema from OCaml types. This project aims to simplify the process of creating JSON schemas by automatically deriving them from your OCaml type definitions.
`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.

## Installation

```sh
opam install ppx_deriving_jsonschema
```

## `[@@deriving jsonschema]`

```ocaml
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_jsonschema
```

Such a type will be turned into a JSON schema like this:
```json
{
"$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" ]
}
```

### Conversion rules

#### Basic types

Types `int`, `int32`, `int64`, `string`, `float`, `bool` are converted to their JSON equivalents.

Type `char` is converted to `{ "type": "string", "minLength": 1, "maxLength": 1}`.

#### List and arrays

OCaml lists and arrays are converted to `{ "type": "array", "items": { "type": "..." } }`.

#### Tuples

Tuples are converted to `{ "type": "array", "items": [...] }`.

#### Variants and polymorphic variants

Variants are converted to `{ "type": "string", "enum": [...] }`.

if the JSON variant names differ from OCaml conventions, users can specify the corresponding JSON string explicitly using `[@name "constr"]`, for example:

```ocaml
type t =
| Typ [@name "type"]
| Class [@name "class"]
[@@deriving jsonschema]
```

#### Records

Records are converted to `{ "type": "object", "properties": {...}, "required": [...] }`.

The fields of type `option` are not included in the `required` list.

When the JSON object keys differ from the ocaml field names, users can specify the corresponding JSON key implicitly using `[@key "field"]`, for example:

```ocaml
type t = {
typ : float [@key "type"];
class_ : float [@key "CLASS"];
}
[@@deriving jsonschema]
```

#### References

Rather than inlining the definition of a type it is possible to use a [json schema `$ref`](https://json-schema.org/understanding-json-schema/structuring#dollarref) 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.

```ocaml
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_jsonschema
```

Would produce the following schema:
```json
{
"$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"
]
}
```
21 changes: 8 additions & 13 deletions src/ppx_deriving_jsonschema.ml
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,12 @@ let predefined_types = [ "string"; "int"; "float"; "bool" ]
let is_predefined_type type_name = List.mem type_name predefined_types

let type_ref ~loc type_name =
let name = estring ~loc ("#/defs/" ^ type_name) in
let name = estring ~loc ("#/$defs/" ^ type_name) in
[%expr `Assoc [ "$ref", `String [%e name] ]]

let type_def ~loc type_name =
let type_name =
match type_name with
| "int" -> "integer"
| "float" -> "number"
| "bool" -> "boolean"
| _ -> type_name
in
[%expr `Assoc [ "type", `String [%e estring ~loc type_name] ]]
let type_def ~loc type_name = [%expr `Assoc [ "type", `String [%e estring ~loc type_name] ]]

let char ~loc = [%expr `Assoc [ "type", `String "string"; "minLength", `Int 1; "maxLength", `Int 1 ]]

let enum ~loc values =
let values = List.map (fun name -> [%expr `String [%e estring ~loc name]]) values in
Expand All @@ -73,10 +67,11 @@ let is_optional_type core_type =

let rec type_of_core ~loc core_type =
match core_type with
| [%type: int] -> type_def ~loc "int"
| [%type: float] -> type_def ~loc "float"
| [%type: int] | [%type: int32] | [%type: int64] -> type_def ~loc "integer"
| [%type: float] -> type_def ~loc "number"
| [%type: string] -> type_def ~loc "string"
| [%type: bool] -> type_def ~loc "bool"
| [%type: bool] -> type_def ~loc "boolean"
| [%type: char] -> char ~loc
| [%type: [%t? t] option] -> type_of_core ~loc t
| [%type: [%t? t] list] | [%type: [%t? t] array] ->
let t = type_of_core ~loc t in
Expand Down
104 changes: 98 additions & 6 deletions test/test.expected.ml
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,25 @@ type event =
opt: int option [@key "opt_int"];
a: float array ;
l: string list ;
t: [ `Foo | `Bar | `Baz ] }[@@deriving jsonschema]
t: [ `Foo | `Bar | `Baz ] ;
c: char }[@@deriving jsonschema]
include
struct
let event_jsonschema =
`Assoc
[("type", (`String "object"));
("properties",
(`Assoc
[("t",
[("c",
(`Assoc
[("type", (`String "string"));
("enum",
(`List [`String "Foo"; `String "Bar"; `String "Baz"]))]));
("minLength", (`Int 1));
("maxLength", (`Int 1))]));
("t",
(`Assoc
[("type", (`String "string"));
("enum",
(`List [`String "Foo"; `String "Bar"; `String "Baz"]))]));
("l",
(`Assoc
[("type", (`String "array"));
Expand All @@ -119,7 +125,8 @@ include
("date", (`Assoc [("type", (`String "number"))]))]));
("required",
(`List
[`String "t";
[`String "c";
`String "t";
`String "l";
`String "a";
`String "comment";
Expand Down Expand Up @@ -271,7 +278,8 @@ include
[("type", (`String "object"));
("properties",
(`Assoc
[("scores_ref", (`Assoc [("$ref", (`String "#/defs/numbers"))]));
[("scores_ref",
(`Assoc [("$ref", (`String "#/$defs/numbers"))]));
("player", (`Assoc [("type", (`String "string"))]))]));
("required", (`List [`String "scores_ref"; `String "player"]))]
[@@warning "-32-39"]
Expand All @@ -280,3 +288,87 @@ let () =
print_schema ~id:"https://ahrefs.com/schemas/player_scores"
~title:"Player scores" ~description:"Object representing player scores"
~definitions:[("numbers", numbers_jsonschema)] player_scores_jsonschema
type address = {
street: string ;
city: string ;
zip: string }[@@deriving jsonschema]
include
struct
let address_jsonschema =
`Assoc
[("type", (`String "object"));
("properties",
(`Assoc
[("zip", (`Assoc [("type", (`String "string"))]));
("city", (`Assoc [("type", (`String "string"))]));
("street", (`Assoc [("type", (`String "string"))]))]));
("required",
(`List [`String "zip"; `String "city"; `String "street"]))]
[@@warning "-32-39"]
end[@@ocaml.doc "@inline"][@@merlin.hide ]
type t = {
name: string ;
age: int ;
email: string option ;
address: address }[@@deriving jsonschema]
include
struct
let t_jsonschema =
`Assoc
[("type", (`String "object"));
("properties",
(`Assoc
[("address", address_jsonschema);
("email", (`Assoc [("type", (`String "string"))]));
("age", (`Assoc [("type", (`String "integer"))]));
("name", (`Assoc [("type", (`String "string"))]))]));
("required",
(`List [`String "address"; `String "age"; `String "name"]))]
[@@warning "-32-39"]
end[@@ocaml.doc "@inline"][@@merlin.hide ]
let () = print_schema t_jsonschema
type tt =
{
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]
include
struct
let tt_jsonschema =
`Assoc
[("type", (`String "object"));
("properties",
(`Assoc
[("retreat_address",
(`Assoc [("$ref", (`String "#/$defs/shared_address"))]));
("work_address",
(`Assoc [("$ref", (`String "#/$defs/shared_address"))]));
("home_address",
(`Assoc [("$ref", (`String "#/$defs/shared_address"))]));
("email", (`Assoc [("type", (`String "string"))]));
("age", (`Assoc [("type", (`String "integer"))]));
("name", (`Assoc [("type", (`String "string"))]))]));
("required",
(`List
[`String "retreat_address";
`String "work_address";
`String "home_address";
`String "age";
`String "name"]))][@@warning "-32-39"]
end[@@ocaml.doc "@inline"][@@merlin.hide ]
let () =
print_schema ~definitions:[("shared_address", address_jsonschema)]
tt_jsonschema
type c = char[@@deriving jsonschema]
include
struct
let c_jsonschema =
`Assoc
[("type", (`String "string"));
("minLength", (`Int 1));
("maxLength", (`Int 1))][@@warning "-32-39"]
end[@@ocaml.doc "@inline"][@@merlin.hide ]
let () = print_schema c_jsonschema
34 changes: 34 additions & 0 deletions test/test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type event = {
a : float array;
l : string list;
t : [ `Foo | `Bar | `Baz ];
c : char;
}
[@@deriving jsonschema]

Expand Down Expand Up @@ -139,3 +140,36 @@ let () =
~description:"Object representing player scores"
~definitions:[ "numbers", numbers_jsonschema ]
player_scores_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 () = print_schema t_jsonschema

type tt = {
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 () = print_schema ~definitions:[ "shared_address", address_jsonschema ] tt_jsonschema

type c = char [@@deriving jsonschema]

let () = print_schema c_jsonschema
Loading

0 comments on commit 4e31f1c

Please sign in to comment.