heitortsergent
heitortsergent

Reputation: 2141

How do I combine multiple OpenAPI 3 specification files together?

I want to combine an API specification written using the OpenAPI 3 spec, that is currently divided into multiple files that reference each other using $ref. How can I do that?

Upvotes: 46

Views: 77830

Answers (7)

Caranown
Caranown

Reputation: 193

I arrived here because I thought I had to combine openapi specs, because I had a lib project which I referenced in an app project, where the openapi spec is created. I noticed that if the relevant class was copied into my app project, it works, but when referenced from lib, it only made it into the spec as a generic object type.

To generate the spec, I'm using the smallrye-open-api-maven-plugin, but this might be relevant for other plugins as well. It turns out there is a setting <scanPackages> where I just had to include the lib project's package path, and that solved my issue.

This does not answer the original question, in the case where you have a legitimate need for multiple specs, but maybe this is helpful for others.

Upvotes: 0

Loic
Loic

Reputation: 1248

For what it worths redoc manages merge of different openapi files - even in different formats (JSON / YAML).

Upvotes: 5

Jason Pepas
Jason Pepas

Reputation: 26

I wrote a Python script which does this (follows $refs which are filesystem paths and in-lines the contents of those files, recursively):

#!/usr/bin/env python3

# Expand all file-based $ref's inline to produce a single YAML file.

import sys
import os
import yaml

# Is this a local file-based $ref?
# File-based refs look like paths, e.g. "../models/user.yaml".
# Non file-based refs look like URL fragments, e.g. "#/models/user"
def is_file_ref(ref):
    return (ref.startswith("/") or ref.startswith("./") or ref.startswith("../")) \
        and ref.endswith(".yaml")

# Merge two dictionaries.  Throws on key collision.
def merge_or_die(d1, d2):
    for k, v in d2.items():
        if k in d1:
            raise Exception("Refusing to clobber key '%s' with value '%s'" % (k, v))
        d1[k] = v
    return d1

# Recursively descend through the JSON, expanding any file-based refs inline.
def inline_json(js_in, pwd):
    if type(js_in) is dict:
        js_out = {}
        for k, v in js_in.items():
            if k == "$ref" and is_file_ref(v):
                if v.startswith("/"):
                    yml_fpath = v
                else:
                    yml_fpath = os.path.join(pwd, v)
                yml_in = open(yml_fpath, "r").read()
                v = yaml.safe_load(yml_in)
                pwd = os.path.split(yml_fpath)[0]
                js_out = merge_or_die(js_out, inline_json(v, pwd))
            else:
                js_out[k] = inline_json(v, pwd)
        return js_out
    else:
        return js_in

if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.stderr.write("Usage: %s <input_file>\n" % sys.argv[0])
        sys.exit(1)
    fpath_in = sys.argv[1]
    if fpath_in.startswith("/"):
        yml_fpath = fpath_in
    else:
        yml_fpath = os.path.join(os.getcwd(), fpath_in)
    yml_in = open(yml_fpath, "r").read()
    js_in = yaml.safe_load(yml_in)
    pwd = os.path.split(yml_fpath)[0]
    js_out = inline_json(js_in, pwd)
    yml_out = yaml.dump(js_out, sort_keys=False)
    print(yml_out)
./inline-yaml.py root.yaml > combined.yaml

Upvotes: 0

Timothy G.
Timothy G.

Reputation: 9175

I found that the Redocly CLI was a viable option for doing exactly what you need (addressing $ref's used within specifications).

You can configure the CLI using a configuration file (that must be located in the root of your project directory as well as be written in YAML format) which allows you to specify certain linting rules among other things. But to bundle your specifications, simply perform a bundle in the directory of where the configuration file is like so:

redocly bundle -o <NameOfFileOrDir>

After running this command, all of your $ref's will be replaced with the actual code of whatever the $ref was for, combining them into one definition.

If you have multiple elements defined in your apis object in your configuration file (i.e. multiple API definitions), it will create a directory named whatever you specified for the -o option and the folder will contain all of your definitions.

Upvotes: 5

Helen
Helen

Reputation: 97991

Most OpenAPI tools can work with multi-file OpenAPI definitions and resolve $refs dynamically.

If you specifically need to get a single resolved file, Swagger Codegen can do this. Codegen has a CLI version (used in the examples below), a Maven plugin (usage example) and a Docker image.

The input file (-i argument of the CLI) can be a local file or a URL.

Note: Line breaks are added for readability.

OpenAPI 3.0 example

Use Codegen 3.x to resolve OpenAPI 3.0 files:

java -jar swagger-codegen-cli-3.0.35.jar generate
     -l openapi-yaml
     -i ./path/to/openapi.yaml
     -o ./OUT_DIR
     -DoutputFile=output.yaml

-l openapi-yaml outputs YAML, -l openapi outputs JSON.

-DoutputFile is optional, the default file name is openapi.yaml / openapi.json.

OpenAPI 2.0 example

Use Codegen 2.x to resolve OpenAPI 2.0 files (swagger: '2.0'):

java -jar swagger-codegen-cli-2.4.28.jar generate
     -l swagger-yaml
     -i ./path/to/openapi.yaml
     -o ./OUT_DIR
     -DoutputFile=output.yaml

-l swagger-yaml outputs YAML, -l swagger outputs JSON.

-DoutputFile is optional, the default file name is swagger.yaml / swagger.json.

Upvotes: 27

Robert Massaioli
Robert Massaioli

Reputation: 13497

I wrote a quick tool to do this recently. I call it openapi-merge. There is a library and an associated CLI tool:

In order to use the CLI tool you just write a configuration file and then run npx openapi-merge-cli. The configuration file is fairly simple and would look something like this:

{
  "inputs": [
    {
      "inputFile": "./gateway.swagger.json"
    },
    {
      "inputFile": "./jira.swagger.json",
      "pathModification": {
        "stripStart": "/rest",
        "prepend": "/jira"
      }
    },
    {
      "inputFile": "./confluence.swagger.json",
      "disputePrefix": "Confluence",
      "pathModification": {
        "prepend": "/confluence"
      }
    }
  ], 
  "output": "./output.swagger.json"
}

For more details, see the README on the NPM package.

Upvotes: 35

heitortsergent
heitortsergent

Reputation: 2141

One way to do this is to use the open-source project speccy.

Open the terminal and install speccy by running (requires Node.js):

npm install speccy -g

Then run:

speccy resolve path/to/spec.yaml -o spec-output.yaml

Upvotes: 24

Related Questions