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-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 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:

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:

  • [x] additionalOperations — enumerated by oapi_operations like any other operation, with path-level parameters merged; method is the lowercased method name, raw_method the literal key ("PROPFIND"). Gives webDAV-style APIs first-class treatment (previously only reachable via x- methods).
  • [x] The query HTTP method — recognized as a first-class path-item key.
  • [x] Tag summary — passes through oapi_tags (tested); use it as a short display name where description is prose.
  • [x] in: querystring parameters — grouped under their own "querystring" key by oapi_parameters / grouped_parameters (the parameter is content-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 with in: query parameters warns (the spec forbids it) but keeps both.

Passthrough (data reaches templates, no helper logic):

  • [ ] Tag parent / kind — the fields are readable, but oapi_operations and oapi_tags treat tags as a flat list: no hierarchy building, no filtering by kind.
  • [ ] Response summary, Server name — readable on the raw objects.
  • [ ] Security scheme additions (deviceAuthorization flow, oauth2MetadataUrl, deprecated) — readable via oapi_security_schemes.

Not supported (TODO):

  • [ ] Streaming / sequential media types — itemSchema, itemEncoding and prefixEncoding are ignored by the schema renderers and oapi_examples.
  • [ ] Example Object dataValue / serializedValueoapi_examples only reads value.
  • [ ] xml.nodeType — the XML renderer still keys off the 3.1 attribute / wrapped flags.
  • [ ] $self and multi-document descriptions — consistent with the existing local-$refs-only policy.
  • [ ] components.mediaTypes reuse — untested; generic local $ref resolution 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 infers object/array when type is omitted — but does not merge allOf members into a single object literal.
  • The XML renderer honors xml.name, xml.prefix, xml.namespace, xml.attribute and xml.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, 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.