Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions src/alterschema/alterschema.cc
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ inline auto compile_non_standalone_subschema(
#include "linter/unsatisfiable_max_contains.h"
#include "linter/unsatisfiable_min_properties.h"
#include "linter/valid_default.h"
#include "linter/valid_example.h"
#include "linter/valid_examples.h"

#undef ONLY_CONTINUE_IF
Expand Down Expand Up @@ -497,6 +498,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void {
bundle.add<InvalidExternalRef>();
bundle.add<UnknownFormatPrefix>();
bundle.add<ValidDefault>();
bundle.add<ValidExample>();
bundle.add<ValidExamples>();
}

Expand Down
100 changes: 100 additions & 0 deletions src/alterschema/linter/valid_example.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
class ValidExample final : public SchemaTransformRule {
public:
using mutates = std::true_type;
using reframe_after_transform = std::true_type;
ValidExample(Compiler compiler = default_schema_compiler)
: SchemaTransformRule{"valid_example", "Only set an `example` value that "
"validates against the schema"},
compiler_{std::move(compiler)} {};

[[nodiscard]] auto
condition(const sourcemeta::core::JSON &schema,
const sourcemeta::core::JSON &root,
const sourcemeta::core::SchemaVocabularies &vocabularies,
const sourcemeta::core::SchemaFrame &frame,
const sourcemeta::core::SchemaFrame::Location &location,
const sourcemeta::core::SchemaWalker &walker,
const sourcemeta::core::SchemaResolver &resolver) const
-> SchemaTransformRule::Result override {
using Known = SchemaVocabularies::Known;
ONLY_CONTINUE_IF(
vocabularies.contains_any({Known::OPENAPI_3_0_BASE,
Known::OPENAPI_3_1_BASE,
Known::OPENAPI_3_2_BASE}) &&
schema.is_object() && schema.defines("example"));

if (vocabularies.contains(Known::OPENAPI_3_0_BASE)) {
ONLY_CONTINUE_IF(!schema.defines("$ref"));
}

const auto &instance{schema.at("example")};

@augmentcode augmentcode Bot Oct 29, 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.

Could you check serialized non-JSON examples at src/alterschema/linter/valid_example.h:30? The OpenAPI Schema Object specification explicitly permits strings containing examples not naturally representable in JSON/YAML, so an object schema with an XML-string example is now rejected for being a string and deleted by lint/fix despite being permitted.

Severity: medium

Fix This in Augment

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


if (frame.standalone()) {
const auto base{frame.uri(location.pointer)};
assert(base.has_value());
Template schema_template;
try {
schema_template = compile(root, walker, resolver, this->compiler_,
frame, base.value().get(), Mode::Exhaustive);
} catch (const CompilerReferenceTargetNotSchemaError &) {
throw;
} catch (const sourcemeta::core::SchemaVocabularyError &) {
throw;
} catch (...) {
return false;
}
SimpleOutput output{instance};
Evaluator evaluator;
const auto result{
evaluator.validate(schema_template, instance, std::ref(output))};
if (result) {
return false;
}

std::ostringstream message;
for (const auto &entry : output) {
message << entry.message << "\n";
message << " at instance location \"";
sourcemeta::core::stringify(entry.instance_location, message);
message << "\"\n";
message << " at evaluate path \"";
sourcemeta::core::stringify(entry.evaluate_path, message);
message << "\"\n";
}

return {{{"example"}}, std::move(message).str()};
}

sourcemeta::core::WeakPointer base;
const auto schema_template{compile_non_standalone_subschema(
root, frame, location, walker, resolver, this->compiler_, base)};
ONLY_CONTINUE_IF(schema_template.has_value());
SimpleOutput output{instance, base};
Evaluator evaluator;
const auto result{evaluator.validate(schema_template.value(), instance,
std::ref(output))};
Comment on lines +73 to +74

@augmentcode augmentcode Bot Oct 29, 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.

Could you check an OpenAPI 3.0 request-body schema with required: [id], a readOnly: true id property, and example: {} at src/alterschema/linter/valid_example.h:74–75? OpenAPI makes id required only in responses, but the default compiler ignores readOnly/writeOnly while enforcing required, so lint/fix deletes this valid request example (likewise for response examples omitting required writeOnly properties).

Other locations where this applies: src/alterschema/linter/valid_example.h:48–49

Severity: medium

Other Locations
  • src/alterschema/linter/valid_example.h:48

Fix This in Augment

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

if (result) {
return false;
}

std::ostringstream message;
for (const auto &entry : output) {
message << entry.message << "\n";
message << " at instance location \"";
sourcemeta::core::stringify(entry.instance_location, message);
message << "\"\n";
message << " at evaluate path \"";
sourcemeta::core::stringify(entry.evaluate_path, message);
message << "\"\n";
}

return {{{"example"}}, std::move(message).str()};
}

auto transform(JSON &schema, const Result &) const -> void override {
schema.erase("example");
}

private:
const Compiler compiler_;
};
74 changes: 74 additions & 0 deletions test/alterschema/alterschema_lint_openapi_3_0_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1526,3 +1526,77 @@ TEST(openapi_3_0_unsatisfiable_drop_validation_with_ref_sibling) {

EXPECT_EQ(document, expected);
}

TEST(openapi_3_0_valid_example) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"title": "Test",
"description": "A test schema",
"type": "string",
"example": 1
})JSON");

LINT_AND_FIX_WITH_DEFAULT_DIALECT(document, result, traces,
OPENAPI_3_0_DIALECT);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 1);
EXPECT_LINT_TRACE(traces, 0, "", "valid_example",
"Only set an `example` value that validates against the "
"schema",
true);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"title": "Test",
"description": "A test schema",
"type": "string"
})JSON");

EXPECT_EQ(document, expected);
}

TEST(openapi_3_0_valid_example_matching_instance) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"title": "Test",
"description": "A test schema",
"type": "string",
"example": "foo"
})JSON");

LINT_WITHOUT_FIX_WITH_DEFAULT_DIALECT(document, result, traces,
OPENAPI_3_0_DIALECT);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 0);
}

TEST(openapi_3_0_valid_example_sibling_to_ref) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"title": "Test",
"description": "A test schema",
"properties": {
"foo": { "$ref": "#/properties/bar", "example": 1 },
"bar": { "type": "string" }
}
})JSON");

LINT_AND_FIX_WITH_DEFAULT_DIALECT(document, result, traces,
OPENAPI_3_0_DIALECT);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 1);
EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "draft_ref_siblings",
"In Draft 7 and older dialects, keywords sibling to "
"`$ref` are never evaluated",
true);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"title": "Test",
"description": "A test schema",
"properties": {
"foo": { "$ref": "#/properties/bar" },
"bar": { "type": "string" }
}
})JSON");

EXPECT_EQ(document, expected);
}
189 changes: 189 additions & 0 deletions test/alterschema/alterschema_lint_openapi_3_1_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,192 @@ TEST(openapi_3_1_pattern_properties_non_ecma_regex_valid) {
EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 0);
}

TEST(openapi_3_1_valid_examples) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"title": "Test",
"description": "A test schema",
"examples": [ {} ],
"properties": {
"foo": { "type": "string", "examples": [ 1 ] }
}
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 1);
EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_examples",
"Only include instances in the `examples` array that "
"validate against the schema",
true);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"title": "Test",
"description": "A test schema",
"examples": [ {} ],
"properties": {
"foo": { "type": "string" }
}
})JSON");

EXPECT_EQ(document, expected);
}

TEST(openapi_3_1_valid_examples_matching_instances) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"title": "Test",
"description": "A test schema",
"examples": [ {} ],
"properties": {
"foo": { "type": "string", "examples": [ "bar" ] }
}
})JSON");

LINT_WITHOUT_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 0);
}

TEST(openapi_3_1_duplicate_examples) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"title": "Test",
"description": "A test schema",
"examples": [ {} ],
"properties": {
"foo": { "type": "string", "examples": [ "bar", "bar" ] }
}
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 1);
EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "duplicate_examples",
"Setting duplicate values in `examples` is redundant",
true);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"title": "Test",
"description": "A test schema",
"examples": [ {} ],
"properties": {
"foo": { "type": "string", "examples": [ "bar" ] }
}
})JSON");

EXPECT_EQ(document, expected);
}

TEST(openapi_3_1_top_level_examples) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"title": "Test",
"description": "A test schema",
"type": "string"
})JSON");

LINT_WITHOUT_FIX(document, result, traces);

EXPECT_FALSE(result.first);
EXPECT_EQ(traces.size(), 1);
EXPECT_LINT_TRACE(traces, 0, "", "top_level_examples",
"Set a non-empty examples array at the top level of the "
"schema to illustrate the expected data",
false);
}

TEST(openapi_3_1_valid_example) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"title": "Test",
"description": "A test schema",
"examples": [ {} ],
"properties": {
"foo": { "type": "string", "example": 1 }
}
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 1);
EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example",
"Only set an `example` value that validates against the "
"schema",
true);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"title": "Test",
"description": "A test schema",
"examples": [ {} ],
"properties": {
"foo": { "type": "string" }
}
})JSON");

EXPECT_EQ(document, expected);
}

TEST(openapi_3_1_valid_example_matching_instance) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"title": "Test",
"description": "A test schema",
"examples": [ {} ],
"properties": {
"foo": { "type": "string", "example": "bar" }
}
})JSON");

LINT_WITHOUT_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 0);
}

TEST(openapi_3_1_valid_example_sibling_to_ref) {
sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"title": "Test",
"description": "A test schema",
"examples": [ {} ],
"properties": {
"foo": { "$ref": "#/$defs/helper", "example": 1 }
},
"$defs": {
"helper": { "type": "string" }
}
})JSON");

LINT_AND_FIX(document, result, traces);

EXPECT_TRUE(result.first);
EXPECT_EQ(traces.size(), 1);
EXPECT_LINT_TRACE(traces, 0, "/properties/foo", "valid_example",
"Only set an `example` value that validates against the "
"schema",
true);

const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://spec.openapis.org/oas/3.1/dialect/base",
"title": "Test",
"description": "A test schema",
"examples": [ {} ],
"properties": {
"foo": { "$ref": "#/$defs/helper" }
},
"$defs": {
"helper": { "type": "string" }
}
})JSON");

EXPECT_EQ(document, expected);
}
Loading
Loading