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
8 changes: 4 additions & 4 deletions src/alterschema/alterschema.cc
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,16 @@ auto WALK_UP(const JSON &root, const SchemaFrame &frame,
assert(!relative_pointer.empty() && relative_pointer.at(0).is_property());
const auto parent{frame.traverse(frame.uri(parent_pointer).value().get())};
assert(parent.has_value());
const auto parent_vocabularies{
frame.vocabularies(parent.value().get(), resolver)};
const auto keyword_type{
walker(relative_pointer.at(0).to_property(),
frame.vocabularies(parent.value().get(), resolver))
.type};
walker(relative_pointer.at(0).to_property(), parent_vocabularies).type};

if (!should_continue(keyword_type)) {
return std::nullopt;
}

if (matches(get(root, parent_pointer))) {
if (matches(get(root, parent_pointer), parent_vocabularies)) {
return std::cref(parent.value().get().pointer);
}

Expand Down
27 changes: 22 additions & 5 deletions src/alterschema/canonicalizer/items_implicit.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ class ItemsImplicit final : public SchemaTransformRule {

[[nodiscard]] auto
condition(const sourcemeta::core::JSON &schema,
const sourcemeta::core::JSON &,
const sourcemeta::core::JSON &root,
const sourcemeta::core::Vocabularies &vocabularies,
const sourcemeta::core::SchemaFrame &,
const sourcemeta::core::SchemaFrame::Location &,
const sourcemeta::core::SchemaWalker &,
const sourcemeta::core::SchemaResolver &) const
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 {
ONLY_CONTINUE_IF(
((vocabularies.contains(
Expand All @@ -31,6 +31,23 @@ class ItemsImplicit final : public SchemaTransformRule {
schema.is_object() && schema.defines("type") &&
schema.at("type").is_string() &&
schema.at("type").to_string() == "array" && !schema.defines("items"));
ONLY_CONTINUE_IF(
!(schema.defines("unevaluatedItems") &&
vocabularies.contains_any(
{Vocabularies::Known::JSON_Schema_2020_12_Unevaluated,
Vocabularies::Known::JSON_Schema_2019_09_Applicator})));
ONLY_CONTINUE_IF(
!WALK_UP_IN_PLACE_APPLICATORS(
root, frame, location, walker, resolver,
[](const JSON &ancestor,
const Vocabularies &ancestor_vocabularies) {
return ancestor.defines("unevaluatedItems") &&
ancestor_vocabularies.contains_any(
{Vocabularies::Known::JSON_Schema_2020_12_Unevaluated,
Vocabularies::Known::
JSON_Schema_2019_09_Applicator});
})
.has_value());
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion src/alterschema/canonicalizer/type_inherit_in_place.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class TypeInheritInPlace final : public SchemaTransformRule {
return IS_IN_PLACE_APPLICATOR(keyword_type) &&
keyword_type != SchemaKeywordType::ApplicatorElementsInPlace;
},
[](const JSON &ancestor_schema) {
[](const JSON &ancestor_schema, const Vocabularies &) {
return ancestor_schema.defines("type");
})};

Expand Down
2 changes: 1 addition & 1 deletion src/alterschema/common/required_properties_in_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class RequiredPropertiesInProperties final : public SchemaTransformRule {
!this->defined_in_properties_sibling(schema, property.to_string()) &&
!WALK_UP_IN_PLACE_APPLICATORS(
root, frame, location, walker, resolver,
[&](const JSON &ancestor) {
[&](const JSON &ancestor, const Vocabularies &) {
return this->defined_in_properties_sibling(
ancestor, property.to_string());
})
Expand Down
104 changes: 104 additions & 0 deletions test/alterschema/alterschema_canonicalize_2019_09_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -687,3 +687,107 @@ TEST(AlterSchema_canonicalize_2019_09, items_implicit_1) {

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_canonicalize_2019_09,
items_implicit_skipped_with_unevaluated_items) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These regressions cover unevaluatedItems on an ancestor of the array schema; it may also be worth exercising the case where the array schema itself defines unevaluatedItems with no in-place-applicator parent, since WALK_UP_IN_PLACE_APPLICATORS won’t detect that.

Severity: low

Other Locations
  • test/alterschema/alterschema_canonicalize_2020_12_test.cc:1701

Fix This in Augment

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

auto document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"contains": { "type": "boolean" },
"unevaluatedItems": false
})JSON");

CANONICALIZE(document, result, traces);

EXPECT_TRUE(result.first);

const auto expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"anyOf": [
{ "enum": [ null ] },
{ "enum": [ false, true ] },
{
"type": "object",
"minProperties": 0,
"properties": {}
},
{
"type": "array",
"minItems": 0,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 10, 2026

Choose a reason for hiding this comment

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

P1: Preserve the explicit items schema in this expected output; removing it changes the array semantics.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/alterschema/alterschema_canonicalize_2019_09_test.cc, line 715:

<comment>Preserve the explicit `items` schema in this expected output; removing it changes the array semantics.</comment>

<file context>
@@ -687,3 +687,85 @@ TEST(AlterSchema_canonicalize_2019_09, items_implicit_1) {
+      },
+      {
+        "type": "array",
+        "minItems": 0,
+        "contains": {
+          "enum": [ false, true ]
</file context>
Suggested change
"minItems": 0,
"minItems": 0,
"items": {
"enum": [ false, true ]
},
Fix with Cubic

"contains": {
"enum": [ false, true ]
}
},
{
"type": "string",
"minLength": 0
},
{ "type": "number" }
],
"unevaluatedItems": false
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_canonicalize_2019_09,
items_implicit_skipped_with_direct_unevaluated_items) {
auto document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "array",
"unevaluatedItems": false
})JSON");

CANONICALIZE(document, result, traces);

EXPECT_TRUE(result.first);

const auto expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "array",
"minItems": 0,
"unevaluatedItems": false
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_canonicalize_2019_09,
items_implicit_skipped_with_anyof_and_unevaluated_items) {
auto document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"anyOf": [
{ "items": { "type": "boolean" } },
true
],
"unevaluatedItems": false
})JSON");

CANONICALIZE(document, result, traces);

EXPECT_TRUE(result.first);

const auto expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2019-09/schema",
"anyOf": [
{ "enum": [ null ] },
{ "enum": [ false, true ] },
{
"type": "object",
"minProperties": 0,
"properties": {}
},
{
"type": "array",
"minItems": 0
},
{
"type": "string",
"minLength": 0
},
{ "type": "number" }
],
"unevaluatedItems": false
})JSON");

EXPECT_EQ(document, expected);
}
104 changes: 104 additions & 0 deletions test/alterschema/alterschema_canonicalize_2020_12_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,110 @@ TEST(AlterSchema_canonicalize_2020_12,
EXPECT_EQ(document, expected);
}

TEST(AlterSchema_canonicalize_2020_12,
items_implicit_skipped_with_direct_unevaluated_items) {
auto document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"unevaluatedItems": false
})JSON");

CANONICALIZE(document, result, traces);

EXPECT_TRUE(result.first);

const auto expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "array",
"minItems": 0,
"unevaluatedItems": false
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_canonicalize_2020_12,
items_implicit_skipped_with_anyof_items_and_unevaluated_items) {
auto document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{ "items": { "type": "boolean" } },
true
],
"unevaluatedItems": false
})JSON");

CANONICALIZE(document, result, traces);

EXPECT_TRUE(result.first);

const auto expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{ "enum": [ null ] },
{ "enum": [ false, true ] },
{
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 10, 2026

Choose a reason for hiding this comment

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

P2: Preserve prefixItems here; removing it changes how unevaluatedItems: false validates non-empty arrays.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/alterschema/alterschema_canonicalize_2020_12_test.cc, line 1720:

<comment>Preserve `prefixItems` here; removing it changes how `unevaluatedItems: false` validates non-empty arrays.</comment>

<file context>
@@ -1697,6 +1697,88 @@ TEST(AlterSchema_canonicalize_2020_12,
+    "anyOf": [
+      { "enum": [ null ] },
+      { "enum": [ false, true ] },
+      {
+        "type": "object",
+        "minProperties": 0,
</file context>
Fix with Cubic

"type": "object",
"minProperties": 0,
"properties": {}
},
{
"type": "array",
"minItems": 0
},
{
"type": "string",
"minLength": 0
},
{ "type": "number" }
],
"unevaluatedItems": false
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_canonicalize_2020_12,
items_implicit_skipped_with_anyof_prefix_items_and_unevaluated_items) {
auto document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{ "prefixItems": [ { "type": "boolean" } ] },
true
],
"unevaluatedItems": false
})JSON");

CANONICALIZE(document, result, traces);

EXPECT_TRUE(result.first);

const auto expected = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"anyOf": [
{ "enum": [ null ] },
{ "enum": [ false, true ] },
{
"type": "object",
"minProperties": 0,
"properties": {}
},
{
"type": "array",
"minItems": 0
},
{
"type": "string",
"minLength": 0
},
{ "type": "number" }
],
"unevaluatedItems": false
})JSON");

EXPECT_EQ(document, expected);
}

TEST(AlterSchema_canonicalize_2020_12, ref_into_subschema_via_absolute_uri) {
auto document = sourcemeta::core::parse_json(R"JSON({
"$schema": "https://json-schema.org/draft/2020-12/schema",
Expand Down
Loading