Keeping OpenAPI DRY and Portable

Phil Sturgeon
by Phil Sturgeon on March 11, 2020 8 min read

If you mention OpenAPI (or use its old name “Swagger”) to an API developer, there’s a chance they groan or make some other funny noise. When you ask why, you’ll often hear a terrible tale of 10,000 line YAML files, repeated content, and never-ending git conflicts. Problems like these have been a thing of the past for a while, thanks to editors like Stoplight Studio making it easier to work with referenced and split files into manageable chunks. However, to transport these separated OpenAPI documents, you’ll need to know two terms: bundling and dereferencing!

We’ve written before about using reference objects to avoid repeating yourself, but to briefly catch everyone up, we’ll need to repeat ourselves juuuust a bit.

Background Reference

In OpenAPI v2.0, there was a definitions keyword, which would go in the root of the file. Multiple examples, parameters, or schemas would be plonked in there altogether, and then operations, path items, and other schemas could reference them.

swagger: 2.0
info:
  # ...
paths:
  # ...
definitions:
  Category:
    type: object
    properties:
      id:
        type: integer
        format: int64
      name:
        type: string
    Tag:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string

Definitions could then be referenced with the $ref keyword, and it would be just like they had been defined in place.

'200':
  schema:
    $ref: '#/definitions/Category

In OpenAPI v3.0, even more things could have a reference:

paths:
  /customers/{CustomerId}:
    put:
      requestBody:
        $ref: '#/components/requestBodies/CreateCustomer'
      parameters:
        - $ref: '#/components/parameters/CustomerId'
      responses:
        '204':
          description: Updated

This reuse helped avoid repetition, but OpenAPI documents could still get pretty big. Having every endpoint, every schema, every parameter, every example… woof.

Splitting into Multiple Files

To avoid mega YAML, many folks recommend splitting API descriptions into multiple files. This has other benefits like letting developers use those same schema files for contract testing – using the same source of truth as the descriptions that are eventually rendered as beautiful documentation.

Person:
  $ref: '../schemas/person.yaml'

The downside to splitting OpenAPI documents up is that many tools don’t understand how to read these files. Some tools ask you to upload a single openapi.yaml document, and when you upload that single document to the web server, their code has no idea what a ../schemas/person.yaml is.

Some tooling simply refuses to follow a $ref to an “external file” because it could be a security issue. Others support an external file if it’s on the file system but fail to follow a URL for a variety of reasons… Stoplight tooling supports external files and external URLs, but your mileage may vary in the greater world of OpenAPI tooling.

Solutions to this are many; some tools allow you to upload a ZIP file, at which point they sniff all the file contents to figure out which is the “main file”, then support references to other documents. This functionality is pretty rare, a lot of the time tools just break.

Bundling Multiple Documents into One

If your tool-chain relies on a tool that cannot handle external $refs, one solution can be to “bundle” multiple files into one.

Bundling (also sometimes known as “External Inlining”) is the process of moving the contents of an external $ref into the components object of the main file in question.

For example, let’s take a look at some OpenAPI for the Giphy API. The entire giphy.yaml is a little large, even split across multiple files, so let’s look at one operation:

paths:
  /gifs:
    get:
      description: | A multiget version of the get GIF by ID endpoint.
      operationId: getGifsById
      parameters:
        - $ref: '#/parameters/gifIds'
      responses:
        '200':
          description: 'ok'
          schema:
            properties:
              data:
                items:
                  $ref: './schemas/gif.v1.yaml'
                type: array
              meta:
                $ref: './schemas/meta.v1.yaml'
              pagination:
                $ref: './schemas/pagination.v1.yaml'
            type: object
        '400':
          $ref: '#/responses/BadRequest'

They’ve still got some common responses saved in the main file, but multiple schemas are split into different files in the schemas directory. Stoplight Studio users can right-click on any OpenAPI document, “Export > Bundled” then choose from “Copy to Clipboard” or “Save to File”.

Exporting Bundled from Stoplight Studio

The results of this bundled file look a bit like this:

paths:
  /gifs:
    get:
      description: | A multiget version of the get GIF by ID endpoint.
      operationId: getGifsById
      parameters:
        - $ref: '#/parameters/gifIds'
      responses:
        '200':
          description: ''
          schema:
            properties:
              data:
                items:
                  $ref: '#/paths/~1gifs~1random/get/responses/200/schema/properties/data'
                type: array
              meta:
                type: object
                description: | The Meta Object contains basic information regarding the request, whether it was successful, and the response given by the API. Check `responses` to see a description of types of response codes the API might give you under different circumstances.
                properties:
                  msg:
                    description: HTTP Response Message
                    example: OK
                    type: string
                  response_id:
                    description: A unique ID paired with this response from the API.
                    example: 57eea03c72381f86e05c35d2
                    type: string
                  status:
                    description: HTTP Response Code
                    example: 200
                    format: int32
                    type: integer
              pagination:
                type: object
                description: | The Pagination Object contains information relating to the number of total results available as well as the number of results fetched and their relative positions.
                properties:
                  count:
                    description: Total number of items returned.
                    example: 25
                    format: int32
                    type: integer
                  offset:
                    description: Position in pagination.
                    example: 75
                    format: int32
                    type: integer
                  total_count:
                    description: Total number of items available.
                    example: 250
                    format: int32
                    type: integer
            type: object
        '400':
          $ref: '#/responses/BadRequest'

The output here is a little funny looking at first, but what it’s doing is rather smart. It loops through all the references, and the first time it finds one, it’ll replace it with the contents of the $ref target, then the next time it finds one, it’ll just change the $ref to target that first definition. In this example, it put meta and pagination directly into this operation, but the data has a $ref to elsewhere.

data:
  type: array
  items:
    $ref: '#/paths/~1gifs~1random/get/responses/200/schema/properties/data'

Bundling Outside of Studio

We added this functionality to Studio using a NodeJS module called APIDevTools/json-schema-ref-parser. This battle-hardened module handles all $ref logic in Studio, Docs, Prism, and everything else in the Stoplight ecosystem.

Until recently, we maintained a similar NodeJS module stoplightio/json-ref-parser, but we’ve combined efforts with this one so we can all solve more significant, more interesting problems, quicker.

Another benefit is that it has a CLI wrapper swagger-cli available. If you’re using another OpenAPI editor, are muddling through with Vim, or need to bundle as part of an automated workflow via the CLI, you can use swagger-cli on the command line to get the same result as if you clicked the button in Studio.

No $ref Support At All?

Bundling helps when tools only support some types of references, but what if the tool cannot handle the $ref keyword at all? If you come across one of these tools, the best thing to do is to run away and find a replacement. If there is no alternative tool, try supporting that open-source project with a pull request (via json-schema-ref-parser or an equivalent JSON Schema tool in the appropriate programming language).

Sometimes you are truly stuck with this tool, and for this scenario, there is dereferencing. When using this strategy, $refs are replaced with their values, as though somebody went through doing copy & paste on each one. No more $ref keywords exist at all. If you have 10 operations referencing the same model 10 times, you now have 10 copies of that model embedded in different parts of the document.

This strategy is something you only want to use as a last resort.

In Studio, the process is the same:

Exporting Dereferenced from Stoplight Studio

Looking at the contents of this file, it looks quite different, but… it’s giant, and I’m not even going to paste a chunk in here. The bundled file was 676 lines, but the dereferenced file is 10,123 lines long! Take a look at giphy.deref.yaml.

Summary

Ideally, all tooling would be compliant with the OpenAPI and JSON Schema specifications, implementing 100% of the expected functionality in the intended fashion. Unfortunately, the real world is never so simple as the ideal. If you need to use some tooling that cannot handle $ref properly, give one of these two approaches a try, either right from Stoplight Studio, swagger-cli, or a low-level JSON Schema $ref aware utility. You’ll keep your OpenAPI documents nicely organized in source control, but share simple, portable documents.

get-started-with-stoplight

 

Share this post

Stoplight to Join SmartBear!

As a part of SmartBear, we are excited to offer a world-class API solution for all developers' needs.

Learn More
The blog CTA goes here! If you don't need a CTA, make sure you turn the "Show CTA Module" option off.

Take a listen to The API Intersection.

Hear from industry experts about how to use APIs and save time, save money, and grow your business.

Listen Now