openapi generates type-safe OCaml API clients from OpenAPI 3.x specifications. The generated code uses:
opam install openapiUse the openapi-gen CLI tool to generate OCaml code from an OpenAPI spec:
# Basic generation
openapi-gen generate spec.json -o ./my_api -n my_api
# With dune regeneration rules
openapi-gen generate spec.json -o ./my_api -n my_api --regen-o, --output — Output directory for generated code (required)-n, --name — Package name for generated library (defaults to API title)--regen — Include dune.inc rules for dune build @gen --auto-promoteThe generator produces a complete dune library:
my_api/
├── dune # Library configuration (wrapped)
├── dune.inc # Regeneration rules (if --regen used)
├── types.ml # Type definitions with jsont codecs
├── types.mli # Type interfaces
├── client.ml # API client functions
├── client.mli # Client interface
├── my_api.ml # Main wrapped module
└── my_api.mli # Main module interfaceAll schema types are generated as modules within Types:
(* Access a type *)
let user : My_api.Types.User.t = {
id = 123;
name = "Alice";
email = Some "alice@example.com";
}
(* Encode to JSON *)
let json = Jsont.encode My_api.Types.User.t_jsont user
(* Decode from JSON *)
let user' = Jsont.decode My_api.Types.User.t_jsont jsonCreate a client and call API operations:
let () =
Eio_main.run @@ fun env ->
Eio.Switch.run @@ fun sw ->
(* Create the client *)
let client = My_api.Client.create ~sw env
~base_url:"https://api.example.com" in
(* Make a request - returns typed value *)
let user = My_api.Client.get_user ~id:"123" client () in
Printf.printf "User: %s\n" user.name
(* List endpoints return typed lists *)
let users = My_api.Client.list_users client () in
List.iter (fun u -> Printf.printf "- %s\n" u.name) usersFor POST/PUT/PATCH requests, pass the typed value directly:
(* Create typed request body *)
let new_user : My_api.Types.CreateUserDto.t = {
name = "Bob";
email = "bob@example.com";
} in
(* Pass as the body parameter - encoding is automatic *)
let created = My_api.Client.create_user ~body:new_user client ()If you used --regen, the generated dune.inc includes rules to regenerate the client when the spec changes:
# Regenerate and promote changes
dune build @gen --auto-promoteThis is useful for CI pipelines to ensure generated code stays in sync with the OpenAPI specification.
Openapi.Spec — OpenAPI 3.x specification types with jsont codecsOpenapi.Codegen — Code generation from spec to OCamlOpenapi.Runtime — Runtime utilities for generated clientsThe Openapi.Runtime module provides helpers used by generated code:
(* Path template rendering *)
Openapi.Runtime.Path.render
~params:[("userId", "123"); ("postId", "456")]
"/users/{userId}/posts/{postId}"
(* => "/users/123/posts/456" *)
(* Query string encoding *)
Openapi.Runtime.Query.encode [("page", "1"); ("limit", "10")]
(* => "?page=1&limit=10" *)Here's a complete example generating a client for the Immich photo server:
# Generate the client
openapi-gen generate immich-openapi-specs.json -o ./immich -n immich
# In your code:
let () =
Eio_main.run @@ fun env ->
Eio.Switch.run @@ fun sw ->
let client = Immich.Client.create ~sw env
~base_url:"http://localhost:2283/api" in
(* List albums *)
let albums_json = Immich.Client.get_all_albums client () in
(* Get server info *)
let info = Immich.Client.get_server_info client () in
...Jsont.json. Proper implementation would generate OCaml variant types with discriminator-based decoding. See Union Types below for details.Jsont.json. Proper implementation would merge all referenced schemas into a single record type.additionalProperties: true become Jsont.json.$ref in parameters are skipped; only inline parameters are used.multipart/form-data is not supported. Binary file uploads require special handling not yet implemented.application/json content types are supported.application/x-www-form-urlencoded is not supported.$ref pointers starting with #/ are supported. External file references are not resolved.To properly support oneOf/anyOf, the generator would need to:
discriminator property if present to determine the tag fieldGenerate a decoder that:
Example of what generated code might look like:
(* For oneOf with discriminator *)
type pet =
| Dog of Dog.t
| Cat of Cat.t
let pet_jsont : pet Jsont.t =
(* Read discriminator field "petType" to determine variant *)
...