oaspec
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 615 unit tests, ShellSpec CLI tests, 40 integration compile tests, and 179 test fixtures (including 94 OSS-derived edge-case specs)
Why oaspec
- 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:
gen/my_api/
types.gleam
decode.gleam
encode.gleam
request_types.gleam
response_types.gleam
middleware.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
middleware.gleam
guards.gleam (only if schemas have validation constraints)
client.gleamExample generated code:
/// 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"
}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.
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
oaspecfrom the latest release and run it withescript oaspec <command>. Erlang/OTP 27+ must be on yourPATH.
Build from source (all platforms)
Requires Gleam 1.15+, Erlang/OTP 27+, and rebar3. Works on Linux, macOS, and Windows.
git clone https://github.com/nao1215/oaspec.git
cd oaspec
gleam deps download
gleam run -m gleescriptOn Linux/macOS, move the binary into your PATH:
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
- Create a config file.
oaspec init-
Edit
oaspec.yaml.
input: openapi.yaml
package: my_api
output:
dir: ./gen- Run the generator.
oaspec generate --config=oaspec.yaml
You can also run gleam run -- generate --config=oaspec.yaml.
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 |
output.dir | no | ./gen | Base output directory |
output.server | no | <dir>/<package> | Server output path |
output.client | no | <dir>_client/<package> | Client output path |
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 |
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:
oaspec validate --config=oaspec.yamlCI integration
Use --check and --fail-on-warnings to verify generated code stays in sync:
# Fail if generated code would differ from what's committed
oaspec generate --config=oaspec.yaml --check --fail-on-warningsBest 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 typedadditionalProperties -
References: local
$refresolution for schemas, parameters, request bodies, responses, and path items, including circular-reference detection -
Parameters: path, query, header, and cookie parameters, including array serialization and
style: deepObject -
Request bodies:
application/json,application/x-www-form-urlencoded, andmultipart/form-data -
Responses: typed status-code variants,
$refresponses,defaultresponses, 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
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 xmlannotations are not handled by the parser- Some fields are parsed and preserved but not yet used by codegen: webhooks, externalDocs, tags, examples, links, response headers, encoding
- Operation-level and path-level server overrides are supported in generated clients (precedence: operation > path > top-level)
- The following are normalized to supported equivalents:
const: String const normalized to single-value enumtype: [T, null]: Normalized to nullabletype: [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 |
middleware.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 |
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
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 filesgenerate.validate_only(spec, config)— run validation without code generation
Example: generate files from a parsed spec
import oaspec/config
import oaspec/generate
import oaspec/openapi/parser
let assert Ok(spec) = parser.parse_file("openapi.yaml")
let cfg = config.Config(
input: "openapi.yaml",
output_server: "./gen/my_api",
output_client: "./gen_client/my_api",
package: "my_api",
mode: config.Both,
)
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
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 for tool versions and just as a task runner.
mise install
just check
just shellspec
just integrationTest 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 |