Kinds
GuidesA kind is a value with a grammar of its own, written as a JSON string. Each
ready-made kind is a plain OCaml type, or Ptime’s or Mtime’s for a time and
Uuidm’s for a UUID, with its schema’s format and its Zod check, and
accepts no string that check refuses, so a browser never turns away what
the server would take.
| Kind | OCaml | Reads |
|---|---|---|
J.instant |
Ptime.t, to the ms |
RFC 3339 date-time |
J.date |
int * int * int |
YYYY-MM-DD |
J.duration |
Mtime.Span.t, to the ms |
ISO 8601, weeks or days to seconds |
J.uuid ?version () |
Uuidm.t |
RFC 9562 |
J.base64, J.base64url |
string, the bytes |
RFC 4648 |
J.uri |
string |
RFC 3986, held to the URL standard, IDNA included |
J.ipv4, J.ipv6 |
string |
dotted decimal, IPv6 |
Read and written back
Section titled “Read and written back”module J = Wiretype
let back name description text = match J.decode description text with | Error problems -> Printf.printf "%-9s %s: %s\n" name text (J.Problem.list_to_string problems) | Ok v -> ( match J.encode description v with | Ok written -> Printf.printf "%-9s %s -> %s\n" name text written | Error u -> Printf.printf "%-9s %s: %s\n" name text (J.Unwritable.to_string u))
let () = back "instant" J.instant {|"2026-09-30T14:00:00.123456+02:00"|}; back "date" J.date {|"2026-09-30"|}; back "duration" J.duration {|"PT1H30M"|}; back "uuid" (J.uuid ()) {|"0190F6E8-7E0B-7C55-8F1A-3C1D2B4A5E6F"|}; back "base64" J.base64 {|"aGVsbG8="|}; back "base64url" J.base64url {|"aGVsbG8"|}; back "uri" J.uri {|"https://example.com/a?b=c"|}; back "uri" J.uri {|"https://example.com:99999/"|}; back "ipv4" J.ipv4 {|"192.168.0.1"|}; back "ipv4" J.ipv4 {|"192.168.0.01"|}; back "ipv6" J.ipv6 {|"FE80::1"|}instant "2026-09-30T14:00:00.123456+02:00" -> "2026-09-30T12:00:00.123Z"date "2026-09-30" -> "2026-09-30"duration "PT1H30M" -> "PT1H30M"uuid "0190F6E8-7E0B-7C55-8F1A-3C1D2B4A5E6F" -> "0190f6e8-7e0b-7c55-8f1a-3c1d2b4a5e6f"base64 "aGVsbG8=" -> "aGVsbG8="base64url "aGVsbG8" -> "aGVsbG8"uri "https://example.com/a?b=c" -> "https://example.com/a?b=c"uri "https://example.com:99999/": the document: This must be a URI with its scheme, like https://example.com/path. (malformed)ipv4 "192.168.0.1" -> "192.168.0.1"ipv4 "192.168.0.01": the document: This must be an IPv4 address, like 192.0.2.1. (malformed)ipv6 "FE80::1" -> "FE80::1"Each kind
Section titled “Each kind”instantreads RFC 3339’sdate-timewith any fraction and offset, and writes it in UTC with milliseconds, so one instant has one spelling. A fraction past the millisecond is dropped, toward the past, in reading and in writing. A leap second is refused, and so is an instant outside the years 0000 to 9999 once in UTC, which noPtime.tis.dateis(year, month, day), as RFC 3339’sfull-date.durationis ISO 8601: weeks, or days, hours, minutes and seconds, to the millisecond; never years or months, which are no number of milliseconds. A day is 24 hours. One longer than anMtime.Span.tholds, or a negative one, which ISO 8601 has no spelling for, is refused.uuidreads versions 1 to 8, with the nil and the max UUID, or the one version asked for (~version:V7), in either case, and writes it in lower case. The max UUID is read in lower case alone, as Zod reads it. Any 128 bits are aUuidm.t, so one of another version, or of a variant RFC 9562 does not define, is unwritable.base64is RFC 4648 §4’s padded base64, andbase64url§5’s unpadded base64url; the OCaml string holds the bytes.uriis RFC 3986, less what the URL standard refuses: a port past 65535, anIPvFuturehost, an empty host after userinfo or before a port, anhttp,https,ws,wssorftpURI with no host or a host that is not a domain or an address, and afileURI with userinfo, a port, or a host that is not empty, a domain or an address. Anxn--label is decoded as Punycode and held to UTS #46 as the URL standard applies it, with Unicode 15.0.0’s tables, the bidi rule of RFC 5893 across the host included. Such a label is refused, where a browser might take it, when it holds a joiner or a code point that may not be in NFC, when it is longer than the 63 octets DNS allows a label, or when it decodes to nothing but ASCII or to anotherxn--.ipv4is dotted decimal: four numbers to 255, none with a leading zero.ipv6is RFC 4291 §2.2’s full and compressed forms, with a dotted IPv4 address in the last thirty-two bits, kept as written:FE80::1stays in upper case. A zone (%eth0) is no part of an address, and is refused.
Derived
Section titled “Derived”The deriver describes a Ptime.t as an instant, an Mtime.Span.t as a
duration and a Uuidm.t as a UUID:
module J = Wiretype
type booking = { starts : Ptime.t; lasts : Mtime.Span.t; guest : Uuidm.t }[@@deriving wiretype]
let () = match J.decode booking_json {|{"starts": "2026-09-30T12:00:00Z", "lasts": "P1W", "guest": "0190f6e8-7e0b-7c55-8f1a-3c1d2b4a5e6f"}|} with | Error problems -> print_endline (J.Problem.list_to_string problems) | Ok booking -> ( match J.encode booking_json booking with | Ok text -> print_endline text | Error u -> print_endline (J.Unwritable.to_string u))A program that names these types links ptime, mtime or uuidm beside
wiretype.
Why these types
Section titled “Why these types”Each kind is over a plain OCaml type, so no library’s type is in an
application’s API, but for time and identity: an instant is a Ptime.t, a
duration an Mtime.Span.t and a UUID a Uuidm.t, the types OCaml’s
libraries hold them in.
What each kind is, its format says, so a schema prints no description
of the library’s for it: a doc is its user’s, a member’s or one J.map
gives around the kind. A kind of your own is
J.kind.
