OCaml Support - atdml¶
Atdml reference¶
Note
Atdml is experimental. Its generated interface and the set of supported features may still change in future releases.
Description¶
Atdml is a command-line program that takes as input type definitions in the
ATD syntax and produces a self-contained OCaml module that serializes and
deserializes values to and from JSON via the
Yojson Safe.t
intermediate representation.
Atdml is the recommended successor to atdgen for OCaml JSON support. Users are encouraged to migrate from atdgen to atdml. Future development priority will be given to atdml; atdgen will receive minimal maintenance only.
Compared to atdgen, atdml:
generates a single
foo.ml/foo.mlipair from a singlefoo.atdfile, with no need for a separate-tpass;requires only yojson at runtime — there is no separate
atdml-runtimepackage to link;generates straightforward, readable OCaml code that does not rely on internal Yojson streaming functions;
does not support the biniou binary format (JSON only).
Atdml uses the following packages:
atd: parser for the syntax of type definitionsyojson: parser and printer for JSON
Command-line usage¶
Command-line help¶
Call atdml --help for the full list of available options.
File mode¶
$ atdml foo.atd
Reads foo.atd and writes foo.ml and foo.mli in the current
directory. The base name is derived from the input file by stripping the
.atd extension and lower-casing.
Stdin mode¶
$ atdml < foo.atd
Reads ATD source from stdin and writes a self-contained OCaml snippet to
stdout that can be copy-pasted into utop or ocaml:
module type Types = sig
...
end
module Types : Types = struct
...
end
Example¶
Input file foo.atd:
type color = [
| Red
| Green
| Blue
]
type point = {
x: float;
y: float;
?label: string option;
}
Generate foo.ml and foo.mli:
$ atdml foo.atd
The generated foo.mli exposes, for each ATD type, a type definition,
conversion functions, and a submodule:
type color =
| Red
| Green
| Blue
val color_of_yojson : Yojson.Safe.t -> color
val yojson_of_color : color -> Yojson.Safe.t
val color_of_json : string -> color
val json_of_color : color -> string
module Color : sig
type nonrec t = color
val of_yojson : Yojson.Safe.t -> t
val to_yojson : t -> Yojson.Safe.t
val of_json : string -> t
val to_json : t -> string
end
type point = {
x: float;
y: float;
label: string option;
}
val create_point : x:float -> y:float -> ?label:string -> unit -> point
val point_of_yojson : Yojson.Safe.t -> point
val yojson_of_point : point -> Yojson.Safe.t
val point_of_json : string -> point
val json_of_point : point -> string
module Point : sig
type nonrec t = point
val create : x:float -> y:float -> ?label:string -> unit -> t
val of_yojson : Yojson.Safe.t -> t
val to_yojson : t -> Yojson.Safe.t
val of_json : string -> t
val to_json : t -> string
end
Generated interface¶
For each ATD type foo, atdml generates the following in the .mli
and .ml files:
Type definition¶
A type definition matching the ATD definition, using the OCaml representations described in the Default type mapping section below.
type foo = ...
For record types, a create_foo constructor with labelled arguments is also
generated. Required fields use mandatory labelled arguments; optional fields
(?foo) and with-default fields (~foo) use optional labelled arguments:
val create_foo : required_field:int -> ?optional_field:string -> unit -> foo
Conversion functions¶
For each ATD type foo:
val foo_of_yojson : Yojson.Safe.t -> foo
val yojson_of_foo : foo -> Yojson.Safe.t
val foo_of_json : string -> foo
val json_of_foo : foo -> string
For parametric types (e.g. type ('a, 'b) foo), the conversion functions
take additional function arguments for the type parameters:
val foo_of_yojson :
(Yojson.Safe.t -> 'a) ->
(Yojson.Safe.t -> 'b) ->
Yojson.Safe.t -> ('a, 'b) foo
val yojson_of_foo :
('a -> Yojson.Safe.t) ->
('b -> Yojson.Safe.t) ->
('a, 'b) foo -> Yojson.Safe.t
Submodule¶
Each type also gets a submodule named after the type, capitalised. The submodule bundles the type and its conversion functions under a single name, which is convenient for use as a module argument:
module Foo : sig
type nonrec t = foo
val create : required_field:int -> ?optional_field:string -> unit -> t (* record types only *)
val of_yojson : Yojson.Safe.t -> t
val to_yojson : t -> Yojson.Safe.t
val of_json : string -> t
val to_json : t -> string
end
Reserved name handling¶
Some ATD type names can conflict with OCaml keywords (module, type,
…) or with the naming convention used by atdml (yojson, json).
Atdml automatically renames such types by appending an underscore. For
example, an ATD type named module becomes module_ in OCaml.
The same treatment applies to record field names and variant constructor
names: any name that is an OCaml keyword (if, end, type, …)
gets a trailing underscore appended.
Primitive type aliases¶
When an ATD type is an unparameterized alias of a primitive type
(unit, bool, int, float, or string), atdml emits a
private type in the generated .mli:
(* ATD *)
type email = string
type score = float
(* generated .mli *)
type email = private string
type score = private float
val create_email : string -> email
val create_score : float -> score
The private keyword prevents direct construction of the alias type
outside the generated module, so the compiler names the alias (email,
score) rather than the underlying primitive in error messages, and
rejects accidental direct construction (e.g. let x : email = "hello").
The create_* function is the intended constructor.
Coercing back to the primitive uses the standard :> operator:
let s : string = (my_email :> string)
The generated .ml keeps a transparent alias, so create_email is an
identity function with no runtime overhead.
Each alias also gets a submodule with a create alias:
module Email : sig
type nonrec t = email
val create : string -> t
...
end
Default type mapping¶
The following table summarizes the default mapping between ATD types and OCaml types, and their JSON representations.
ATD |
OCaml |
JSON |
|---|---|---|
|
|
|
|
|
|
|
|
integer (e.g. |
|
|
number (e.g. |
|
|
string (e.g. |
|
|
any JSON value |
|
|
JSON array |
|
|
|
|
|
|
|
defined by annotation |
representation of |
|
|
JSON array (e.g. |
sum type |
regular variants |
|
sum type (poly) |
polymorphic variants |
|
record |
record |
JSON object |
Notes:
JSON integers are also accepted when a
floatis expected.'a optionuses the ATD convention (not the ppx_yojson_conv one):"None"forNone,["Some", ...]forSome.'a nullablemaps toNonefor JSONnullandSome xfor any other value.The
abstracttype maps toYojson.Safe.t, accepting any JSON value.JSON tuples use the array notation, e.g.
[1, "hello", true].Sum type constructors without arguments are represented as a JSON string, e.g.
"Leaf". Constructors with an argument use a two-element array, e.g.["Node", ...]. An alternative single-key object encoding ({"Node": ...}) is available via<json repr="object">.Optional record fields (marked with
?) are omitted from the JSON object when their value isNone.With-default record fields (marked with
~) are omitted from the JSON object when their value equals the default.
ATD Annotations¶
Section json¶
Field name¶
Position: after a record field name or variant constructor name
Values: any string
Semantics: specifies an alternate name to use in the JSON representation, overriding the ATD name.
Example:
type color = [
| Red
| Green <json name="green">
| Blue
]
type profile = {
id <json name="user_id">: string;
name: string;
}
A valid JSON object of the profile type above is:
{"user_id": "abc123", "name": "Alice"}
Field repr¶
Position: on a sum type expression (after the closing ]), or on a
list type expression
Values for sum types: "object"
Values for list types: "object"
Sum types — repr="object" (externally-tagged object encoding):
Semantics: tagged variants are encoded as single-key JSON objects
{"Constructor": payload} instead of the default two-element array
["Constructor", payload]. Unit variants (no payload) are always
encoded as "Constructor" strings, unchanged.
This matches the externally tagged representation used by Serde (Rust) and is a compact choice when interoperating with languages where single-key objects are more natural than two-element arrays.
Example:
type shape = [
| Circle of float
| Square of float
| Point
] <json repr="object">
Valid JSON values:
{"Circle": 3.14}{"Square": 1.0}"Point"← unit variant, still a string
Association lists — repr="object":
Semantics: a (string * 'a) list type annotated with <json repr="object">
is encoded as a JSON object {"key": value, ...} instead of the default
array-of-pairs encoding.
Example:
type string_map = (string * int) list <json repr="object">
Valid JSON: {"a": 1, "b": 2}
Section ocaml¶
Field attr¶
Position: on a type definition, i.e. on the left-hand side just before the
equal sign =
Values: the contents of a ppx annotation without the enclosing [@@ and
]
Semantics: appends a ppx attribute to the generated OCaml type definition,
in both the .mli and .ml files.
Example:
type point <ocaml attr="deriving show, eq"> = {
x: float;
y: float;
}
translates to
type point = {
x: float;
y: float;
}
[@@deriving show, eq]
This is useful for attaching ppx rewriters such as ppx_deriving or
ppx_yojson_conv to generated types. Note that the ppx library must be
present in the build environment; atdml does not provide it.
Field default¶
Position: after a record field name marked with a ~ symbol
Values: any valid OCaml expression
Semantics: specifies an explicit default value for the field, overriding
the implicit default (false for bool, 0 for int, ""
for string, etc.). When reading JSON, the default is used when the
field is absent.
Example:
type settings = {
~retries <ocaml default="3">: int;
~verbose: bool;
~label <ocaml default="\"unnamed\"">: string;
}
Field field_prefix¶
Position: on a record type expression (after the closing })
Values: any string that, when prepended to a field name, forms a valid
OCaml identifier prefix (e.g. "t_", "my_record_")
Semantics: prepends the given string to each OCaml record field name in the generated type definition and in all serialization/deserialization code. The prefix is applied to the raw ATD field name before checking for OCaml keyword conflicts, so the result is always a valid, unique OCaml identifier. The JSON field names are unaffected.
The labeled arguments of the generated create_ function are not
prefixed: they use the ATD field names directly (with keyword escaping
applied to the unprefixed names). This allows callers to write
create_point ~x ~y regardless of what prefix the type definition uses
internally.
Example:
(* ATD *)
type point = {
x: float;
y: float;
} <ocaml field_prefix="p_">
(* generated OCaml *)
type point = {
p_x: float;
p_y: float;
}
val create_point : x:float -> y:float -> unit -> point
Keyword escaping is applied independently to labels and to prefixed field
names. For example, with prefix "mod":
ATD field
ule: label~ule, field namemodule_("mod" ^ "ule"="module", which is a keyword, so"_"is appended)ATD field
if: label~if_("if"is a keyword), field namemodif("mod" ^ "if"="modif", which is not a keyword)
Field name¶
Position: after a variant constructor name
Values: any string making a valid OCaml constructor name
Semantics: specifies an alternate OCaml constructor name, overriding the
ATD name in the generated type definition. The JSON name is unaffected
(use <json name="..."> to change the JSON name).
Example:
type shape = [
| Dot <ocaml name="Point">
| Arc <json name="arc"> <ocaml name="ArcShape"> of float
]
translates to
type shape =
| Point
| ArcShape of float
while the JSON representation still uses "Dot" and "arc".
Fields private and public¶
Position: on a type definition (left-hand side before the =)
These flags control whether the generated .mli declares the type as
private.
<ocaml private>Forces a
privatemodifier on the type in the.mli. Works on any type (record, sum type, or alias). The.mlis unaffected.<ocaml public>Suppresses the default
privatethat atdml emits for unparameterized primitive aliases (see Primitive type aliases).
Example:
(* ATD *)
type id = string (* private by default *)
type open_id <ocaml public> = string (* suppress private *)
type ids <ocaml private> = id list (* force private *)
type point <ocaml private> = { (* force private on record *)
x: float;
y: float;
}
(* generated .mli *)
type id = private string
type open_id = string
type ids = private id list
type point = private {
x: float;
y: float;
}
Field repr¶
Position: on a sum type expression (after the closing ])
Values: "poly"
Semantics: when repr="poly", the sum type is represented using OCaml
polymorphic variants (`` Foo`) instead of regular variants (Foo).
Example:
type status = [
| Active
| Inactive
| Pending of string
] <ocaml repr="poly">
translates to
type status = [
| `Active
| `Inactive
| `Pending of string
]
The JSON representation is identical regardless of whether polymorphic or regular variants are used.
Fields module, t, wrap, and unwrap¶
Using a custom wrapper¶
The built-in wrap type constructor allows adding a layer of abstraction
on top of the concrete type used for JSON serialization. The JSON
representation is unchanged; only the OCaml type is different.
Position: after a wrap type constructor
A common use case is to parse strings used as unique identifiers and
wrap the result into an abstract type. Given an OCaml module Uid that
provides:
type t
val wrap : string -> t
val unwrap : t -> string
the following ATD definition:
type uid = string wrap <ocaml module="Uid">
generates an OCaml type uid = Uid.t with serialization going through
Uid.wrap and Uid.unwrap.
It is also possible to specify t, wrap, and unwrap inline,
without a module:
type uid = string wrap <ocaml t="Uid.t" wrap="Uid.wrap" unwrap="Uid.unwrap">
The module field can be combined with t to override the type name
while keeping the module’s wrap/unwrap:
type uid = string wrap <ocaml module="Uid" t="Uid.uid">
Without any annotation, the wrap constructor has no effect on the OCaml
representation; wrap and unwrap both default to the identity function,
making the type a transparent alias for the inner type:
(* No annotation: behaves like 'type tag = string' *)
type tag = string wrap
The individual fields and their semantics:
module: an OCaml module nameM. Defaults fort,wrap, andunwrapbecomeM.t,M.wrap, andM.unwrap.t: overrides the OCaml type name.wrap: an OCaml expression used to convert from the serialized type to the abstract type (called on deserialization).unwrap: an OCaml expression used to convert from the abstract type to the serialized type (called on serialization).
Optional and default record fields¶
ATD provides two ways to declare record fields that may be absent in JSON.
Optional fields (?)¶
A field prefixed with ? is optional. In OCaml, its type is 'a option.
When missing from the JSON object, it deserializes to None. When
serializing, a None value causes the field to be omitted from the JSON
object.
(* ATD *)
type person = {
name: string;
?email: string option;
}
The create_person function uses an OCaml optional argument for email:
val create_person : name:string -> ?email:string -> unit -> person
Note that ?email:string (not ?email:string option) — OCaml wraps the
value in Some implicitly.
With-default fields (~)¶
A field prefixed with ~ has a default value. When missing from the JSON
object, it deserializes to the default. When serializing, a value equal to the
default is omitted from the JSON object.
The default value is determined as follows:
If an
<ocaml default="EXPR">annotation is present,EXPRis used.Otherwise, the implicit default for the type is used:
falseforbool,0forint,0.forfloat,""forstring,[]for lists.
(* ATD *)
type config = {
~timeout: int;
~verbose: bool;
~prefix <ocaml default="\"api_\"">: string;
}
The create_config function uses OCaml optional arguments for all
with-default fields:
val create_config : ?timeout:int -> ?verbose:bool -> ?prefix:string -> unit -> config
Mutually recursive types¶
When two or more ATD types refer to each other, atdml detects the mutual
recursion and emits them as a type ... and ... group and let rec ...
and ... function groups. Types that are not mutually recursive are emitted
independently with plain type and let definitions, which keeps the
generated code easy to read and helps the OCaml compiler process each
definition efficiently.
Example:
(* ATD *)
type tree = [
| Leaf
| Node of node
]
type node = {
value: int;
children: (tree * tree);
}
These two types are mutually recursive, so atdml generates:
type node = {
value: int;
children: (tree * tree);
}
and tree =
| Leaf
| Node of node
let rec node_of_yojson ... = ...
and tree_of_yojson ... = ...
Parametric types¶
ATD type parameters map directly to OCaml type parameters and to higher-order function arguments in the conversion functions. Example:
(* ATD *)
type 'a result = [
| Ok of 'a
| Error of string
]
generates
type 'a result =
| Ok of 'a
| Error of string
val result_of_yojson : (Yojson.Safe.t -> 'a) -> Yojson.Safe.t -> 'a result
val yojson_of_result : ('a -> Yojson.Safe.t) -> 'a result -> Yojson.Safe.t
JSON adapters¶
JSON adapters are a mechanism for rearranging JSON data on-the-fly so as to
make it compatible with ATD, without modifying the ATD type definitions. They
are particularly useful when a JSON API uses conventions that differ from the
ATD defaults — for instance when sum types are encoded as objects with a
"type" discriminator field rather than as ["Constructor", ...] arrays.
The user-provided adapter module must implement two functions for the Yojson path, plus an optional third function for the jsonlike path:
val normalize : Yojson.Safe.t -> Yojson.Safe.t
(** Convert from the original JSON to ATD-compatible JSON.
Called before deserialization via of_yojson. *)
val restore : Yojson.Safe.t -> Yojson.Safe.t
(** Convert from ATD-compatible JSON back to the original JSON.
Called after serialization via yojson_of. *)
val normalize_jsonlike : Atd_jsonlike.AST.t -> Atd_jsonlike.AST.t
(** Optional. Convert from the original jsonlike AST to ATD-compatible form.
Called before deserialization via of_jsonlike. If absent, of_jsonlike
will not apply any adapter transformation for this type. *)
Field adapter.ocaml¶
Position: on a sum type or record type expression
Value: an OCaml module identifier providing normalize, restore, and
optionally normalize_jsonlike
Example — adapting a type-field encoded sum type:
type image = { url: string }
type text = { title: string; body: string }
type document = [
| Image of image
| Text of text
] <json adapter.ocaml="My_adapter">
With the following adapter module:
(* My_adapter.ml *)
(* Converts {"type": "Image", ...rest} <-> ["Image", {...rest}] *)
let normalize = function
| `Assoc fields ->
let tag = match List.assoc_opt "type" fields with
| Some (`String s) -> s
| _ -> failwith "missing 'type' field"
in
let rest = List.filter (fun (k, _) -> k <> "type") fields in
`List [`String tag; `Assoc rest]
| x -> x
let restore = function
| `List [`String tag; `Assoc rest] ->
`Assoc (("type", `String tag) :: rest)
| x -> x
let normalize_jsonlike = function
| Atd_jsonlike.AST.Object (loc, fields) ->
let tag = match List.find_opt (fun (_, k, _) -> k = "type") fields with
| Some (_, _, Atd_jsonlike.AST.String (_, s)) -> s
| _ -> failwith "missing 'type' field"
in
let rest = List.filter (fun (_, k, _) -> k <> "type") fields in
Atd_jsonlike.AST.Array (loc,
[Atd_jsonlike.AST.String (loc, tag); Atd_jsonlike.AST.Object (loc, rest)])
| x -> x
ATD-compatible JSON values for document:
["Image", {"url": "https://example.com/pic.jpg"}]["Text", {"title": "Hello", "body": "World"}]
Original JSON values (as produced and consumed by the adapter):
{"type": "Image", "url": "https://example.com/pic.jpg"}{"type": "Text", "title": "Hello", "body": "World"}
Fields adapter.to_ocaml and adapter.from_ocaml¶
An alternative form that allows specifying the normalize and restore
functions as inline OCaml expressions, without requiring a dedicated module:
type document = [
| Image of image
| Text of text
]
<json
adapter.to_ocaml="My_adapter.normalize"
adapter.from_ocaml="My_adapter.restore"
>
Both fields must be provided together. The values are OCaml expressions of
type Yojson.Safe.t -> Yojson.Safe.t.
Note
Inline adapters (adapter.to_ocaml / adapter.from_ocaml) do not
support normalize_jsonlike. Use adapter.ocaml with a module if
jsonlike adapter support is needed.
Import declarations¶
Atdml supports ATD from ... import declarations that reference individual
types defined in other ATD modules.
Syntax¶
from module_name import type1, type2, ...
from long.module.path as alias import 'a param_type, plain_type
Each type to be used must be listed explicitly in the import statement.
Parameterized types include their arity as type variable placeholders
(e.g. 'a t or ('a, 'b) pair); these enforce that the same arity
is used consistently throughout the importing file.
The imported module maps to an OCaml module. For a simple import like
from base_types import person_name, atdml expects an OCaml module
Base_types (the last component of the path, capitalised) to be
available in scope.
For a dotted path like from long.module.path import tag, atdml expects
the OCaml module path Long.Module.Path to be in scope.
For an alias like from long.module.path as ext import tag, the generated
code uses Ext as the local module name in the .ml file (with
module Ext = Long.Module.Path) while the generated .mli uses the
full Long.Module.Path name directly.
Language-specific name annotation¶
The <ocaml name="M"> annotation on the module path overrides the OCaml
module name used in generated code:
from foo <ocaml name="Foo_module"> import bar
from long.path as ext <ocaml name="External"> import baz
Example¶
Given base_types.atd:
type person_name = string
And greeting.atd:
from base_types import person_name
type greeting = {
name: base_types.person_name;
message: string;
}
Atdml generates greeting.mli:
type greeting = {
name: Base_types.person_name;
message: string;
}
val create_greeting : name:Base_types.person_name -> message:string -> unit -> greeting
val greeting_of_yojson : Yojson.Safe.t -> greeting
val yojson_of_greeting : greeting -> Yojson.Safe.t
...