36

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?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
heitortsergent
  • 1,971
  • 2
  • 20
  • 23

6 Answers6

25

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.

Robert Massaioli
  • 13,379
  • 7
  • 57
  • 73
  • 4
    Finally someone who combines multiple specifications, not "merges" the `$ref`s! Thank you so much – Rémi B. Mar 02 '21 at 18:18
  • So far, this tool is pretty dope! `echo '{"inputs":[{"inputFile":"./lib.yaml"},{"inputFile":"./api.yaml"}],"output":"./v1.json"}' > ${PWD}/docs/merge.json` `docker run --rm -ti -v ${PWD}/docs:/docs -w /docs 3apaxicom/npx openapi-merge-cli --config ./merge.json` – chaseisabelle Jun 28 '22 at 03:38
  • @Robert Massaioli great tool. but is this not maintained anymore? I can see a lot of open PRs and there are no releases in 2 years. – Bee May 11 '23 at 07:44
  • this tool cannot handle duplicates - if you have two spec files with the same endpoint in it, it will error. There seems to be no settings for skipping duplicates – Kappacake Aug 08 '23 at 16:10
23

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.

Helen
  • 87,344
  • 17
  • 243
  • 314
  • 4
    I personally could not figure out an option to "combine" or "merge" multiple input definitions, using codegen; could you provide an example? The `-i` swtich accepts single input - event passing multiple values (`-i def1.json -i def2.json`) does not work – asceta May 04 '20 at 11:24
  • 2
    @asceta this Q&A is about the situation when you have one "main" API definition file that uses `$ref` to reference external definitions. If you mean you have two different API definitions (with different sets of endpoints, schemas, etc.) that you want to merge, it's a different use case, and you should [ask a new question](/questions/ask?tags=openapi). – Helen May 04 '20 at 12:03
  • I want to merge multiple swagger files to a single file. All these files have same endpoints, but they are split into multiple files based on their version or child operations. Can merge these multiple files to a single be done using Codegen ? – Nigel Thomas Jan 23 '23 at 09:45
  • 1
    @NigelThomas no, use [`redocly join`](https://redocly.com/docs/cli/commands/join/) or [`openapi-merge-cli`](https://www.npmjs.com/package/openapi-merge-cli) instead. – Helen Jan 23 '23 at 10:18
21

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

heitortsergent
  • 1,971
  • 2
  • 20
  • 23
1

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.

Timothy G.
  • 6,335
  • 7
  • 30
  • 46
  • There is also a helpful Visual Studio Code extension for Redocly: https://marketplace.visualstudio.com/items?itemName=Redocly.openapi-vs-code – Timothy G. Feb 02 '23 at 03:13
0

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
0

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

Loic
  • 1,088
  • 7
  • 19