The deriver, every attribute
Guides[@@deriving wiretype] writes a type’s description as the value
<type>_json – json for a type named t – and, in a signature, its
val. It reads records, enums, tagged unions, polymorphic variants, type
parameters and recursive types. Bounds, defaults, [@with d], read-only,
write-only, deprecated and examples are attributes, and a doc comment is
the doc.
What it writes is the library’s public combinators, so a derived description is one you could have written by hand.
Records
Section titled “Records”A record is an object named by the type – a t by its module – each
field a member:
type product = { id : Uuidm.t; [@read_only] [@default Uuidm.nil] (** given by the server *) name : string; [@min_length 1] [@max_length 80] price : float; [@above 0.] [@multiple_of 0.01] stock : int; [@default 0] [@min 0] tags : string list; [@max_items 10] [@default []] colour : string option; [@key "color"] [@examples [ "red" ]] code : string; [@wiretype.deprecated] (** use [id] *) secret : string option; [@write_only]}[@@deriving wiretype](** A product in the catalogue. *)On a field:
| Attribute | What it does |
|---|---|
[@default e] |
a missing member reads as e |
[@key "k"] |
names the member on the wire |
[@with d] |
describes the field by d; on an option, the inner type’s |
[@min], [@max], [@multiple_of] |
bound an int, int64 or float |
[@above], [@below] |
bound a float, greater and less than |
[@min_length], [@max_length] |
bound a string, in code points |
[@min_items], [@max_items] |
bound a list |
[@dict] |
a (key * value) list is a map, below |
[@min_properties], [@max_properties] |
bound a map |
[@read_only] |
in answers alone: never read, and in no request’s schema |
[@write_only] |
in requests alone: never written, and in no answer’s schema |
[@wiretype.deprecated] |
the schema says it is deprecated |
[@examples [...]] |
the schema’s examples; on an option, values of the inner type |
An option field may be left out or null, as Object.opt_mem reads it.
An option of an option, or of unit, is refused wherever it is written –
a field, a list’s item, a tuple’s – since None and Some None would
both be written null. A least and a most written as literals with no
value between them are refused. A read-only field is never read, so one
with no [@default e] that is no option is refused, since nothing would
build it.
A field’s doc comment is its doc, as the type’s is the object’s, the
enum’s or the union’s. OCaml’s own [@deprecated] is OCaml’s, never a
document’s: a field or a constructor it marks is derived as any other, and
what is derived uses it with no alert.
A variant of constant constructors is an enum, a word per constructor: its
name with the first letter lowered, or [@name "w"]:
type status = Draft | Published | Archived [@name "gone"][@@deriving wiretype]Unions
Section titled “Unions”A variant with arguments is a union on a tag – "type", or [@@tag "k"],
which only a union has. Each case is an inline record’s members, the
members of its one argument’s object, or none:
type point = { x : float; y : float } [@@deriving wiretype]
(** Something drawn. *)type drawing = Dot of point | Line of { from : point; to_ : point } | Blank[@@deriving wiretype] [@@tag "kind"]A case whose one argument is a scalar, a time, a UUID, a list, an array, an option or a tuple is refused, since a case is an object’s members: write an inline record instead.
Polymorphic variants
Section titled “Polymorphic variants”A polymorphic variant is either an enum or a union, the same way:
type answer = [ `Yes | `No | `Maybe ] [@@deriving wiretype][@@rename_all camel], kebab or snake (the default) spells every
member and word from its OCaml name, and a trailing _ after a keyword –
type_ – is dropped. [@@kind "k"] names the object:
type t = { first_name : string; last_name : string; type_ : string }[@@deriving wiretype] [@@rename_all camel] [@@kind "person"]Type parameters
Section titled “Type parameters”A type parameter 'a is an argument, its description, in order:
type 'a page = { items : 'a list; next : string option } [@@deriving wiretype]
let products = page_json Product.product_jsonA type with parameters has no kind unless [@@kind] gives it one, so a
schema writes it where it is used – 'a page a page of users in one
answer and of orders in another – since a kind names one component, and
two arguments under one are a schema’s Kind_shared error.
Recursive types
Section titled “Recursive types”A type of the group that refers to itself is described through J.rec':
type tree = { label : string; children : tree list } [@@deriving wiretype]A recursive type with no parameter is built as its module loads; one with parameters is a function, each of whose descriptions is built a level at a time as it is first read, written or walked. Either may be shared between domains.
Every other type
Section titled “Every other type”type event = { at : Ptime.t; lasted : Mtime.Span.t; where : float * float; scores : (string * int) list; [@dict] [@max_properties 20] extra : Wiretype.Value.t; state : Status.status; total : float; [@with Wiretype.number_bounded ~min:0. ()]}[@@deriving wiretype]- An
int,int64,float,stringandboolare their own descriptions, andunitisnull. Alistand anarrayare a JSON array, and anoptionis its type ornull. - A
Ptime.tis an instant (J.instant), anMtime.Span.tor anMtime.spana duration (J.duration), and aUuidm.ta UUID (J.uuid ()). - A tuple is a JSON array of exactly its items, each by its own description.
- A
(key * value) listmarked[@dict]– on its field, or on the type itself – is a map (J.dict). Unmarked, it is a list of pairs, since which one it is cannot be read from the type. - Any other type
M.tis described byM.json, andubyu_json. AWiretype.Value.t, by whatever alias it is written, is any JSON. - An
int32, anativeint, acharandbyteshave no description, and are refused, saying what to write instead.
Each of these is the same by its stdlib module’s name – String.t,
'a List.t, Stdlib.Int32.t. The deriver reads only the name written, so
a module of one of these names declared here is read as the stdlib’s, or
as Ptime’s, Mtime’s or Uuidm’s: its type is described with
[@with String.json].
What it prints
Section titled “What it prints”The descriptions above, walked as answers and printed as Zod:
module J = Wiretype
let () = let ctx = J.Schema.create () in let walk name description = ignore (J.Schema.walk ctx Encode ~at:name description : J.Schema.t) in walk "product" Product.product_json; walk "drawing" Drawing.drawing_json; walk "answer" Answer.answer_json; walk "person" Person.json; walk "products" Page.products; walk "tree" Tree.tree_json; walk "event" Event.event_json; print_string (J.Schema.Zod.components (J.Schema.components ctx))Of what it prints, the product, the drawing, the person and the tree.
secret is write-only, so no answer has it; answer and products have
no kind, so neither is a component, and each would be written where it is
used:
/** A product in the catalogue. */export const ProductSchema = z.object({ /** given by the server */ id: z.optional(z.uuid()), name: z.string().check(z.minLength(1), z.maxLength(80)), price: z.number().check(z.gt(0), z.multipleOf(0.01)), stock: z.optional(z.int().check(z.gte(0))), tags: z.optional(z.array(z.string()).check(z.maxLength(10))), color: z.optional(z.string()), /** @deprecated use [id] */ code: z.string(),});export type Product = z.infer<typeof ProductSchema>;
/** Something drawn. */export const DrawingSchema = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("dot"), x: z.number(), y: z.number(), }), z.object({ kind: z.literal("line"), from: PointSchema, to: PointSchema, }), z.object({ kind: z.literal("blank"), }),]);export type Drawing = z.infer<typeof DrawingSchema>;
export const PersonSchema = z.object({ firstName: z.string(), lastName: z.string(), type: z.string(),});export type Person = z.infer<typeof PersonSchema>;
export const TreeSchema = z.object({ label: z.string(), get children() { return z.array(TreeSchema); },});export type Tree = z.infer<typeof TreeSchema>;What it refuses
Section titled “What it refuses”An attribute spelt [@wiretype.x] for no x above, or one of these where
it is not read – a [@key] on a constructor – is refused, since it would
say nothing; a short name that is none of these is left to another
deriver.
A function, an object type and a variant constructor with more than one argument have no JSON shape here. Each refusal is an error at its place in the derived code, with what to write instead, so an editor shows every one in a file, and the rest of the file is still derived.
The deriver refuses at compile time what it sees in the type: a name given
twice, two constructors written alike, an option of an option or of
unit, a least and a most written as literals with no value between them,
a read-only field with no default that is no option, and a case whose one
argument is a scalar, a list, an option or a tuple. The rest it leaves to
the library, when the program starts: a bound that means nothing alone,
though written as a literal ([@multiple_of 0], a negative
[@min_length]), and what it cannot see, such as a case whose one argument
is a type named elsewhere that is no object.
