Packages
oaspec
0.15.0
0.68.0
0.67.0
0.66.0
0.65.0
0.64.0
0.63.0
0.62.0
0.61.0
0.60.0
0.59.0
0.58.1
0.58.0
0.57.0
0.56.0
0.55.0
0.54.0
0.53.0
0.52.0
0.51.0
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.0
0.40.0
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.3
0.6.1
0.6.0
0.5.0
0.4.0
0.3.0
0.1.3
Generate Gleam code from OpenAPI 3.x specifications
Current section
Files
Jump to
Current section
Files
README.md
# oaspec
[](https://hex.pm/packages/oaspec)
[](https://hex.pm/packages/oaspec)
[](https://github.com/nao1215/oaspec/actions/workflows/ci.yml)
Generate usable Gleam code from OpenAPI 3.x specifications.
`oaspec` is aimed at practical, typed code generation rather than a feature checklist. It handles the OpenAPI cases that tend to break real projects, such as `$ref` resolution, `allOf`, `oneOf` and `anyOf`, `deepObject` query parameters, form bodies, multipart bodies, and multiple security schemes, while failing fast when a spec goes outside the supported subset.
- Generate client and server-side modules from a single spec
- Produce readable Gleam types, encoders, decoders, request types, and response types
- Handle real-world OpenAPI patterns: unions, nullable fields, `additionalProperties`, form bodies, multipart, and security
- Backed by 684 unit tests, ShellSpec CLI tests, 40 integration compile tests, and 223 test fixtures (including 94 OSS-derived edge-case specs)
## Why oaspec?
**oaspec is the OpenAPI code generator built for Gleam.** Generated code
is regular Gleam: no templates, no runtime magic, type-safe end to end.
| | `oaspec` | Generic multi-language generators (e.g. openapi-generator) | Single-language generators for other targets (e.g. oapi-codegen for Go) |
|-----------------------------------------------------------|:--------:|:----------------------------------------------------------:|:----------------------------------------------------------------------:|
| First-class Gleam output | Yes | No | No |
| Idiomatic types, decoders, encoders, and request/response records | Yes | Templated, not always idiomatic | Language-specific |
| Refuses to emit broken code on unsupported spec patterns | Yes | Sometimes | Partial |
- Built for Gleam: the generated code is shaped like normal Gleam modules, not generic templates awkwardly translated from another ecosystem.
- Focused on practical OpenAPI: coverage is strongest around the features teams actually ship with, not just toy Petstore specs.
- Strict by default: unsupported features are reported explicitly instead of being silently dropped into broken output.
## What you get
Given one OpenAPI spec, `oaspec` generates modules you can keep in your repository:
```text
gen/my_api/
types.gleam
decode.gleam
encode.gleam
request_types.gleam
response_types.gleam
guards.gleam (only if schemas have validation constraints)
handlers.gleam
router.gleam
gen_client/my_api/
types.gleam
decode.gleam
encode.gleam
request_types.gleam
response_types.gleam
guards.gleam (only if schemas have validation constraints)
client.gleam
```
Example generated code:
```gleam
/// A pet in the store
pub type Pet {
Pet(
id: Int,
name: String,
status: PetStatus,
tag: Option(String),
)
}
pub type PetStatus {
PetStatusAvailable
PetStatusPending
PetStatusSold
}
pub fn create_pet(config: ClientConfig, body: types.CreatePetRequest)
-> Result(response_types.CreatePetResponse, ClientError) {
// ...
}
pub fn list_pets(req: request_types.ListPetsRequest)
-> response_types.ListPetsResponse {
let _ = req
panic as "unimplemented: list_pets"
}
```
## Is oaspec right for your spec?
A one-minute check before you paste in your OpenAPI document — if your
spec stays inside the green list below, `oaspec` will generate code; if
it relies on anything in the red list, generation stops with a clear
diagnostic instead of producing broken output.
**Generates code for:**
- Schemas: `object`, primitives, arrays, enums, nullable, `allOf`,
`oneOf`, `anyOf`, typed `additionalProperties`
- Local `$ref` (and relative-file external `$ref`) across schemas,
parameters, request bodies, responses, and path items
- Parameters: path, query, header, cookie, plus array styles (`form`,
`pipeDelimited`, `spaceDelimited`) and objects via `deepObject`
- Request bodies: `application/json`,
`application/x-www-form-urlencoded`, `multipart/form-data`
- Typed response variants, typed response headers, and `$ref` /
`default` responses
- Security: `apiKey`, HTTP (bearer/basic/digest), OAuth2, OpenID Connect
(bearer token attachment)
**Stops with a diagnostic for:**
- JSON Schema 2020 keywords: `$defs`, `prefixItems`, `if/then/else`,
`dependentSchemas`, `not`, `unevaluatedProperties` /
`unevaluatedItems`, `contentEncoding` / `contentMediaType` /
`contentSchema`
- XML request/response bodies with structural decoding, `xml`
annotations, and `mutualTLS` security
**Parsed but not yet turned into code:** callbacks, webhooks,
`externalDocs`, tags, examples, links, `encoding` metadata.
See [Current Boundaries](#current-boundaries) for the full list,
including server-mode restrictions and normalization rules. The
boundaries are kept in sync with the capability registry at
[`src/oaspec/capability.gleam`](src/oaspec/capability.gleam) by a
drift-detection test.
## Quickstart
### Install from GitHub release (Linux / macOS)
Requires Erlang/OTP 27+. The release binary is an Erlang escript that runs on any platform with Erlang installed.
```sh
curl -fSL -o oaspec https://github.com/nao1215/oaspec/releases/latest/download/oaspec
chmod +x oaspec
sudo mv oaspec /usr/local/bin/
```
> On Windows, download `oaspec` from the [latest release](https://github.com/nao1215/oaspec/releases/latest) and run it with `escript oaspec <command>`. Erlang/OTP 27+ must be on your `PATH`.
### Build from source (all platforms)
Requires Gleam 1.15+, Erlang/OTP 27+, and `rebar3`. Works on Linux, macOS, and Windows.
```sh
git clone https://github.com/nao1215/oaspec.git
cd oaspec
gleam deps download
gleam run -m gleescript
```
On Linux/macOS, move the binary into your PATH:
```sh
sudo mv oaspec /usr/local/bin/
```
On Windows, move `oaspec` to a directory on your `PATH` and run it with `escript oaspec <command>`.
### Generate code
1. Create a config file.
```sh
oaspec init
```
2. Edit `oaspec.yaml`.
```yaml
input: openapi.yaml
package: my_api
output:
dir: ./gen
```
3. Run the generator.
```sh
oaspec generate --config=oaspec.yaml
```
You can also run `gleam run -- generate --config=oaspec.yaml`.
### Runnable examples
Working examples live under [`examples/`](./examples):
- [`examples/petstore_client`](./examples/petstore_client) — minimal client usage against a canned HTTP transport. Run it from the repo root with `just example-petstore`.
- [`examples/server_adapter`](./examples/server_adapter) — wires the generated `router.route/5` to a framework-free adapter. Run it from the repo root with `just example-server-adapter`.
## Configuration
Generated server code is written to `<dir>/<package>`. Generated client code is written to `<dir>_client/<package>`. The basename of each output directory must match `package` so imports such as `import my_api/types` resolve correctly.
| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `input` | yes | - | Path to an OpenAPI 3.x spec in YAML or JSON |
| `package` | no | `api` | Gleam module namespace prefix |
| `mode` | no | `both` | `server`, `client`, or `both` |
| `validate` | no | `false` | Enable guard validation in generated server/client code |
| `output.dir` | no | `./gen` | Base output directory |
| `output.server` | no | `<dir>/<package>` | Server output path |
| `output.client` | no | `<dir>_client/<package>` | Client output path |
### Configuration paths
All path-valued fields — `input`, `output.dir`, `output.server`,
`output.client` — are resolved **relative to the current working
directory** when oaspec runs, not the directory the config file lives
in.
A config at the repo root that refers to a sibling spec works with no
prefix:
```text
myproject/
├── oaspec.yaml # input: openapi.yaml
└── openapi.yaml
```
```sh
cd myproject
oaspec generate --config=oaspec.yaml # resolves ./openapi.yaml
```
If the config lives in a subdirectory, its `input` must be reachable
from where the command is run, so either use a path relative to that
CWD or keep invoking oaspec from the config's own directory:
```text
myproject/
├── api/
│ ├── oaspec.yaml # input: openapi.yaml
│ └── openapi.yaml
└── (other code)
```
```sh
cd myproject/api
oaspec generate --config=oaspec.yaml # resolves ./openapi.yaml
# or, from the repo root:
oaspec generate --config=api/oaspec.yaml # needs input: api/openapi.yaml
```
Output directories (`output.dir`, `output.server`, `output.client`)
are created automatically if they do not exist; existing files in the
target directories are overwritten by the newly generated code.
If the input spec or the config file itself cannot be opened, oaspec
exits with a `Config file not found` / `parse_file` diagnostic that
includes the path it attempted to read.
### CLI commands
| Command | Description |
|---------|-------------|
| `oaspec generate` | Generate Gleam code from an OpenAPI specification |
| `oaspec validate` | Validate an OpenAPI specification without generating code |
| `oaspec init` | Create a default `oaspec.yaml` config file |
### CLI options for `generate`
| Flag | Default | Description |
|------|---------|-------------|
| `--config=<path>` | `./oaspec.yaml` | Path to config file |
| `--mode=<mode>` | `both` | `server`, `client`, or `both` (overrides config) |
| `--output=<path>` | - | Override output base directory |
| `--check` | `false` | Check that generated code matches existing files without writing |
| `--fail-on-warnings` | `false` | Treat warnings as errors |
| `--validate` | `false` | Enable guard validation in generated server/client code |
### CLI options for `validate`
| Flag | Default | Description |
|------|---------|-------------|
| `--config=<path>` | `./oaspec.yaml` | Path to config file |
| `--mode=<mode>` | `both` | `server`, `client`, or `both` (overrides config) |
### Validate
Check a spec for unsupported patterns without generating code:
```sh
oaspec validate --config=oaspec.yaml
```
### Guard validation
By default, generated code does not validate request bodies at runtime. Enable `validate` in the config file or pass `--validate` to `generate` to add schema-constraint checks:
```yaml
validate: true
```
```sh
oaspec generate --config=oaspec.yaml --validate
```
When enabled, generated routers validate request bodies against schema constraints and return 422 on failure. Generated clients validate request bodies before sending.
### CI integration
Use `--check` and `--fail-on-warnings` to verify generated code stays in sync:
```sh
# Fail if generated code would differ from what's committed
oaspec generate --config=oaspec.yaml --check --fail-on-warnings
```
## Best For
- Generating typed Gleam clients from an OpenAPI contract
- Keeping request and response types in sync with an external API spec
- Bootstrapping server-side types, handlers, and router support from the same source spec
- Catching unsupported spec features early in CI instead of after code generation
## OpenAPI Support
`oaspec` supports OpenAPI 3.0.x and a practical subset of OpenAPI 3.1 in YAML or JSON.
Coverage is strongest in these areas:
- Schemas: component schemas, primitive aliases, enums, nullable fields, arrays, objects, `allOf`, `oneOf`, `anyOf`, and typed `additionalProperties`
- References: local `$ref` resolution for schemas, parameters, request bodies, responses, and path items, including circular-reference detection
- Parameters: path, query, header, and cookie parameters, including array serialization (`style: form`, `style: pipeDelimited`, `style: spaceDelimited`) and objects via `style: deepObject`
- Request bodies: `application/json`, `application/x-www-form-urlencoded`, and `multipart/form-data`
- Responses: typed status-code variants, `$ref` responses, `default` responses, typed response headers, and text or binary passthrough cases
- Security: `apiKey` (header, query, cookie), HTTP auth (bearer, basic, digest), OAuth2, and OpenID Connect. For OAuth2 and OpenID Connect, the generated client attaches a bearer token to requests; token acquisition, refresh, and flow execution are outside the generated code.
- Generation safety: name collision handling, keyword escaping, validation guards, and capability errors with clear failure modes
<!-- BEGIN GENERATED:BOUNDARIES -->
## Current Boundaries
These boundaries are generated from the capability registry in `src/oaspec/capability.gleam`.
These are the most important limitations today:
- The following keywords are detected and rejected: `$defs`, `prefixItems`, `if/then/else`, `dependentSchemas`, `not`, `unevaluatedProperties`, `unevaluatedItems`, `contentEncoding`, `contentMediaType`, `contentSchema`, `mutualTLS`
- `xml` annotations are not handled by the parser
- Some fields are parsed and preserved but not yet used by codegen: callbacks, webhooks, externalDocs, tags, examples, links, encoding
- Operation-level and path-level server overrides are supported in generated clients (precedence: operation > path > top-level)
- Server-mode code generation rejects the following spec configurations (supported in client mode): `server: complex path parameters`, `server: non-primitive query array items`, `server: non-primitive header array items`, `server: complex deepObject properties`, `server: mixed form-urlencoded request`, `server: complex form-urlencoded fields`, `server: mixed multipart request`, `server: complex multipart fields`, `server: unsupported request content type`
- The following are normalized to supported equivalents:
- `const`: String const normalized to single-value enum
- `type: [T, null]`: Normalized to nullable
- `type: [T1, T2]`: Normalized to oneOf
<!-- END GENERATED:BOUNDARIES -->
## Mode-Specific Support
`oaspec` generates different files depending on the `--mode` flag. Some features have mode-specific restrictions enforced at validation time.
### Generated files
| File | server | client |
|------|--------|--------|
| `types.gleam` | yes | yes |
| `decode.gleam` | yes | yes |
| `encode.gleam` | yes | yes |
| `request_types.gleam` | yes | yes |
| `response_types.gleam` | yes | yes |
| `guards.gleam` | yes | yes |
| `handlers.gleam` | yes | - |
| `router.gleam` | yes | - |
| `client.gleam` | - | yes |
### Feature restrictions by mode
| Feature | server | client | Notes |
|---------|--------|--------|-------|
| JSON request/response bodies | yes | yes | |
| Path / query / header / cookie parameters | yes | yes | |
| `style: deepObject` parameters | restricted | yes | Server: only primitive scalars and primitive arrays |
| Array query parameters | restricted | yes | Server: only inline primitive item schemas |
| `style: pipeDelimited` / `style: spaceDelimited` query arrays | yes | yes | Query array parameters only; primitive item types. Non-exploded joins with `\|` / `%20`, exploded degenerates to form-style `name=a&name=b`. |
| `application/x-www-form-urlencoded` | restricted | yes | Server: must be sole content type; only primitive fields and shallow nested objects |
| `multipart/form-data` | restricted | yes | Server: must be sole content type; only primitive scalar fields |
| Security (apiKey, HTTP, OAuth2, OpenID Connect) | yes | yes | Client attaches credentials via config; OAuth2/OpenID Connect: bearer token only |
## Library API
`oaspec` can be used as a Gleam library, not just a CLI tool. The generation pipeline is pure (no IO) and split into composable steps.
### Pipeline overview
```text
parse → normalize → resolve → capability check → hoist → dedup → validate → codegen
```
The `oaspec/generate` module wraps this pipeline into two entry points:
- `generate.generate(spec, config)` — run the full pipeline and return generated files
- `generate.validate_only(spec, config)` — run validation without code generation
### Example: generate files from a parsed spec
```gleam
import oaspec/config
import oaspec/generate
import oaspec/openapi/parser
let assert Ok(spec) = parser.parse_file("openapi.yaml")
let cfg = config.new(
input: "openapi.yaml",
output_server: "./gen/my_api",
output_client: "./gen_client/my_api",
package: "my_api",
mode: config.Both,
validate: False,
)
case generate.generate(spec, cfg) {
Ok(summary) -> {
// summary.files: List(GeneratedFile) — path and content for each file
// summary.warnings: List(Diagnostic) — non-blocking warnings
// summary.spec_title: String
}
Error(generate.ValidationErrors(errors:)) -> {
// errors: List(Diagnostic) — blocking validation errors
}
}
```
### Example: validate without generating
```gleam
case generate.validate_only(spec, cfg) {
Ok(summary) -> // spec is valid; summary.warnings may be non-empty
Error(generate.ValidationErrors(errors:)) -> // spec has errors
}
```
### Key modules
| Module | Purpose |
|--------|---------|
| `oaspec/openapi/parser` | Parse YAML/JSON spec into `OpenApiSpec(Unresolved)` |
| `oaspec/config` | Load config from YAML or construct programmatically |
| `oaspec/generate` | Pure generation pipeline (parse → codegen) |
| `oaspec/codegen/writer` | Write generated files to disk |
| `oaspec/openapi/diagnostic` | Structured warnings and errors |
## Development
This project uses [mise](https://mise.jdx.dev/) for tool versions and [just](https://just.systems/) as a task runner.
```sh
mise install
just check
just shellspec
just integration
```
Test structure:
| Command | Tool | What it tests |
|---------|------|---------------|
| `just test` | gleeunit | Parser, validator, naming, config, collision detection |
| `just shellspec` | ShellSpec | CLI behaviour, file generation, content, unsupported feature detection |
| `just integration` | gleeunit | Generated code compiles and the generated modules work together |
## License
[MIT](LICENSE)