From c34f8cbccd44a88a302999a19f70cfd3bff5cbef Mon Sep 17 00:00:00 2001 From: Joost van der Hoff Date: Fri, 3 May 2019 12:37:02 +0200 Subject: [PATCH 1/7] Add option to include request and response body schemas Include schemas from: paths///requestBody/content//schema paths///responses//content//schema Closes #25 --- openapi2jsonschema/command.py | 45 ++++++++++++++++++++++++++++++++++- openapi2jsonschema/util.py | 16 +++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/openapi2jsonschema/command.py b/openapi2jsonschema/command.py index 8261f2a..0e721cb 100644 --- a/openapi2jsonschema/command.py +++ b/openapi2jsonschema/command.py @@ -5,6 +5,7 @@ import urllib import os import sys +import re from jsonref import JsonRef # type: ignore import click @@ -16,6 +17,7 @@ allow_null_optional_fields, change_dict_values, append_no_duplicates, + get_components_from_body_definition, ) from openapi2jsonschema.errors import UnsupportedError @@ -48,8 +50,22 @@ is_flag=True, help="Prohibits properties not in the schema (additionalProperties: false)", ) +@click.option( + "--include-bodies", + is_flag=True, + help="Include request and response bodies as if they are components", +) @click.argument("schema", metavar="SCHEMA_URL") -def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): +def default( + output, + schema, + prefix, + stand_alone, + expanded, + kubernetes, + strict, + include_bodies, +): """ Converts a valid OpenAPI specification into a set of JSON Schema files """ @@ -120,6 +136,33 @@ def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): else: components = data["components"]["schemas"] + if include_bodies: + for path, path_definition in data["paths"].items(): + for http_method, http_method_definition in path_definition.items(): + name_prefix_fmt = "paths_{:s}_{:s}_{{:s}}_".format( + # Paths "/" and "/root" will conflict, + # no idea how to solve this elegantly. + path.lstrip("/").replace("/", "_") or "root", + http_method.upper(), + ) + name_prefix_fmt = re.sub( + r"\{([^:\}]+)\}", + r"_\1_", + name_prefix_fmt, + ) + if "requestBody" in http_method_definition: + components.update(get_components_from_body_definition( + http_method_definition["requestBody"], + prefix=name_prefix_fmt.format("request") + )) + responses = http_method_definition["responses"] + for response_code, response in responses.items(): + response_name_part = "response_{}".format(response_code) + components.update(get_components_from_body_definition( + response, + prefix=name_prefix_fmt.format(response_name_part), + )) + for title in components: kind = title.split(".")[-1].lower() if kubernetes: diff --git a/openapi2jsonschema/util.py b/openapi2jsonschema/util.py index 68fd8d4..9370bbd 100644 --- a/openapi2jsonschema/util.py +++ b/openapi2jsonschema/util.py @@ -111,3 +111,19 @@ def append_no_duplicates(obj, key, value): obj[key] = [] if value not in obj[key]: obj[key].append(value) + + +def get_components_from_body_definition(body_definition, prefix=""): + MIMETYPE_TO_TYPENAME_MAP = { + "application/json": "json", + "application/vnd.api+json": "jsonapi", + } + result = {} + for mimetype, definition in body_definition["content"].items(): + type_name = MIMETYPE_TO_TYPENAME_MAP.get( + mimetype, + mimetype.replace("/", "_"), + ) + if "schema" in definition: + result["{:s}{:s}".format(prefix, type_name)] = definition["schema"] + return result From 11d254b177eff1272fff8e9941b57184e4c8a094 Mon Sep 17 00:00:00 2001 From: Joost van der Hoff Date: Fri, 3 May 2019 12:46:03 +0200 Subject: [PATCH 2/7] Do not downcase filenames Closes #20 --- openapi2jsonschema/command.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openapi2jsonschema/command.py b/openapi2jsonschema/command.py index 0e721cb..a022738 100644 --- a/openapi2jsonschema/command.py +++ b/openapi2jsonschema/command.py @@ -164,7 +164,7 @@ def default( )) for title in components: - kind = title.split(".")[-1].lower() + kind = title.split(".")[-1] if kubernetes: group = title.split(".")[-3].lower() api_version = title.split(".")[-2].lower() @@ -198,8 +198,7 @@ def default( if ( kubernetes and stand_alone - and kind - in [ + and kind.lower() in [ "jsonschemaprops", "jsonschemapropsorarray", "customresourcevalidation", From 483eda6f2712639c2bc0338eefb942d9f1d57683 Mon Sep 17 00:00:00 2001 From: Joost van der Hoff Date: Fri, 3 May 2019 12:53:58 +0200 Subject: [PATCH 3/7] Move new functionality to separate function --- openapi2jsonschema/command.py | 31 ++++--------------------------- openapi2jsonschema/util.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/openapi2jsonschema/command.py b/openapi2jsonschema/command.py index a022738..305235b 100644 --- a/openapi2jsonschema/command.py +++ b/openapi2jsonschema/command.py @@ -5,7 +5,6 @@ import urllib import os import sys -import re from jsonref import JsonRef # type: ignore import click @@ -17,7 +16,7 @@ allow_null_optional_fields, change_dict_values, append_no_duplicates, - get_components_from_body_definition, + get_request_and_response_body_components_from_paths, ) from openapi2jsonschema.errors import UnsupportedError @@ -137,31 +136,9 @@ def default( components = data["components"]["schemas"] if include_bodies: - for path, path_definition in data["paths"].items(): - for http_method, http_method_definition in path_definition.items(): - name_prefix_fmt = "paths_{:s}_{:s}_{{:s}}_".format( - # Paths "/" and "/root" will conflict, - # no idea how to solve this elegantly. - path.lstrip("/").replace("/", "_") or "root", - http_method.upper(), - ) - name_prefix_fmt = re.sub( - r"\{([^:\}]+)\}", - r"_\1_", - name_prefix_fmt, - ) - if "requestBody" in http_method_definition: - components.update(get_components_from_body_definition( - http_method_definition["requestBody"], - prefix=name_prefix_fmt.format("request") - )) - responses = http_method_definition["responses"] - for response_code, response in responses.items(): - response_name_part = "response_{}".format(response_code) - components.update(get_components_from_body_definition( - response, - prefix=name_prefix_fmt.format(response_name_part), - )) + components.update( + get_request_and_response_body_components_from_paths(data["paths"]), + ) for title in components: kind = title.split(".")[-1] diff --git a/openapi2jsonschema/util.py b/openapi2jsonschema/util.py index 9370bbd..8424e51 100644 --- a/openapi2jsonschema/util.py +++ b/openapi2jsonschema/util.py @@ -1,5 +1,7 @@ #!/usr/bin/env python +import re + def iteritems(d): if hasattr(dict, "iteritems"): @@ -127,3 +129,33 @@ def get_components_from_body_definition(body_definition, prefix=""): if "schema" in definition: result["{:s}{:s}".format(prefix, type_name)] = definition["schema"] return result + + +def get_request_and_response_body_components_from_paths(paths): + components = {} + for path, path_definition in paths.items(): + for http_method, http_method_definition in path_definition.items(): + name_prefix_fmt = "paths_{:s}_{:s}_{{:s}}_".format( + # Paths "/" and "/root" will conflict, + # no idea how to solve this elegantly. + path.lstrip("/").replace("/", "_") or "root", + http_method.upper(), + ) + name_prefix_fmt = re.sub( + r"\{([^:\}]+)\}", + r"_\1_", + name_prefix_fmt, + ) + if "requestBody" in http_method_definition: + components.update(get_components_from_body_definition( + http_method_definition["requestBody"], + prefix=name_prefix_fmt.format("request") + )) + responses = http_method_definition["responses"] + for response_code, response in responses.items(): + response_name_part = "response_{}".format(response_code) + components.update(get_components_from_body_definition( + response, + prefix=name_prefix_fmt.format(response_name_part), + )) + return components From 72d89f140a2bef8b70df0febe90380ed041c7fa9 Mon Sep 17 00:00:00 2001 From: Zeust the Unoobian Date: Thu, 15 Oct 2020 10:19:35 +0200 Subject: [PATCH 4/7] Make "content" key in body definition optional This was committed as a GitHub review suggestion. Commit message suggested by Github: Update openapi2jsonschema/util.py Co-authored-by: Adam Kitain --- openapi2jsonschema/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi2jsonschema/util.py b/openapi2jsonschema/util.py index 8424e51..2e881c7 100644 --- a/openapi2jsonschema/util.py +++ b/openapi2jsonschema/util.py @@ -121,7 +121,7 @@ def get_components_from_body_definition(body_definition, prefix=""): "application/vnd.api+json": "jsonapi", } result = {} - for mimetype, definition in body_definition["content"].items(): + for mimetype, definition in body_definition.get("content", {}).items(): type_name = MIMETYPE_TO_TYPENAME_MAP.get( mimetype, mimetype.replace("/", "_"), From a5a801c0fd1079439258d90a1e8e7841bcc0cb7e Mon Sep 17 00:00:00 2001 From: dima Date: Mon, 30 Aug 2021 11:19:54 +0300 Subject: [PATCH 5/7] Fix --stand-alone errors when resolving references to generated files --- openapi2jsonschema/command.py | 17 ++++++++++++++--- openapi2jsonschema/util.py | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/openapi2jsonschema/command.py b/openapi2jsonschema/command.py index 9cd1bfc..ac29c66 100644 --- a/openapi2jsonschema/command.py +++ b/openapi2jsonschema/command.py @@ -126,6 +126,7 @@ def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): else: components = data["components"]["schemas"] + generated_files = [] for title in components: kind = title.split(".")[-1].lower() if kubernetes: @@ -183,9 +184,9 @@ def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): specification = updated if stand_alone: - base = "file://%s/%s/" % (os.getcwd(), output) - specification = JsonRef.replace_refs( - specification, base_uri=base) + # Put generated file on list for dereferencig $ref elements + # after all files will be generated + generated_files.append(full_name) if "additionalProperties" in specification: if specification["additionalProperties"]: @@ -209,6 +210,16 @@ def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): except Exception as e: error("An error occured processing %s: %s" % (kind, e)) + if stand_alone: + base = "file://%s/%s/" % (os.getcwd(), output) + for file_name in generated_files: + full_path = "%s/%s.json" % (output, file_name) + specification = json.load(open(full_path)) + specification = JsonRef.replace_refs( + specification, base_uri=base) + with open(full_path, "w") as schema_file: + schema_file.write(json.dumps(specification, indent=2)) + with open("%s/all.json" % output, "w") as all_file: info("Generating schema for all types") contents = {"oneOf": []} diff --git a/openapi2jsonschema/util.py b/openapi2jsonschema/util.py index 34270d2..470f1d1 100644 --- a/openapi2jsonschema/util.py +++ b/openapi2jsonschema/util.py @@ -90,7 +90,7 @@ def change_dict_values(d, prefix, version): if version < "3": new_v = "%s%s" % (prefix, v) else: - new_v = v.replace("#/components/schemas/", "") + ".json" + new_v = v.replace("#/components/schemas/", "").lower() + ".json" else: new_v = v new[k] = new_v From ec2b31f2f224d103aaf90001147300b48fc04baa Mon Sep 17 00:00:00 2001 From: dima Date: Mon, 30 Aug 2021 14:27:19 +0300 Subject: [PATCH 6/7] Add no all flag to not generate all.json file --- openapi2jsonschema/command.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/openapi2jsonschema/command.py b/openapi2jsonschema/command.py index ac29c66..4c05ba6 100644 --- a/openapi2jsonschema/command.py +++ b/openapi2jsonschema/command.py @@ -43,13 +43,16 @@ @click.option( "--kubernetes", is_flag=True, help="Enable Kubernetes specific processors" ) +@click.option( + "--no-all", is_flag=True, help="Do not generate all.json file" +) @click.option( "--strict", is_flag=True, help="Prohibits properties not in the schema (additionalProperties: false)", ) @click.argument("schema", metavar="SCHEMA_URL") -def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): +def default(output, schema, prefix, stand_alone, expanded, kubernetes, no_all, strict): """ Converts a valid OpenAPI specification into a set of JSON Schema files """ @@ -220,19 +223,20 @@ def default(output, schema, prefix, stand_alone, expanded, kubernetes, strict): with open(full_path, "w") as schema_file: schema_file.write(json.dumps(specification, indent=2)) - with open("%s/all.json" % output, "w") as all_file: - info("Generating schema for all types") - contents = {"oneOf": []} - for title in types: - if version < "3": - contents["oneOf"].append( - {"$ref": "%s#/definitions/%s" % (prefix, title)} - ) - else: - contents["oneOf"].append( - {"$ref": (title.replace("#/components/schemas/", "") + ".json")} - ) - all_file.write(json.dumps(contents, indent=2)) + if not no_all: + with open("%s/all.json" % output, "w") as all_file: + info("Generating schema for all types") + contents = {"oneOf": []} + for title in types: + if version < "3": + contents["oneOf"].append( + {"$ref": "%s#/definitions/%s" % (prefix, title)} + ) + else: + contents["oneOf"].append( + {"$ref": (title.replace("#/components/schemas/", "") + ".json")} + ) + all_file.write(json.dumps(contents, indent=2)) if __name__ == "__main__": From 801f51a070ab50210477f18e39ddc9a4103c6ad4 Mon Sep 17 00:00:00 2001 From: Vladimir Neverov Date: Tue, 19 Oct 2021 18:35:10 +0300 Subject: [PATCH 7/7] Add 'null' type to type list for fields marked as nullable in OpenAPI spec --- openapi2jsonschema/util.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openapi2jsonschema/util.py b/openapi2jsonschema/util.py index 34270d2..929a385 100644 --- a/openapi2jsonschema/util.py +++ b/openapi2jsonschema/util.py @@ -77,7 +77,10 @@ def allow_null_optional_fields(data, parent=None, grand_parent=None, key=None): def change_dict_values(d, prefix, version): new = {} try: + is_nullable = False for k, v in iteritems(d): + if k == 'nullable': + is_nullable = True new_v = v if isinstance(v, dict): new_v = change_dict_values(v, prefix, version) @@ -94,6 +97,10 @@ def change_dict_values(d, prefix, version): else: new_v = v new[k] = new_v + if is_nullable and 'type' in new: + if not isinstance(new['type'], list): + new['type'] = [new['type']] + new['type'].append('null') return new except AttributeError: return d