jekyll-openapi
A (mostly) spec-compliant OpenAPI 3.1 toolkit for Liquid-based static site generators (Jekyll in particular).
It deliberately does not render HTML. The gem provides:
- a navigable object model over a parsed OpenAPI document — dereferencing, tag and operation enumeration, parameter merging;
- schema-to-text renderers (TypeScript-like and simplified XML) exposed as opt-in Liquid filters;
…and your site provides the templates. See examples/ for complete, copy-paste-able templates.
The gem works as a plain Ruby library too — without Jekyll or Liquid. That usage, the object model, and the renderer API are documented in examples/ruby/README.md.
Jekyll integration
Add the gem to your Gemfile and list it as a plugin — nothing else to do,
the filters register themselves and warnings are routed to Jekyll's logger:
# Gemfile
group :plugins do
gem "jekyll-openapi"
end
# _config.yml
plugins:
- jekyll-openapi
Walkthrough: a minimal API page
Four steps take you from a spec file to a rendered reference page.
1. Enable the plugin as shown above (Gemfile + plugins: entry). No
_plugins/ file is needed.
2. Drop your spec into _data/ so Jekyll parses it into site.data:
_data/openapi.yml # YAML or JSON; becomes site.data.openapi
3. Write a page that walks the tags and operations with the filters:
---
title: API Reference
---
{% assign api = site.data.openapi %}
<h1>{{ api.info.title }}</h1>
{% assign tags = api | oapi_tags %}
{% for tag in tags %}
<h2>{{ tag.name }}</h2>
{{ tag.description | markdownify }}
{% assign ops = api | oapi_operations: tag.name %}
{% for op in ops %}
<h3>{{ op.method | upcase }} {{ op.path }}</h3>
{{ op.operation.description | markdownify }}
{% endfor %}
{% endfor %}
4. Build (bundle exec jekyll serve) and you have a page listing every
operation, grouped by tag. That's a complete, working integration.
oapi_operations already does the tedious parts for you: it skips
non-operation path-item keys, handles x- extension methods, filters by tag,
and merges path-level parameters into each operation.
Each op in the loop is a plain Hash you can traverse further:
| key | content |
|---|---|
"path" |
"/pets/{petId}" |
"method" |
normalized method, x- prefix stripped ("mkcol") |
"raw_method" |
the literal key ("x-mkcol") |
"operation" |
the Operation object, dereferenced |
"parameters" |
path-level + operation-level parameters, merged (operation wins on same name/location) |
"grouped_parameters" |
same, grouped: {"query" => [...], "path" => [...], "header" => [...], "cookie" => [...]} |
Going further
The walkthrough above is intentionally bare. For a production-quality page — tag index/navigation, parameter tables, request/response bodies with rendered schemas — start from the complete examples and restyle them:
- examples/jekyll/api-doc.html — a full API reference page: tag cloud, operations grouped by tag, path/query parameter tables, request and response schemas.
- examples/jekyll/_includes/oapi/schema.html — a reusable include that picks the XML or TypeScript renderer based on the content type.
Liquid filters
Registered automatically in Jekyll. (In a plain Liquid setup you register them yourself — see examples/ruby/README.md.)
| filter | input → output |
|---|---|
oapi_dereference |
document → document with local $refs made navigable (lazy, name-preserving). Optional — every filter below dereferences internally; call it only if you also hand-walk the raw hash. Idempotent. |
oapi_tags |
document → tag list |
oapi_operations |
document (+ optional tag argument) → operation list |
oapi_parameters |
parameter list → grouped by location |
oapi_merge_parameters |
path params, operation params → merged list |
oapi_examples |
parameter or media-type object → normalized {name, value, summary, description} list, unifying examples/example/schema-level examples |
oapi_headers |
response object → normalized {name, description, required, deprecated, schema} list ($ref'd headers resolved) |
oapi_schemas |
document → named component-schema map (for a Models section) |
oapi_security_schemes |
document → named security-scheme map |
oapi_servers |
document → server list |
oapi_schema |
JSON-Schema → TypeScript-like string |
oapi_xml_schema |
JSON-Schema → simplified XML string |
oapi_html_schema |
JSON-Schema → semantic HTML; named component refs become links. Works on dereferenced or raw {"$ref": …} schemas (link mode needs only the pointer). |
oapi_schema_anchor |
schema name → schema-<name> anchor id |
oapi_operation_anchor |
operation entry (or operationId) → fragment id; falls back to method+path when operationId is absent |
oapi_tag_anchor |
tag object (or name) → fragment id |
oapi_representation |
media-type string → "xml"/"json"/"text"/"binary" (robust to text/xml, *+xml, ; charset=…) |
oapi_is_xml |
media-type string → boolean (XML-family) |
The text schema filters turn a JSON-Schema into readable text, e.g.
{{ content.schema | oapi_schema }} produces:
{
name: "string"
age?: integer
}
oapi_html_schema instead emits semantic, class-annotated HTML (<dl>/<ul>,
never tables) in which a $ref to a named component schema renders as a link to
#schema-<name> rather than being inlined — so operation bodies stay compact,
reused types are cross-linked, and recursive schemas terminate. Descriptions are
rendered as markdown when run inside Jekyll (it reuses the site's configured
converter) and as escaped plain text otherwise. Pair it with
oapi_schemas + oapi_schema_anchor to emit a matching "Models" section; see
examples/jekyll/_includes/oapi/models.html
and schema.html (which picks a
renderer by content type), styled by
_sass/oapi-schema.scss.
Error handling
The library never raises on malformed input: it warns through
JekyllOpenAPI.logger (in Jekyll, routed to Jekyll's own logger) and degrades
gracefully — unresolvable and external $refs are left in place and warn only
when followed; recursive schemas terminate (rendered as a link, or their name in
the text renderers); invalid schemas render as "". Your build never breaks on
a bad spec.
Compliance notes
Intentional simplifications, in the spirit of "readable docs over exhaustive fidelity":
- Only local (
#/...) references are resolved; external files/URLs are left as-is. - The TypeScript renderer supports
oneOf/anyOf(|),allOf(&), 3.1 type arrays (type: [string, "null"]), enums,additionalProperties, and infersobject/arraywhentypeis omitted — but does not mergeallOfmembers into a single object literal. - The XML renderer honors
xml.name,xml.prefix,xml.namespace,xml.attributeandxml.wrapped, and renders example values where provided.
Development
rake test # or: ruby -Ilib -Itest test/test_<name>.rb
Tests run against generic fixtures (test/fixtures/petstore.yml); when the gem
sources live inside the eCorpus_doc repository, an extra integration suite runs
the whole pipeline over the real eCorpus API definition.