Packages
A comprehensive OpenAPI specification parser for Elixir. Supports OpenAPI 2.0 (Swagger), 3.0, and 3.1 specifications in both JSON and YAML formats with full validation.
Current section
Files
Jump to
Current section
Files
openapi_parser
README.md
README.md
# OpenapiParser
[](https://github.com/tomgeene/openapi_parser/actions/workflows/ci.yml)
[](https://hex.pm/packages/openapi_parser)
[](https://hexdocs.pm/openapi_parser)
A comprehensive OpenAPI specification parser for Elixir that supports:
- **OpenAPI 2.0** (Swagger)
- **OpenAPI 3.0**
- **OpenAPI 3.1**
Both **JSON** and **YAML** formats are supported with full validation.
## Features
- ✅ **Complete OpenAPI Support** - Parse all three major versions
- ✅ **Format Flexible** - JSON and YAML with auto-detection
- ✅ **Comprehensive Validation** - Validates required fields, types, formats, and semantic rules
- ✅ **Type Safe** - Full Elixir typespecs for all structs
- ✅ **Well Tested** - Extensive test coverage with boundary value testing
- ✅ **Production Ready** - Used for building API clients, documentation generators, and validation tools
## Installation
Add `openapi_parser` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:openapi_parser, "~> 0.1.0"}
]
end
```
Then run:
```bash
mix deps.get
```
## Quick Start
### Parse from a String
```elixir
# Parse JSON
json_spec = """
{
"openapi": "3.1.0",
"info": {
"title": "My API",
"version": "1.0.0"
},
"paths": {
"/users": {
"get": {
"responses": {
"200": {
"description": "Success"
}
}
}
}
}
}
"""
{:ok, spec} = OpenapiParser.parse(json_spec, format: :json)
# => {:ok, %OpenapiParser.Spec.OpenAPI{version: :v3_1, document: ...}}
# Parse YAML
yaml_spec = """
openapi: 3.0.0
info:
title: My API
version: 1.0.0
paths:
/users:
get:
responses:
'200':
description: Success
"""
{:ok, spec} = OpenapiParser.parse(yaml_spec, format: :yaml)
```
### Parse from a File
```elixir
# Auto-detects format from file extension
{:ok, spec} = OpenapiParser.parse_file("openapi.json")
{:ok, spec} = OpenapiParser.parse_file("swagger.yaml")
# Access the parsed data
IO.puts(spec.document.info.title)
# => "My API"
IO.puts(spec.document.info.version)
# => "1.0.0"
```
## Parsing Options
All parsing functions accept the following options:
- `:format` - Input format (`:json`, `:yaml`, or `:auto`). Default: `:auto`
- `:validate` - Whether to validate the spec. Default: `true`
- `:resolve_refs` - Whether to resolve `$ref` references. Default: `false`
```elixir
# Parse without validation (faster, but may accept invalid specs)
{:ok, spec} = OpenapiParser.parse(content, validate: false)
# Parse with reference resolution
{:ok, spec} = OpenapiParser.parse_file("spec.yaml", resolve_refs: true)
# Explicit format
{:ok, spec} = OpenapiParser.parse(content, format: :json, validate: true)
```
## Working with Parsed Specs
### Access Spec Information
```elixir
{:ok, spec} = OpenapiParser.parse_file("openapi.yaml")
# Check version
case spec.version do
:v2 -> IO.puts("Swagger 2.0")
:v3_0 -> IO.puts("OpenAPI 3.0")
:v3_1 -> IO.puts("OpenAPI 3.1")
end
# Access info
info = spec.document.info
IO.puts("Title: #{info.title}")
IO.puts("Version: #{info.version}")
IO.puts("Description: #{info.description}")
# Access contact information
if info.contact do
IO.puts("Contact: #{info.contact.name}")
IO.puts("Email: #{info.contact.email}")
end
# Access license
if info.license do
IO.puts("License: #{info.license.name}")
end
```
### Iterate Over Paths
```elixir
{:ok, spec} = OpenapiParser.parse_file("openapi.yaml")
# List all paths
Enum.each(spec.document.paths, fn {path, path_item} ->
IO.puts("Path: #{path}")
# Check which HTTP methods are defined
if path_item.get, do: IO.puts(" - GET")
if path_item.post, do: IO.puts(" - POST")
if path_item.put, do: IO.puts(" - PUT")
if path_item.delete, do: IO.puts(" - DELETE")
end)
```
### Access Operation Details
```elixir
{:ok, spec} = OpenapiParser.parse_file("openapi.yaml")
# Get a specific path
path_item = spec.document.paths["/users/{id}"]
# Access GET operation
if path_item.get do
operation = path_item.get
IO.puts("Operation ID: #{operation.operation_id}")
IO.puts("Summary: #{operation.summary}")
IO.puts("Tags: #{inspect(operation.tags)}")
# List parameters
if operation.parameters do
Enum.each(operation.parameters, fn param ->
IO.puts("Parameter: #{param.name} (#{param.location})")
IO.puts(" Required: #{param.required}")
end)
end
# List responses
Enum.each(operation.responses.responses, fn {status, response} ->
IO.puts("Response #{status}: #{response.description}")
end)
end
```
### Access Components/Schemas
```elixir
{:ok, spec} = OpenapiParser.parse_file("openapi.yaml")
# For OpenAPI 3.x
if spec.document.components do
schemas = spec.document.components.schemas
Enum.each(schemas, fn {name, schema} ->
IO.puts("Schema: #{name}")
IO.puts(" Type: #{schema.type}")
if schema.properties do
IO.puts(" Properties:")
Enum.each(schema.properties, fn {prop_name, prop_schema} ->
IO.puts(" - #{prop_name}: #{prop_schema.type}")
end)
end
end)
end
# For Swagger 2.0
if spec.version == :v2 and spec.document.definitions do
Enum.each(spec.document.definitions, fn {name, schema} ->
IO.puts("Definition: #{name}")
end)
end
```
## Building an API Client
Here's a practical example of using the parser to build an API client:
```elixir
defmodule MyAPIClient do
def build_from_spec(spec_path) do
{:ok, spec} = OpenapiParser.parse_file(spec_path, validate: true)
# Extract server URL
server_url = case spec.version do
:v2 ->
scheme = List.first(spec.document.schemes) || "https"
"#{scheme}://#{spec.document.host}#{spec.document.base_path}"
_ ->
List.first(spec.document.servers).url
end
# Build client functions for each operation
operations = extract_operations(spec.document.paths)
%{
base_url: server_url,
operations: operations,
info: spec.document.info
}
end
defp extract_operations(paths) do
Enum.flat_map(paths, fn {path, path_item} ->
[:get, :post, :put, :delete, :patch]
|> Enum.filter(&Map.get(path_item, &1))
|> Enum.map(fn method ->
operation = Map.get(path_item, method)
%{
method: method,
path: path,
operation_id: operation.operation_id,
summary: operation.summary,
parameters: operation.parameters || []
}
end)
end)
end
end
# Use it
client = MyAPIClient.build_from_spec("api_spec.yaml")
IO.puts("API: #{client.info.title}")
IO.puts("Base URL: #{client.base_url}")
IO.puts("Available operations: #{length(client.operations)}")
```
## Validation
The parser performs comprehensive validation including:
- **Required fields** - Ensures all required fields are present
- **Type checking** - Validates field types (string, integer, boolean, etc.)
- **Format validation** - Validates string formats (email, uri, url, uuid, etc.)
- **Enum validation** - Checks enumerated values
- **Semantic rules** - e.g., path parameters must be required, paths must start with "/"
- **HTTP status codes** - Validates status code formats
- **Content types** - Validates media type formats
### Handling Validation Errors
```elixir
case OpenapiParser.parse(invalid_spec, validate: true) do
{:ok, spec} ->
IO.puts("Valid spec!")
{:error, message} ->
IO.puts("Validation error: #{message}")
# Example: "paths./users.get.parameters[0]: Path parameter must be required"
end
```
## Supported Specifications
### OpenAPI 3.1
- Full JSON Schema 2020-12 support
- Type arrays (e.g., `type: ["string", "null"]`)
- `const` keyword
- License `identifier` field
- And all OpenAPI 3.0 features
### OpenAPI 3.0
- Components (schemas, responses, parameters, etc.)
- Request bodies with multiple content types
- Callbacks and links
- Discriminators
- Security schemes (apiKey, http, oauth2, openIdConnect)
- Server variables
- Cookie parameters
### Swagger 2.0
- Definitions
- Parameters (including formData and file types)
- Responses with headers
- Security definitions
- Global consumes/produces
- Collection formats
## Error Handling
```elixir
# File not found
{:error, message} = OpenapiParser.parse_file("nonexistent.yaml")
# Invalid JSON/YAML
{:error, message} = OpenapiParser.parse("{invalid json", format: :json)
# Missing version
{:error, message} = OpenapiParser.parse("""
{
"info": {"title": "API", "version": "1.0.0"},
"paths": {}
}
""")
# => {:error, "Missing version field (swagger or openapi)"}
# Validation errors
{:error, message} = OpenapiParser.parse("""
{
"openapi": "3.0.0",
"info": {"title": "API", "version": "1.0.0"}
}
""", validate: true)
# => {:error, "paths is required"}
```
## Advanced Usage
### Custom Validation
```elixir
defmodule MyValidator do
def validate_custom_rules(spec) do
with {:ok, spec} <- OpenapiParser.parse_file("spec.yaml"),
:ok <- validate_operation_ids(spec),
:ok <- validate_descriptions(spec) do
{:ok, spec}
end
end
defp validate_operation_ids(spec) do
operations = extract_all_operations(spec.document.paths)
operation_ids = Enum.map(operations, & &1.operation_id)
if length(operation_ids) == length(Enum.uniq(operation_ids)) do
:ok
else
{:error, "Duplicate operation IDs found"}
end
end
defp validate_descriptions(spec) do
# Ensure all operations have descriptions
# ... custom validation logic
:ok
end
end
```
### Generating Documentation
```elixir
defmodule DocGenerator do
def generate_markdown(spec_path) do
{:ok, spec} = OpenapiParser.parse_file(spec_path)
"""
# #{spec.document.info.title}
Version: #{spec.document.info.version}
#{spec.document.info.description}
## Endpoints
#{generate_paths_doc(spec.document.paths)}
"""
end
defp generate_paths_doc(paths) do
paths
|> Enum.map(fn {path, path_item} ->
generate_path_doc(path, path_item)
end)
|> Enum.join("\n\n")
end
defp generate_path_doc(path, path_item) do
"""
### #{path}
#{generate_operations_doc(path_item)}
"""
end
defp generate_operations_doc(path_item) do
[:get, :post, :put, :delete]
|> Enum.filter(&Map.get(path_item, &1))
|> Enum.map(fn method ->
operation = Map.get(path_item, method)
"- **#{String.upcase(to_string(method))}**: #{operation.summary || operation.description}"
end)
|> Enum.join("\n")
end
end
```
## Performance Considerations
- **Parsing** is generally fast, even for large specifications (10,000+ lines)
- **Validation** adds ~20-30% overhead
- **Reference resolution** can be expensive for deeply nested references
- For best performance: parse once, cache the result
```elixir
# Good: Parse once
{:ok, spec} = OpenapiParser.parse_file("spec.yaml")
# Use spec multiple times...
# Avoid: Parsing repeatedly
Enum.each(1..100, fn _ ->
{:ok, spec} = OpenapiParser.parse_file("spec.yaml") # Don't do this!
end)
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history and changes.
## Resources
- [OpenAPI Specification](https://spec.openapis.org/oas/latest.html)
- [Swagger 2.0 Specification](https://swagger.io/specification/v2/)
- [Hex Package](https://hex.pm/packages/openapi_parser)
- [Documentation](https://hexdocs.pm/openapi_parser)