Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 124 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,81 +1,157 @@
# OpenAPI Documentation Generator
# OpenAPI Markdown Generator

This is a Python script that generates API documentation from an OpenAPI specification file.
[![PyPI version](https://badge.fury.io/py/openapi-markdown.svg)](https://pypi.org/project/openapi-markdown/)
[![Python versions](https://img.shields.io/pypi/pyversions/openapi-markdown.svg)](https://pypi.org/project/openapi-markdown/)
[![License](https://img.shields.io/github/license/vrerv/openapi-markdown)](https://github.com/vrerv/openapi-markdown/blob/main/LICENSE)
[![Build Status](https://github.com/vrerv/openapi-markdown/actions/workflows/ci.yml/badge.svg)](https://github.com/vrerv/openapi-markdown/actions)

## Usage
A minimal tool that converts OpenAPI 3.x (JSON or YAML) into compact Markdown for documentation.

`pip install openapi-markdown`
## Features

### CLI
- Minimal, compact Markdown output optimized for small files.
- Supports OpenAPI in JSON and YAML formats.
- Jinja2 templates for full customization.
- Optional path filtering to generate docs for selected API subsets.
- CLI and library usage.
- UTF-8 reading/writing.

You can use `openapi2markdown` command as follows:
## Installation

```
% openapi2markdown --help
Usage: openapi2markdown [OPTIONS] INPUT_FILE [OUTPUT_FILE]
Install from PyPI:

Convert OpenAPI spec to Markdown documentation.
```bash
pip install openapi-markdown
```

INPUT_FILE: Path to OpenAPI specification file (JSON or YAML) OUTPUT_FILE:
Path where markdown file will be generated (optional, defaults to INPUT_FILE
with .md extension)
For development:

Options:
-t, --templates-dir DIRECTORY Custom templates directory path
-f, --filter-paths TEXT Only generate apis that start with the given
path, multiple paths are allowed
```bash
git clone https://github.com/vrerv/openapi-markdown.git
cd openapi-markdown
pip install -r requirements.txt
pip install -e .
```

### Library
## Quickstart (CLI)

```python
from openapi_markdown.generator import to_markdown
Show help:

apiFile = "./tests/openapi.json"
outputFile = "api_doc.md"
templatesDir = "templates"
options = {
'filter_paths': ['/client']
}
```bash
openapi2markdown --help
```

to_markdown(apiFile, outputFile, templates_dir, options)
Generate Markdown:

```bash
openapi2markdown path/to/openapi.yaml api_doc.md
```

- If you want to use your own template, creates 'templates' directory and put [`api_doc_template.md.j2`](src/openapi_markdown/templates/api_doc_template.md.j2) file in it.
- You can change templates directory by passing it as the 3rd argument of `to_markdown`.
Filter by path prefixes:

### Default templates
```bash
openapi2markdown spec.yaml api_doc.md --filter-paths /api/v1 --filter-paths /auth
```

There are internal templates you can use. If not set default is used.
Use custom templates:

* templates - default
* templates/embed - prints objects hierarchially as list in every request/response
```bash
openapi2markdown spec.yaml api_doc.md --templates-dir ./my-templates
```

## Development
## Quickstart (Python)

### Requirements
```python
from openapi_markdown.generator import to_markdown

- Python 3.x
- json
- PyYAML
- openapi-core
- Jinja2
to_markdown(
api_file="./data/openapi.yaml",
output_file="api_doc.md",
templates_dir="templates", # optional
options={'filter_paths': ['/client']} # optional
)
```

`pip install -r requirement.txt`
## Templates

install a project in editble mode
`pip install -e ./`
Templates live in `src/openapi_markdown/templates`. Key files:

run tests
- `api_doc_template.md.j2` — main document template
- `_content.md.j2`, `_object_schema.md.j2`, `_example.md.j2`, `_security_scheme.md.j2` — partials

`python -m unittest`
To customize, copy the `templates` folder and modify Jinja2 templates. The generator will prefer a provided directory when passed via `--templates-dir` or `templates_dir` argument.

### Deploy
## Examples

Basic conversion:

```bash
openapi2markdown https://example.com/openapi.json api_doc.md
```
python3 -m pip install --upgrade twine

Only generate docs for `/users` endpoints:

```bash
openapi2markdown openapi.yaml users.md --filter-paths /users
```

## Development

Requirements:

- Python 3.8+
- Dependencies: Jinja2, PyYAML, openapi-core (see `requirements.txt`)

Run tests:

```bash
python -m unittest
```
./pypi.sh

Coding workflow:

```bash
git checkout -b feature/my-change
# make changes
git add .
git commit -m "Describe change"
git push origin feature/my-change
# open a pull request against upstream/main
```

If you don't have push access to upstream, fork first and push to your fork.

## Contributing

- Fork the repo, create feature branches, add tests, and open a pull request.
- Follow repository code style and include a clear PR description.
- Maintain compatibility with JSON and YAML OpenAPI specs.

## Release

Use the included `pypi.sh` helper or follow standard PyPI release steps with `twine`.

## License

MIT License - see `LICENSE` for details.

## Contact & Support

- Issues and feature requests: https://github.com/vrerv/openapi-markdown/issues
- For quick questions, open an issue and tag a maintainer.

## CLI Reference

openapi2markdown accepts the following flags:

- `-h, --help` Show help message and exit
- `-t, --templates-dir DIR`
Use templates from DIR instead of built-in templates
- `-f, --filter-paths PREFIX`
Only include paths that start with PREFIX. Can be repeated.
- `-o, --output FILE`
Output Markdown file (default: api_doc.md)

Example:
```bash
openapi2markdown spec.yaml docs.md --templates-dir ./my-templates --filter-paths /api/v1
16 changes: 7 additions & 9 deletions src/openapi_markdown/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,17 @@ def ref_to_schema(schema, spec_data):


def to_markdown(api_file, output_file, templates_dir='templates', options={}):
# Load the OpenAPI 3.0 specification file in either JSON or YAML format
with open(api_file) as f:
spec_data = json.load(f) if api_file.endswith(".json") else yaml.safe_load(f)
# Resolve all references in the spec data
# spec_data = ref_to_schema(spec_data, spec_data)
# filter spec_data.paths if filter_paths option is provided
with open(api_file, encoding='utf-8') as f:
if api_file.endswith(('.yaml', '.yml')):
spec_data = yaml.safe_load(f)
else:
spec_data = json.load(f)
if 'filter_paths' in options and options['filter_paths']:
spec_data['paths'] = {
k: v for k, v in spec_data['paths'].items()
if any(k.startswith(prefix) for prefix in options['filter_paths'])
}
spec = Spec.from_dict(spec_data)
# Load the Jinja2 template file
if os.path.exists(templates_dir):
env = Environment(loader=FileSystemLoader(templates_dir))
else:
Expand All @@ -79,5 +77,5 @@ def to_markdown(api_file, output_file, templates_dir='templates', options={}):
ref_to_param=lambda ref: ref_to_param(ref, spec_data),
ref_to_schema=lambda ref: ref_to_schema(ref, spec_data))
)
with open(output_file, "w") as f:
f.write(rendered_template)
with open(output_file, "w", encoding='utf-8') as f:
f.write(rendered_template)
44 changes: 31 additions & 13 deletions src/openapi_markdown/templates/_content.md.j2
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
{% if content -%}
{% if content and content is mapping %}
{% for media_type, media in content.items() -%}
### {{ media_type }}

{% for content_type, item in content.items() -%}

{% set schema = item.schema -%}

{{ schema | ref_to_link }}
{% if media.schema -%}
**Schema:**
```json
{{ media.schema | tojson(indent=2) }}
```
{% endif %}

{% if schema %}
{% include './_object_schema.md.j2' %}
{% if media.example -%}
**Example:**
```json
{{ media.example | tojson(indent=2) }}
```
{% endif %}

{% with root = item -%}
{% include './_example.md.j2' -%}
{% endwith -%}
{% endfor -%}
{% if media.examples -%}
**Examples:**
{% for ex_name, ex in media.examples.items() -%}
- **{{ ex_name }}:** {{ ex.summary or 'No summary' }}
```json
{{ ex.value | tojson(indent=2) }}
```
{% endfor %}
{% endif %}

{% endif -%}
{% if media.encoding -%}
**Encoding:**
{% for enc_name, enc in media.encoding.items() -%}
- **{{ enc_name }}:** {{ enc | tojson }}
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
17 changes: 8 additions & 9 deletions src/openapi_markdown/templates/_example.md.j2
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
{% if root.examples %}
Examples

{% for example_name, example in root.examples.items() %}
{% if example.summary -%}
**Summary:** {{ example.summary }}
{% endif %}

{% if example.description %}
{{ example.description }}
{% if example.description -%}
**Description:** {{ example.description }}
{% endif %}

**Value:**
```json
{{ example.value | to_json }}
{{ example.value | tojson(indent=2) }}
```
{% endfor %}
{% endif %}
{% endif %}
52 changes: 45 additions & 7 deletions src/openapi_markdown/templates/_object_schema.md.j2
Original file line number Diff line number Diff line change
@@ -1,7 +1,45 @@
{% if schema.properties -%}
| Field | Type | Description |
|-------|------|-------------|
{% for property_name, property in schema.properties.items() -%}
| {{ property_name }} | {{ property.type }} | {{ property.description if property.description else '' }} |
{% endfor -%}
{% endif -%}
{% if schema.type == 'object' -%}
**Properties:**

| Property | Type | Required | Description | Example |
|----------|------|----------|-------------|---------|
{% for prop_name, prop in schema.properties.items() -%}
| {{ prop_name }} | {{ prop.type or 'object' }} | {{ 'Yes' if prop_name in (schema.required or []) else 'No' }} | {{ prop.description or '' }} | {{ prop.example or '' }} |
{% endfor %}

{% if schema.additionalProperties -%}
**Additional Properties:** Allowed
{% endif %}
{% endif %}

{% if schema.enum -%}
**Enum Values:** {{ schema.enum | join(', ') }}
{% endif %}

{% if schema.oneOf -%}
**One Of:**
{% for sub_schema in schema.oneOf -%}
- {{ sub_schema | tojson }}
{% endfor %}
{% endif %}

{% if schema.allOf -%}
**All Of:**
{% for sub_schema in schema.allOf -%}
- {{ sub_schema | tojson }}
{% endfor %}
{% endif %}

{% if schema.anyOf -%}
**Any Of:**
{% for sub_schema in schema.anyOf -%}
- {{ sub_schema | tojson }}
{% endfor %}
{% endif %}

{% if schema.example -%}
**Example:**
```json
{{ schema.example | tojson(indent=2) }}
```
{% endif %}
Loading