From bfdb05923a9e58ffef467a5f2c261298ce3524d7 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Thu, 24 Sep 2026 18:29:24 -0300 Subject: [PATCH 1/2] Reject unsupported OpenAPI versions in the `lint` command Signed-off-by: Juan Cruz Viotti --- src/command_lint.cc | 14 ++++ src/error.h | 20 ++++++ src/utils.h | 20 ++++++ test/CMakeLists.txt | 4 ++ test/lint/fail_lint_openapi_version.clitest | 72 +++++++++++++++++++ ...ail_lint_openapi_version_malformed.clitest | 42 +++++++++++ .../fail_lint_openapi_version_newer.clitest | 42 +++++++++++ ..._lint_openapi_version_not_a_string.clitest | 44 ++++++++++++ 8 files changed, 258 insertions(+) create mode 100755 test/lint/fail_lint_openapi_version.clitest create mode 100755 test/lint/fail_lint_openapi_version_malformed.clitest create mode 100755 test/lint/fail_lint_openapi_version_newer.clitest create mode 100755 test/lint/fail_lint_openapi_version_not_a_string.clitest diff --git a/src/command_lint.cc b/src/command_lint.cc index 72450f85..2e97b3cd 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -496,6 +496,13 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) throw NotSchemaError{entry.resolution_base}; } + const auto *unsupported_revision{ + unsupported_openapi_version(entry.second)}; + if (unsupported_revision != nullptr) { + throw sourcemeta::core::FileError( + entry.resolution_base, unsupported_revision->to_string()); + } + const auto is_openapi{ sourcemeta::core::openapi_version(entry.second).has_value()}; if (is_openapi && format_output) { @@ -738,6 +745,13 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) throw NotSchemaError{entry.resolution_base}; } + const auto *unsupported_revision{ + unsupported_openapi_version(entry.second)}; + if (unsupported_revision != nullptr) { + throw sourcemeta::core::FileError( + entry.resolution_base, unsupported_revision->to_string()); + } + LOG_VERBOSE(options) << "Linting: " << entry.first << "\n"; const auto is_openapi{ diff --git a/src/error.h b/src/error.h index 2c87ec33..828658b2 100644 --- a/src/error.h +++ b/src/error.h @@ -178,6 +178,21 @@ class UnsupportedOpenAPIFormatError : public std::runtime_error { "The --format option is not supported for OpenAPI descriptions"} {} }; +class UnsupportedOpenAPIVersionError : public std::runtime_error { +public: + UnsupportedOpenAPIVersionError(std::string value) + : std::runtime_error{"This OpenAPI Specification revision is not " + "supported"}, + value_{std::move(value)} {} + + [[nodiscard]] auto value() const -> const std::string & { + return this->value_; + } + +private: + std::string value_; +}; + class OptionConflictError : public std::runtime_error { public: OptionConflictError(const std::string &message) @@ -1168,6 +1183,11 @@ inline auto try_catch(const sourcemeta::core::Options &options, const auto is_json{options.contains("json")}; print_exception(is_json, error); return EXIT_NOT_SUPPORTED; + } catch (const sourcemeta::core::FileError + &error) { + const auto is_json{options.contains("json")}; + print_exception(is_json, error); + return EXIT_NOT_SUPPORTED; } catch (const UnsupportedDialectCodegenError &error) { const auto is_json{options.contains("json")}; print_exception(is_json, error); diff --git a/src/utils.h b/src/utils.h index 5255ae7f..3eed8538 100644 --- a/src/utils.h +++ b/src/utils.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -116,6 +117,25 @@ inline auto looks_like_test_document(const sourcemeta::core::JSON &document) document.defines("tests") && document.at("tests").is_array(); } +// The revision an OpenAPI description declares, when it is one we cannot read. +// A document that declares the field as anything but a string is no OpenAPI +// description by any reading of the specification, so it goes on being read as +// a schema rather than being turned down here +inline auto unsupported_openapi_version(const sourcemeta::core::JSON &document) + -> const sourcemeta::core::JSON * { + if (!document.is_object()) { + return nullptr; + } + + const auto *version{document.try_at("openapi")}; + if (version == nullptr || !version->is_string()) { + return nullptr; + } + + return sourcemeta::core::openapi_version(document).has_value() ? nullptr + : version; +} + inline auto default_dialect( const sourcemeta::core::Options &options, const std::optional &configuration) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c7011ffb..27fed952 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -870,6 +870,10 @@ add_jsonschema_test(lint/pass_lint_openapi_fix) add_jsonschema_test(lint/pass_lint_openapi_fix_yaml) add_jsonschema_test(lint/pass_lint_openapi_fix_paths) add_jsonschema_test(lint/fail_lint_openapi_fix_format) +add_jsonschema_test(lint/fail_lint_openapi_version) +add_jsonschema_test(lint/fail_lint_openapi_version_newer) +add_jsonschema_test(lint/fail_lint_openapi_version_malformed) +add_jsonschema_test(lint/fail_lint_openapi_version_not_a_string) add_jsonschema_test(lint/fail_lint) add_jsonschema_test(lint/fail_lint_color_always) add_jsonschema_test(lint/fail_lint_color_never) diff --git a/test/lint/fail_lint_openapi_version.clitest b/test/lint/fail_lint_openapi_version.clitest new file mode 100755 index 00000000..d2c28dc1 --- /dev/null +++ b/test/lint/fail_lint_openapi_version.clitest @@ -0,0 +1,72 @@ +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Example", "version": "1.0.0" }, + "components": { + "schemas": { + "Pet": { + "type": "string", + "const": "dog" + } + } + } +} +EOF + +// Not supported +RUN lint openapi.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: This OpenAPI Specification revision is not supported +2> at value 3.0.3 +2> at file path [CWD]/openapi.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Not supported +RUN lint openapi.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "This OpenAPI Specification revision is not supported", +1> "value": "3.0.3", +1> "filePath": "[CWD]/openapi.json" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt + +// Not supported +RUN lint openapi.json --fix STDIN /dev/null IN . INTO result_2.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_2.txt + +WRITE expected_2.txt UNTIL EOF +2> error: This OpenAPI Specification revision is not supported +2> at value 3.0.3 +2> at file path [CWD]/openapi.json +EOF + +COMPARE result_2.txt AGAINST expected_2.txt + +WRITE expected_file_0.txt UNTIL EOF +{ + "openapi": "3.0.3", + "info": { "title": "Example", "version": "1.0.0" }, + "components": { + "schemas": { + "Pet": { + "type": "string", + "const": "dog" + } + } + } +} +EOF + +COMPARE openapi.json AGAINST expected_file_0.txt diff --git a/test/lint/fail_lint_openapi_version_malformed.clitest b/test/lint/fail_lint_openapi_version_malformed.clitest new file mode 100755 index 00000000..f90440e5 --- /dev/null +++ b/test/lint/fail_lint_openapi_version_malformed.clitest @@ -0,0 +1,42 @@ +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.1", + "info": { "title": "Example", "version": "1.0.0" }, + "components": { + "schemas": { + "Pet": { + "type": "string", + "const": "dog" + } + } + } +} +EOF + +// Not supported +RUN lint openapi.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: This OpenAPI Specification revision is not supported +2> at value 3.1 +2> at file path [CWD]/openapi.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Not supported +RUN lint openapi.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "This OpenAPI Specification revision is not supported", +1> "value": "3.1", +1> "filePath": "[CWD]/openapi.json" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/lint/fail_lint_openapi_version_newer.clitest b/test/lint/fail_lint_openapi_version_newer.clitest new file mode 100755 index 00000000..c181e51d --- /dev/null +++ b/test/lint/fail_lint_openapi_version_newer.clitest @@ -0,0 +1,42 @@ +WRITE openapi.json UNTIL EOF +{ + "openapi": "3.3.0", + "info": { "title": "Example", "version": "1.0.0" }, + "components": { + "schemas": { + "Pet": { + "type": "string", + "const": "dog" + } + } + } +} +EOF + +// Not supported +RUN lint openapi.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_0.txt + +WRITE expected_0.txt UNTIL EOF +2> error: This OpenAPI Specification revision is not supported +2> at value 3.3.0 +2> at file path [CWD]/openapi.json +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Not supported +RUN lint openapi.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 3 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "error": "This OpenAPI Specification revision is not supported", +1> "value": "3.3.0", +1> "filePath": "[CWD]/openapi.json" +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt diff --git a/test/lint/fail_lint_openapi_version_not_a_string.clitest b/test/lint/fail_lint_openapi_version_not_a_string.clitest new file mode 100755 index 00000000..9ca99879 --- /dev/null +++ b/test/lint/fail_lint_openapi_version_not_a_string.clitest @@ -0,0 +1,44 @@ +WRITE schema.json UNTIL EOF +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Test", + "description": "Test schema", + "examples": [ 1 ], + "openapi": 3 +} +EOF + +// Validation failure +RUN lint schema.json STDIN /dev/null IN . INTO result_0.txt EXPECTING 2 + +WRITE expected_0.txt UNTIL EOF +1> schema.json:6:3: +1> Future versions of JSON Schema will refuse to evaluate unknown keywords or custom keywords from optional vocabularies that don't have an x- prefix (unknown_keywords_prefix) +1> at location "/openapi" +EOF + +COMPARE result_0.txt AGAINST expected_0.txt + +// Validation failure +RUN lint schema.json --json STDIN /dev/null IN . INTO result_1.txt EXPECTING 2 + +REPLACE $CWD WITH '[CWD]' IN result_1.txt + +WRITE expected_1.txt UNTIL EOF +1> { +1> "valid": false, +1> "health": 0, +1> "errors": [ +1> { +1> "path": "[CWD]/schema.json", +1> "id": "unknown_keywords_prefix", +1> "message": "Future versions of JSON Schema will refuse to evaluate unknown keywords or custom keywords from optional vocabularies that don't have an x- prefix", +1> "description": null, +1> "schemaLocation": [ "openapi" ], +1> "position": [ 6, 3, 6, 14 ] +1> } +1> ] +1> } +EOF + +COMPARE result_1.txt AGAINST expected_1.txt From 29fe87390d8550ea3c86b53874a687bcba594f3f Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Thu, 24 Sep 2026 18:43:44 -0300 Subject: [PATCH 2/2] More Signed-off-by: Juan Cruz Viotti --- src/command_lint.cc | 28 ++++++++++++++++------------ src/error.h | 2 +- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/command_lint.cc b/src/command_lint.cc index 2e97b3cd..72117daa 100644 --- a/src/command_lint.cc +++ b/src/command_lint.cc @@ -196,6 +196,20 @@ retag_openapi_stdin(std::vector &entries) } } +// A description of a revision we cannot read is no JSON Schema either, so it is +// turned down rather than linted as one +static auto +reject_unsupported_openapi(const sourcemeta::jsonschema::InputJSON &entry) + -> void { + const auto *version{ + sourcemeta::jsonschema::unsupported_openapi_version(entry.second)}; + if (version != nullptr) { + throw sourcemeta::core::FileError< + sourcemeta::jsonschema::UnsupportedOpenAPIVersionError>( + entry.resolution_base, version->to_string()); + } +} + static auto check_openapi(const sourcemeta::blaze::SchemaTransformer &bundle, const sourcemeta::jsonschema::InputJSON &entry, @@ -496,12 +510,7 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) throw NotSchemaError{entry.resolution_base}; } - const auto *unsupported_revision{ - unsupported_openapi_version(entry.second)}; - if (unsupported_revision != nullptr) { - throw sourcemeta::core::FileError( - entry.resolution_base, unsupported_revision->to_string()); - } + reject_unsupported_openapi(entry); const auto is_openapi{ sourcemeta::core::openapi_version(entry.second).has_value()}; @@ -745,12 +754,7 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options) throw NotSchemaError{entry.resolution_base}; } - const auto *unsupported_revision{ - unsupported_openapi_version(entry.second)}; - if (unsupported_revision != nullptr) { - throw sourcemeta::core::FileError( - entry.resolution_base, unsupported_revision->to_string()); - } + reject_unsupported_openapi(entry); LOG_VERBOSE(options) << "Linting: " << entry.first << "\n"; diff --git a/src/error.h b/src/error.h index 828658b2..4c736fb3 100644 --- a/src/error.h +++ b/src/error.h @@ -185,7 +185,7 @@ class UnsupportedOpenAPIVersionError : public std::runtime_error { "supported"}, value_{std::move(value)} {} - [[nodiscard]] auto value() const -> const std::string & { + [[nodiscard]] auto value() const noexcept -> const std::string & { return this->value_; }