OCaml OpenAPI

Overview

openapi generates type-safe OCaml API clients from OpenAPI 3.x specifications. The generated code uses:

Installation

opam install openapi

Generating a Client

Use 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

CLI Options

Generated Code Structure

The 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 interface

Using Generated Code

Accessing Types

All 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 json

Making API Requests

Create 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) users

Request Bodies

For 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 ()

Keeping Generated Code Updated

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-promote

This is useful for CI pipelines to ensure generated code stays in sync with the OpenAPI specification.

Library Modules

Core Modules

Runtime Utilities

The 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" *)

Example: Immich API

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
  ...

Limitations

Schema Generation

Client Generation

Content Types

Advanced Features

Implementing Union Types

To properly support oneOf/anyOf, the generator would need to:

  1. Analyze schemas in the union to determine variant names
  2. Use the discriminator property if present to determine the tag field
  3. Generate an OCaml variant type with one constructor per schema
  4. Generate a decoder that:

    • Reads the discriminator field if present
    • Pattern matches to select the appropriate decoder
    • Falls back to trying each decoder in order for anyOf
  5. Generate an encoder that pattern matches on the variant

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 *)
  ...

See Also