Packages
oaspec
0.3.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://github.com/nao1215/oaspec/actions/workflows/ci.yml)
[](https://github.com/nao1215/oaspec/actions/workflows/integration.yml)
> [!IMPORTANT]
> oaspec does not cover the full OpenAPI 3.x specification. Support is expanded incrementally.
Generate Gleam code from OpenAPI 3.x specifications.
- Custom types for component schemas
- JSON decoders and encoders (allOf, oneOf/anyOf with discriminator)
- Server handler stubs
- Client SDK with parameter serialization and response decoding
- Middleware (logging, retry)
- Security scheme support (apiKey header/query, Bearer token)
- OpenAPI descriptions as doc comments
## Install
### From GitHub Release
Download the `oaspec` escript binary from the [Releases](https://github.com/nao1215/oaspec/releases) page. Requires Erlang/OTP 27+.
```sh
curl -fSL -o oaspec https://github.com/nao1215/oaspec/releases/latest/download/oaspec
chmod +x oaspec
sudo mv oaspec /usr/local/bin/
```
### From source
Requires Gleam 1.15+, Erlang/OTP 27+, and rebar3.
```sh
git clone https://github.com/nao1215/oaspec.git
cd oaspec
gleam deps download
gleam run -m gleescript # produces ./oaspec escript binary
sudo mv oaspec /usr/local/bin/
```
## Usage
### 1. Create a config file
```sh
oaspec init
```
This creates `oaspec.yaml` with a commented template. Edit it for your project:
```yaml
input: openapi.yaml
package: my_api
output:
dir: ./gen # base directory (default: ./gen)
```
Generated code is placed at `<dir>/<package>` (server) and `<dir>_client/<package>` (client). Both directory basenames must match `package` so that Gleam imports resolve correctly. Copy or symlink the output into `src/` to use it.
| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `input` | yes | - | Path to OpenAPI 3.x spec (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 code output path |
| `output.client` | no | `<dir>_client/<package>` | Client code output path |
The directory basename must match `package` so that `import my_api/types` resolves. The CLI `--output` flag works the same as `output.dir`. A mismatch is an early error.
### 2. Run the generator
```sh
oaspec generate --config=oaspec.yaml
```
Options:
```
--config=<path> Path to config file (default: ./oaspec.yaml)
--mode=<mode> server, client, or both (default: both)
--output=<path> Override output base directory
```
You can also run via `gleam run -- generate --config=oaspec.yaml`.
### 3. Generated output
```
gen/my_api/ # server (package = "my_api")
types.gleam # Domain model types
request_types.gleam # Request parameter types
response_types.gleam # Response types (tagged unions by status code)
decode.gleam # JSON decoders
encode.gleam # JSON encoders
middleware.gleam # Middleware types and utilities
handlers.gleam # Handler stubs (TODO placeholders)
router.gleam # Route dispatcher skeleton
gen_client/my_api/ # client
types.gleam
decode.gleam
encode.gleam
middleware.gleam
client.gleam # HTTP client functions
request_types.gleam
response_types.gleam
```
## Generated code examples
Given a Petstore OpenAPI spec:
### Types
```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
}
```
### Server handlers
```gleam
pub fn list_pets(req: request_types.ListPetsRequest) -> response_types.ListPetsResponse {
let _ = req
// TODO: Implement list_pets
todo
}
```
### Client
```gleam
pub fn create_pet(config: ClientConfig, body: types.CreatePetRequest)
-> Result(response_types.CreatePetResponse, ClientError) {
// ...
}
```
### Middleware
```gleam
pub type Handler(req, res) =
fn(req) -> Result(res, MiddlewareError)
pub type Middleware(req, res) =
fn(Handler(req, res)) -> Handler(req, res)
pub fn compose(first: Middleware(req, res), second: Middleware(req, res)) -> Middleware(req, res)
pub fn apply(middlewares: List(Middleware(req, res)), handler: Handler(req, res)) -> Handler(req, res)
pub fn retry(max_retries: Int) -> Middleware(req, res)
```
## OpenAPI support
### Supported
- OpenAPI 3.x (YAML and JSON)
- Paths and operations (GET, POST, PUT, DELETE, PATCH)
- Path, query, header, cookie parameters (path-level merged by `(name, in)`)
- Parameter serialization for Bool, Float, Int, String, `$ref` enum types
- Cookie parameters combined into single header
- Request bodies with `$ref` resolution (typed, auto-encoded)
- allOf in request body (property merging from `$ref` + inline objects)
- Responses with status codes, `$ref` responses from `components.responses`
- `$ref` resolution for parameters, requestBodies, responses, schemas
- Component schemas: types, decoders, encoders
- Primitive component schemas (string, integer, number, boolean): type alias, decoder, encoder
- String enums with unknown-value rejection
- Inline enums in properties (auto-named)
- Inline objects in top-level response/requestBody (anonymous types generated)
- oneOf/anyOf with `$ref` variants: sum types, decoders, encoders
- oneOf discriminator-based decoding
- anyOf try-each decoding
- allOf property merging with decoders/encoders
- Nullable fields, arrays (including `$ref` items)
- Encode/decode roundtrip: `decode(encode(value)) == Ok(value)`
- Circular `$ref` detection
- Fail-fast parser for missing required fields, invalid parameter locations, malformed content
- Client typed body (auto-encoded) and typed response (auto-decoded)
- `default` response handling in client
- Top-level security inheritance (operation-level overrides, `security: []` opts out)
- Security schemes: `apiKey` in header/query, HTTP bearer (first OR alternative applied; AND within one alternative supported)
- Duplicate operationId detection
- Function/type name collision detection after case conversion
- Config validation: output directory basename must match package name
- Gleam keyword escaping in generated field names
### Unsupported (exits with error)
These are detected before code generation. The generator prints an error and exits non-zero.
- `style: deepObject` query parameters
- `multipart/form-data` request bodies (only `application/json`)
- `additionalProperties: true` (untyped map)
- Typed `additionalProperties`
- Inline oneOf/anyOf schemas (variants must be `$ref`)
- Nested inline object/allOf in properties (use `$ref`)
- Array parameters (query/header/cookie with `type: array`)
- Complex schema parameters (object/allOf/oneOf/anyOf in query/header/cookie)
- Inline complex array items (object/allOf/oneOf/anyOf; use `$ref`)
- Duplicate operationId
- Function/type name collisions after case conversion
- Property name collisions after snake_case conversion
- Enum variant collisions after PascalCase conversion
- Non-JSON response content types (only `application/json`)
- Path parameters with `required: false`
### Not yet supported
- Percent-encoding for path/query/cookie parameter values: values are inserted as-is; reserved characters may break URLs
- Validation constraints (minLength, maxLength, pattern, minimum, maximum): parsed but not enforced
- Callbacks: ignored by the generator
- OAuth2 / OpenID Connect: rejected at parse time
- `apiKey` in cookie: rejected at parse time
- HTTP Basic / Digest: rejected at parse time (only bearer supported)
- allOf with non-object sub-schemas
- `text/plain` and other non-JSON response types
### Schema-to-type mapping
| OpenAPI type | Gleam type |
|-------------|-----------|
| `string` | `String` |
| `integer` | `Int` |
| `number` | `Float` |
| `boolean` | `Bool` |
| `array` | `List(T)` |
| `object` | Custom type |
| `enum` | Custom type with variants |
| nullable | `Option(T)` |
| `allOf` | Merged custom type |
| `oneOf`/`anyOf` (`$ref` variants) | Sum type |
## Development
This project uses [mise](https://mise.jdx.dev/) for tool versions and [just](https://just.systems/) as a task runner.
```sh
mise install # install Gleam, Erlang, rebar3
just check # format check, typecheck, build, unit tests
just shellspec # CLI integration tests (ShellSpec)
just integration # generated code compile + roundtrip tests
```
### 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, types/decoders/encoders/handlers/middleware work |
## License
[MIT](LICENSE)