Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/command_lint.cc
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,20 @@ retag_openapi_stdin(std::vector<sourcemeta::jsonschema::InputJSON> &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,
Expand Down Expand Up @@ -496,6 +510,8 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options)
throw NotSchemaError{entry.resolution_base};
}

reject_unsupported_openapi(entry);

const auto is_openapi{
sourcemeta::core::openapi_version(entry.second).has_value()};
if (is_openapi && format_output) {
Expand Down Expand Up @@ -738,6 +754,8 @@ auto sourcemeta::jsonschema::lint(const sourcemeta::core::Options &options)
throw NotSchemaError{entry.resolution_base};
}

reject_unsupported_openapi(entry);

LOG_VERBOSE(options) << "Linting: " << entry.first << "\n";

const auto is_openapi{
Expand Down
20 changes: 20 additions & 0 deletions src/error.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 noexcept -> const std::string & {
return this->value_;
}

private:
std::string value_;
};

class OptionConflictError : public std::runtime_error {
public:
OptionConflictError(const std::string &message)
Expand Down Expand Up @@ -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<UnsupportedOpenAPIVersionError>
&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);
Expand Down
20 changes: 20 additions & 0 deletions src/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <sourcemeta/core/json.h>
#include <sourcemeta/core/jsonpointer.h>
#include <sourcemeta/core/jsonschema.h>
#include <sourcemeta/core/openapi.h>
#include <sourcemeta/core/options.h>
#include <sourcemeta/core/uri.h>
#include <sourcemeta/core/yaml.h>
Expand Down Expand Up @@ -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

@augmentcode augmentcode Bot Sep 24, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/utils.h:135 treats any object with a string-valued openapi member as an OpenAPI description even when it lacks required OpenAPI members such as info, despite JSON Schema permitting arbitrary/custom keyword names. Consequently, a JSON Schema containing "openapi": "3.0.3" now exits with an unsupported-version error instead of being linted (unlike the analogous non-string case covered by the new test), so this rejects inputs that are not OpenAPI descriptions.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

: version;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Only classify an unrecognized openapi string as an unsupported OpenAPI revision after confirming the document has OpenAPI's required structure; otherwise valid JSON Schemas using openapi as a custom keyword are rejected by lint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/utils.h, line 136:

<comment>Only classify an unrecognized `openapi` string as an unsupported OpenAPI revision after confirming the document has OpenAPI's required structure; otherwise valid JSON Schemas using `openapi` as a custom keyword are rejected by `lint`.</comment>

<file context>
@@ -116,6 +117,25 @@ inline auto looks_like_test_document(const sourcemeta::core::JSON &document)
+  }
+
+  return sourcemeta::core::openapi_version(document).has_value() ? nullptr
+                                                                 : version;
+}
+
</file context>

}

inline auto default_dialect(
const sourcemeta::core::Options &options,
const std::optional<sourcemeta::blaze::Configuration> &configuration)
Expand Down
4 changes: 4 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
72 changes: 72 additions & 0 deletions test/lint/fail_lint_openapi_version.clitest
Original file line number Diff line number Diff line change
@@ -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
42 changes: 42 additions & 0 deletions test/lint/fail_lint_openapi_version_malformed.clitest
Original file line number Diff line number Diff line change
@@ -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
42 changes: 42 additions & 0 deletions test/lint/fail_lint_openapi_version_newer.clitest
Original file line number Diff line number Diff line change
@@ -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
44 changes: 44 additions & 0 deletions test/lint/fail_lint_openapi_version_not_a_string.clitest
Original file line number Diff line number Diff line change
@@ -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 ]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The expected end column in "position" is off by one. "openapi": 3 on line 6 spans columns 3-15 ( "openapi": 3 — key at 3-12, colon at 13, space at 14, value 3 at 15), so the position should be [ 6, 3, 6, 15 ]. The existing fail_lint_positions_json.clitest confirms the same tracker reports the full key/value span for an object member ("unknown-1": 1 → [ 6, 5, 6, 18 ], where the value sits at column 18). As written with end column 14, the COMPARE result_1.txt AGAINST expected_1.txt will fail in CI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/lint/fail_lint_openapi_version_not_a_string.clitest, line 38:

<comment>The expected end column in `"position"` is off by one. `"openapi": 3` on line 6 spans columns 3-15 (`  "openapi": 3` — key at 3-12, colon at 13, space at 14, value `3` at 15), so the position should be `[ 6, 3, 6, 15 ]`. The existing `fail_lint_positions_json.clitest` confirms the same tracker reports the full key/value span for an object member (`"unknown-1": 1` → `[ 6, 5, 6, 18 ]`, where the value sits at column 18). As written with end column 14, the `COMPARE result_1.txt AGAINST expected_1.txt` will fail in CI.</comment>

<file context>
@@ -0,0 +1,44 @@
+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>   ]
</file context>
Suggested change
1> "position": [ 6, 3, 6, 14 ]
+1> "position": [ 6, 3, 6, 15 ]

1> }
1> ]
1> }
EOF

COMPARE result_1.txt AGAINST expected_1.txt
Loading