jekyll-openapi
A (mostly) spec-compliant OpenAPI 3.1 toolkit for Liquid-based static site generators (Jekyll in particular), with partial OpenAPI 3.2 support (see OpenAPI 3.2 support).
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-openapiWalkthrough: 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 and OpenAPI 3.2
additionalOperations, 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, lowercase, x- prefix stripped ("mkcol") |
"raw_method" |
the literal key ("x-mkcol", "MKCOL") |
"operation" |
the Operation object, dereferenced |
"parameters" |
path-level + operation-level parameters, merged (operation wins on same name/location) |
"grouped_parameters" |
same, grouped: {"query" => [...], "querystring" => [...], "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.
OpenAPI 3.2 support
Partial. Because the model hands templates plain hashes, most new 3.2 fields flow through untouched and are usable today; "supported" below means the library actively understands the feature, "passthrough" means the raw data reaches your template but no helper interprets it.
Supported:
-
additionalOperations— enumerated byoapi_operationslike any other operation, with path-level parameters merged;methodis the lowercased method name,raw_methodthe literal key ("PROPFIND"). Gives webDAV-style APIs first-class treatment (previously only reachable viax-methods). - The
queryHTTP method — recognized as a first-class path-item key. - Tag
summary— passes throughoapi_tags(tested); use it as a short display name wheredescriptionis prose. -
in: querystringparameters — grouped under their own"querystring"key byoapi_parameters/grouped_parameters(the parameter iscontent-based: render its media-type schema, not a name/value pair). An operation-level querystring parameter overrides a path-level one regardless of name, and mixing it within: queryparameters warns (the spec forbids it) but keeps both.
Passthrough (data reaches templates, no helper logic):
- Tag
parent/kind— the fields are readable, butoapi_operationsandoapi_tagstreat tags as a flat list: no hierarchy building, no filtering by kind. - Response
summary, Servername— readable on the raw objects. - Security scheme additions (
deviceAuthorizationflow,oauth2MetadataUrl,deprecated) — readable viaoapi_security_schemes.
Not supported (TODO):
- Streaming / sequential media types —
itemSchema,itemEncodingandprefixEncodingare ignored by the schema renderers andoapi_examples. - Example Object
dataValue/serializedValue—oapi_examplesonly readsvalue. -
xml.nodeType— the XML renderer still keys off the 3.1attribute/wrappedflags. -
$selfand multi-document descriptions — consistent with the existing local-$refs-only policy. -
components.mediaTypesreuse — untested; generic local$refresolution may already handle it, but no guarantees.
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>.rbTests run against generic fixtures (test/fixtures/petstore.yml, a pristine
OpenAPI 3.1 document, and test/fixtures/filestore.yml, a WebDAV-flavored
OpenAPI 3.2 one); when the gem
sources live inside the eCorpus_doc repository, an extra integration suite runs
the whole pipeline over the real eCorpus API definition.