From 931179d3b9c64d8520f54843ad05c0c8b595a8db Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Thu, 30 Apr 2026 11:07:03 -0400 Subject: [PATCH 1/3] Stricter `$id` framing checks in 2019-09 and 2020-12 Signed-off-by: Juan Cruz Viotti --- src/core/jsonschema/frame.cc | 5 +++ .../jsonschema_frame_2019_09_test.cc | 45 +++++++++++++++++++ .../jsonschema_frame_2020_12_test.cc | 45 +++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/src/core/jsonschema/frame.cc b/src/core/jsonschema/frame.cc index 549e96615..8808ae75c 100644 --- a/src/core/jsonschema/frame.cc +++ b/src/core/jsonschema/frame.cc @@ -661,6 +661,11 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, const auto maybe_fragment{maybe_relative.fragment()}; + // Both 2019-09 and 2020-12 state: + // + // "$id" MUST NOT contain a non-empty fragment, and SHOULD NOT + // contain an empty fragment. + // // See // https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#rfc.section.8.2.2 // See diff --git a/test/jsonschema/jsonschema_frame_2019_09_test.cc b/test/jsonschema/jsonschema_frame_2019_09_test.cc index 372c70443..a0301a045 100644 --- a/test/jsonschema/jsonschema_frame_2019_09_test.cc +++ b/test/jsonschema/jsonschema_frame_2019_09_test.cc @@ -3263,3 +3263,48 @@ TEST(JSONSchema_frame_2019_09, anchor_with_valid_colon) { EXPECT_FRAME_LOCATION_REACHABLE( frame, Static, "https://www.sourcemeta.com/schema#foo:bar", frame.root()); } + +TEST(JSONSchema_frame_2019_09, top_level_id_absolute_with_non_empty_fragment) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.sourcemeta.com/schema#foo", + "$schema": "https://json-schema.org/draft/2019-09/schema" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + + try { + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + FAIL(); + } catch (const sourcemeta::core::SchemaFrameError &error) { + EXPECT_EQ(error.identifier(), "https://www.sourcemeta.com/schema#foo"); + } catch (...) { + FAIL(); + } +} + +TEST(JSONSchema_frame_2019_09, nested_id_absolute_with_non_empty_fragment) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.sourcemeta.com/schema", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$defs": { + "foo": { + "$id": "https://www.sourcemeta.com/nested#foo" + } + } + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + + try { + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + FAIL(); + } catch (const sourcemeta::core::SchemaFrameError &error) { + EXPECT_EQ(error.identifier(), "https://www.sourcemeta.com/nested#foo"); + } catch (...) { + FAIL(); + } +} diff --git a/test/jsonschema/jsonschema_frame_2020_12_test.cc b/test/jsonschema/jsonschema_frame_2020_12_test.cc index 0af7129f6..0d79c1b05 100644 --- a/test/jsonschema/jsonschema_frame_2020_12_test.cc +++ b/test/jsonschema/jsonschema_frame_2020_12_test.cc @@ -7702,3 +7702,48 @@ TEST(JSONSchema_frame_2020_12, dynamic_anchor_with_valid_leading_underscore) { EXPECT_FRAME_LOCATION_REACHABLE( frame, Static, "https://www.sourcemeta.com/schema#_foo", frame.root()); } + +TEST(JSONSchema_frame_2020_12, top_level_id_absolute_with_non_empty_fragment) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.sourcemeta.com/schema#foo", + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + + try { + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + FAIL(); + } catch (const sourcemeta::core::SchemaFrameError &error) { + EXPECT_EQ(error.identifier(), "https://www.sourcemeta.com/schema#foo"); + } catch (...) { + FAIL(); + } +} + +TEST(JSONSchema_frame_2020_12, nested_id_absolute_with_non_empty_fragment) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.sourcemeta.com/schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "foo": { + "$id": "https://www.sourcemeta.com/nested#foo" + } + } + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + + try { + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + FAIL(); + } catch (const sourcemeta::core::SchemaFrameError &error) { + EXPECT_EQ(error.identifier(), "https://www.sourcemeta.com/nested#foo"); + } catch (...) { + FAIL(); + } +} From 95e3c7bf54f724a3680ffb6b6e362415f37c26d0 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Thu, 30 Apr 2026 11:26:54 -0400 Subject: [PATCH 2/3] Test the id # Signed-off-by: Juan Cruz Viotti --- src/core/jsonschema/frame.cc | 8 +- src/core/jsonschema/jsonschema.cc | 12 +++ .../jsonschema_frame_2019_09_test.cc | 101 ++++++++++++++++++ .../jsonschema_frame_2020_12_test.cc | 101 ++++++++++++++++++ .../jsonschema_frame_draft3_test.cc | 39 +++++++ .../jsonschema_frame_draft4_test.cc | 39 +++++++ .../jsonschema_frame_draft6_test.cc | 39 +++++++ .../jsonschema_frame_draft7_test.cc | 39 +++++++ .../jsonschema_identify_2019_09_test.cc | 31 ++++++ .../jsonschema_identify_2020_12_test.cc | 31 ++++++ .../jsonschema_identify_draft3_test.cc | 10 ++ .../jsonschema_identify_draft4_test.cc | 10 ++ .../jsonschema_identify_draft6_test.cc | 10 ++ .../jsonschema_identify_draft7_test.cc | 10 ++ 14 files changed, 478 insertions(+), 2 deletions(-) diff --git a/src/core/jsonschema/frame.cc b/src/core/jsonschema/frame.cc index 8808ae75c..33e1ecf02 100644 --- a/src/core/jsonschema/frame.cc +++ b/src/core/jsonschema/frame.cc @@ -165,7 +165,9 @@ auto find_anchors(const sourcemeta::core::JSON &schema, if (id_value) { assert(id_value->is_string()); const std::string_view id_view{id_value->to_string()}; - if (id_view.starts_with('#')) { + // A bare "#" carries no anchor name, so we treat it as no anchor at + // all. + if (id_view.starts_with('#') && id_view.size() > 1) { // The original string is "#fragment", skip the '#' result.emplace_back(id_view.substr(1), AnchorType::Static); } @@ -181,7 +183,9 @@ auto find_anchors(const sourcemeta::core::JSON &schema, if (id_value) { assert(id_value->is_string()); const std::string_view id_view{id_value->to_string()}; - if (id_view.starts_with('#')) { + // A bare "#" carries no anchor name, so we treat it as no anchor at + // all. + if (id_view.starts_with('#') && id_view.size() > 1) { // The original string is "#fragment", skip the '#' result.emplace_back(id_view.substr(1), AnchorType::Static); } diff --git a/src/core/jsonschema/jsonschema.cc b/src/core/jsonschema/jsonschema.cc index 1fadb1870..6dc3f86ce 100644 --- a/src/core/jsonschema/jsonschema.cc +++ b/src/core/jsonschema/jsonschema.cc @@ -180,6 +180,18 @@ auto sourcemeta::core::identify(const JSON &schema, return default_id; } + // An identifier consisting solely of the empty-fragment marker "#" + // canonicalizes to the empty string and resolves to the parent base. + // It carries no information, so we treat it as if no identifier was + // declared at all (i.e. no new schema resource is introduced). + // See + // https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#rfc.section.8.2.2 + // See + // https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#section-8.2.1-5 + if (identifier.to_string() == "#") { + return default_id; + } + return identifier.to_string(); } diff --git a/test/jsonschema/jsonschema_frame_2019_09_test.cc b/test/jsonschema/jsonschema_frame_2019_09_test.cc index a0301a045..1202ad03a 100644 --- a/test/jsonschema/jsonschema_frame_2019_09_test.cc +++ b/test/jsonschema/jsonschema_frame_2019_09_test.cc @@ -3308,3 +3308,104 @@ TEST(JSONSchema_frame_2019_09, nested_id_absolute_with_non_empty_fragment) { FAIL(); } } + +TEST(JSONSchema_frame_2019_09, top_level_id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "https://json-schema.org/draft/2019-09/schema" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "https://json-schema.org/draft/2019-09/schema", + JSON_Schema_2019_09, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$id", "/$id", "https://json-schema.org/draft/2019-09/schema", + JSON_Schema_2019_09, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", + "https://json-schema.org/draft/2019-09/schema", JSON_Schema_2019_09, "", + false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "https://json-schema.org/draft/2019-09/schema", + "https://json-schema.org/draft/2019-09/schema", std::nullopt, + "https://json-schema.org/draft/2019-09/schema"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} + +TEST(JSONSchema_frame_2019_09, nested_id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.sourcemeta.com/schema", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$defs": { + "foo": { + "$id": "#" + } + } + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_EQ(frame.root(), "https://www.sourcemeta.com/schema"); + + EXPECT_EQ(frame.locations().size(), 6); + + EXPECT_FRAME_STATIC_2019_09_RESOURCE( + frame, "https://www.sourcemeta.com/schema", + "https://www.sourcemeta.com/schema", "", + "https://www.sourcemeta.com/schema", "", std::nullopt, false, false); + EXPECT_FRAME_STATIC_2019_09_POINTER( + frame, "https://www.sourcemeta.com/schema#/$id", + "https://www.sourcemeta.com/schema", "/$id", + "https://www.sourcemeta.com/schema", "/$id", "", false, false); + EXPECT_FRAME_STATIC_2019_09_POINTER( + frame, "https://www.sourcemeta.com/schema#/$schema", + "https://www.sourcemeta.com/schema", "/$schema", + "https://www.sourcemeta.com/schema", "/$schema", "", false, false); + EXPECT_FRAME_STATIC_2019_09_POINTER( + frame, "https://www.sourcemeta.com/schema#/$defs", + "https://www.sourcemeta.com/schema", "/$defs", + "https://www.sourcemeta.com/schema", "/$defs", "", false, false); + EXPECT_FRAME_STATIC_2019_09_SUBSCHEMA( + frame, "https://www.sourcemeta.com/schema#/$defs/foo", + "https://www.sourcemeta.com/schema", "/$defs/foo", + "https://www.sourcemeta.com/schema", "/$defs/foo", "", false, true); + EXPECT_FRAME_STATIC_2019_09_POINTER( + frame, "https://www.sourcemeta.com/schema#/$defs/foo/$id", + "https://www.sourcemeta.com/schema", "/$defs/foo/$id", + "https://www.sourcemeta.com/schema", "/$defs/foo/$id", "/$defs/foo", + false, true); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "https://json-schema.org/draft/2019-09/schema", + "https://json-schema.org/draft/2019-09/schema", std::nullopt, + "https://json-schema.org/draft/2019-09/schema"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE( + frame, Static, "https://www.sourcemeta.com/schema", frame.root()); +} diff --git a/test/jsonschema/jsonschema_frame_2020_12_test.cc b/test/jsonschema/jsonschema_frame_2020_12_test.cc index 0d79c1b05..c159e9ae9 100644 --- a/test/jsonschema/jsonschema_frame_2020_12_test.cc +++ b/test/jsonschema/jsonschema_frame_2020_12_test.cc @@ -7747,3 +7747,104 @@ TEST(JSONSchema_frame_2020_12, nested_id_absolute_with_non_empty_fragment) { FAIL(); } } + +TEST(JSONSchema_frame_2020_12, top_level_id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "https://json-schema.org/draft/2020-12/schema", + JSON_Schema_2020_12, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$id", "/$id", "https://json-schema.org/draft/2020-12/schema", + JSON_Schema_2020_12, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", + "https://json-schema.org/draft/2020-12/schema", JSON_Schema_2020_12, "", + false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "https://json-schema.org/draft/2020-12/schema", + "https://json-schema.org/draft/2020-12/schema", std::nullopt, + "https://json-schema.org/draft/2020-12/schema"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} + +TEST(JSONSchema_frame_2020_12, nested_id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.sourcemeta.com/schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "foo": { + "$id": "#" + } + } + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_EQ(frame.root(), "https://www.sourcemeta.com/schema"); + + EXPECT_EQ(frame.locations().size(), 6); + + EXPECT_FRAME_STATIC_2020_12_RESOURCE( + frame, "https://www.sourcemeta.com/schema", + "https://www.sourcemeta.com/schema", "", + "https://www.sourcemeta.com/schema", "", std::nullopt, false, false); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.sourcemeta.com/schema#/$id", + "https://www.sourcemeta.com/schema", "/$id", + "https://www.sourcemeta.com/schema", "/$id", "", false, false); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.sourcemeta.com/schema#/$schema", + "https://www.sourcemeta.com/schema", "/$schema", + "https://www.sourcemeta.com/schema", "/$schema", "", false, false); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.sourcemeta.com/schema#/$defs", + "https://www.sourcemeta.com/schema", "/$defs", + "https://www.sourcemeta.com/schema", "/$defs", "", false, false); + EXPECT_FRAME_STATIC_2020_12_SUBSCHEMA( + frame, "https://www.sourcemeta.com/schema#/$defs/foo", + "https://www.sourcemeta.com/schema", "/$defs/foo", + "https://www.sourcemeta.com/schema", "/$defs/foo", "", false, true); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.sourcemeta.com/schema#/$defs/foo/$id", + "https://www.sourcemeta.com/schema", "/$defs/foo/$id", + "https://www.sourcemeta.com/schema", "/$defs/foo/$id", "/$defs/foo", + false, true); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "https://json-schema.org/draft/2020-12/schema", + "https://json-schema.org/draft/2020-12/schema", std::nullopt, + "https://json-schema.org/draft/2020-12/schema"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE( + frame, Static, "https://www.sourcemeta.com/schema", frame.root()); +} diff --git a/test/jsonschema/jsonschema_frame_draft3_test.cc b/test/jsonschema/jsonschema_frame_draft3_test.cc index 160ddef97..16f79dc97 100644 --- a/test/jsonschema/jsonschema_frame_draft3_test.cc +++ b/test/jsonschema/jsonschema_frame_draft3_test.cc @@ -823,3 +823,42 @@ TEST(JSONSchema_frame_draft3, nested_relative_ref_with_id) { "https://example.com#/additionalProperties", "https://example.com#/additionalProperties"); } + +TEST(JSONSchema_frame_draft3, top_level_id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "#", + "$schema": "http://json-schema.org/draft-03/schema#" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "http://json-schema.org/draft-03/schema#", + JSON_Schema_Draft_3, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/id", "/id", "http://json-schema.org/draft-03/schema#", + JSON_Schema_Draft_3, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", "http://json-schema.org/draft-03/schema#", + JSON_Schema_Draft_3, "", false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "http://json-schema.org/draft-03/schema", + "http://json-schema.org/draft-03/schema", std::nullopt, + "http://json-schema.org/draft-03/schema#"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} diff --git a/test/jsonschema/jsonschema_frame_draft4_test.cc b/test/jsonschema/jsonschema_frame_draft4_test.cc index c728af810..14460ca04 100644 --- a/test/jsonschema/jsonschema_frame_draft4_test.cc +++ b/test/jsonschema/jsonschema_frame_draft4_test.cc @@ -1300,3 +1300,42 @@ TEST(JSONSchema_frame_draft4, invalid_id_not_string) { FAIL(); } } + +TEST(JSONSchema_frame_draft4, top_level_id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "#", + "$schema": "http://json-schema.org/draft-04/schema#" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "http://json-schema.org/draft-04/schema#", + JSON_Schema_Draft_4, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/id", "/id", "http://json-schema.org/draft-04/schema#", + JSON_Schema_Draft_4, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", "http://json-schema.org/draft-04/schema#", + JSON_Schema_Draft_4, "", false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "http://json-schema.org/draft-04/schema", + "http://json-schema.org/draft-04/schema", std::nullopt, + "http://json-schema.org/draft-04/schema#"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} diff --git a/test/jsonschema/jsonschema_frame_draft6_test.cc b/test/jsonschema/jsonschema_frame_draft6_test.cc index 1730621cc..368483836 100644 --- a/test/jsonschema/jsonschema_frame_draft6_test.cc +++ b/test/jsonschema/jsonschema_frame_draft6_test.cc @@ -1250,3 +1250,42 @@ TEST(JSONSchema_frame_draft6, propertyNames_with_nested_applicators) { EXPECT_FRAME_LOCATION_NON_REACHABLE( frame, Static, "#/propertyNames/definitions/test", frame.root()); } + +TEST(JSONSchema_frame_draft6, top_level_id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "http://json-schema.org/draft-06/schema#" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "http://json-schema.org/draft-06/schema#", + JSON_Schema_Draft_6, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$id", "/$id", "http://json-schema.org/draft-06/schema#", + JSON_Schema_Draft_6, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", "http://json-schema.org/draft-06/schema#", + JSON_Schema_Draft_6, "", false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "http://json-schema.org/draft-06/schema", + "http://json-schema.org/draft-06/schema", std::nullopt, + "http://json-schema.org/draft-06/schema#"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} diff --git a/test/jsonschema/jsonschema_frame_draft7_test.cc b/test/jsonschema/jsonschema_frame_draft7_test.cc index 6e52bee2b..fe652ed5c 100644 --- a/test/jsonschema/jsonschema_frame_draft7_test.cc +++ b/test/jsonschema/jsonschema_frame_draft7_test.cc @@ -1244,3 +1244,42 @@ TEST(JSONSchema_frame_draft7, nested_relative_ref_with_id) { EXPECT_FRAME_LOCATION_REACHABLE( frame, Static, "https://example.com#/additionalProperties", frame.root()); } + +TEST(JSONSchema_frame_draft7, top_level_id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "http://json-schema.org/draft-07/schema#" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "http://json-schema.org/draft-07/schema#", + JSON_Schema_Draft_7, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$id", "/$id", "http://json-schema.org/draft-07/schema#", + JSON_Schema_Draft_7, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", "http://json-schema.org/draft-07/schema#", + JSON_Schema_Draft_7, "", false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "http://json-schema.org/draft-07/schema", + "http://json-schema.org/draft-07/schema", std::nullopt, + "http://json-schema.org/draft-07/schema#"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} diff --git a/test/jsonschema/jsonschema_identify_2019_09_test.cc b/test/jsonschema/jsonschema_identify_2019_09_test.cc index 66c4278a7..106d608ff 100644 --- a/test/jsonschema/jsonschema_identify_2019_09_test.cc +++ b/test/jsonschema/jsonschema_identify_2019_09_test.cc @@ -222,3 +222,34 @@ TEST(JSONSchema_identify_2019_09, reidentify_set_with_top_level_ref) { EXPECT_EQ(document, expected); } + +TEST(JSONSchema_identify_2019_09, id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "https://json-schema.org/draft/2019-09/schema" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +} + +TEST(JSONSchema_identify_2019_09, id_empty_fragment_only_with_default) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "https://json-schema.org/draft/2019-09/schema" + })JSON"); + const auto id{sourcemeta::core::identify(document, + sourcemeta::core::schema_resolver, + "", "https://example.com/fallback")}; + EXPECT_EQ(id, "https://example.com/fallback"); +} + +TEST(JSONSchema_identify_2019_09, id_empty_fragment_only_base_dialect) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "https://json-schema.org/draft/2019-09/schema" + })JSON"); + const auto id{sourcemeta::core::identify( + document, sourcemeta::core::SchemaBaseDialect::JSON_Schema_2019_09)}; + EXPECT_TRUE(id.empty()); +} diff --git a/test/jsonschema/jsonschema_identify_2020_12_test.cc b/test/jsonschema/jsonschema_identify_2020_12_test.cc index 951ab37ee..72a2230e3 100644 --- a/test/jsonschema/jsonschema_identify_2020_12_test.cc +++ b/test/jsonschema/jsonschema_identify_2020_12_test.cc @@ -222,3 +222,34 @@ TEST(JSONSchema_identify_2020_12, reidentify_set_with_top_level_ref) { EXPECT_EQ(document, expected); } + +TEST(JSONSchema_identify_2020_12, id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +} + +TEST(JSONSchema_identify_2020_12, id_empty_fragment_only_with_default) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON"); + const auto id{sourcemeta::core::identify(document, + sourcemeta::core::schema_resolver, + "", "https://example.com/fallback")}; + EXPECT_EQ(id, "https://example.com/fallback"); +} + +TEST(JSONSchema_identify_2020_12, id_empty_fragment_only_base_dialect) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON"); + const auto id{sourcemeta::core::identify( + document, sourcemeta::core::SchemaBaseDialect::JSON_Schema_2020_12)}; + EXPECT_TRUE(id.empty()); +} diff --git a/test/jsonschema/jsonschema_identify_draft3_test.cc b/test/jsonschema/jsonschema_identify_draft3_test.cc index 53c187eb1..1da4d6403 100644 --- a/test/jsonschema/jsonschema_identify_draft3_test.cc +++ b/test/jsonschema/jsonschema_identify_draft3_test.cc @@ -230,3 +230,13 @@ TEST(JSONSchema_identify_draft3, sourcemeta::core::schema_resolver), sourcemeta::core::SchemaReferenceObjectResourceError); } + +TEST(JSONSchema_identify_draft3, id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "#", + "$schema": "http://json-schema.org/draft-03/schema#" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +} diff --git a/test/jsonschema/jsonschema_identify_draft4_test.cc b/test/jsonschema/jsonschema_identify_draft4_test.cc index 29e53f987..041c19b3a 100644 --- a/test/jsonschema/jsonschema_identify_draft4_test.cc +++ b/test/jsonschema/jsonschema_identify_draft4_test.cc @@ -229,3 +229,13 @@ TEST(JSONSchema_identify_draft4, reidentify_set_with_top_level_ref_and_allof) { sourcemeta::core::schema_resolver), sourcemeta::core::SchemaReferenceObjectResourceError); } + +TEST(JSONSchema_identify_draft4, id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "#", + "$schema": "http://json-schema.org/draft-04/schema#" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +} diff --git a/test/jsonschema/jsonschema_identify_draft6_test.cc b/test/jsonschema/jsonschema_identify_draft6_test.cc index 49c66a98e..4f0ea36b6 100644 --- a/test/jsonschema/jsonschema_identify_draft6_test.cc +++ b/test/jsonschema/jsonschema_identify_draft6_test.cc @@ -229,3 +229,13 @@ TEST(JSONSchema_identify_draft6, reidentify_set_with_top_level_ref_and_allof) { sourcemeta::core::schema_resolver), sourcemeta::core::SchemaReferenceObjectResourceError); } + +TEST(JSONSchema_identify_draft6, id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "http://json-schema.org/draft-06/schema#" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +} diff --git a/test/jsonschema/jsonschema_identify_draft7_test.cc b/test/jsonschema/jsonschema_identify_draft7_test.cc index f295d0637..8335a689f 100644 --- a/test/jsonschema/jsonschema_identify_draft7_test.cc +++ b/test/jsonschema/jsonschema_identify_draft7_test.cc @@ -229,3 +229,13 @@ TEST(JSONSchema_identify_draft7, reidentify_set_with_top_level_ref_and_allof) { sourcemeta::core::schema_resolver), sourcemeta::core::SchemaReferenceObjectResourceError); } + +TEST(JSONSchema_identify_draft7, id_empty_fragment_only) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "#", + "$schema": "http://json-schema.org/draft-07/schema#" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +} From 1cfe72f4c5fca102182c02a0d7f968d1c249468e Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Thu, 30 Apr 2026 11:36:58 -0400 Subject: [PATCH 3/3] Empty id Signed-off-by: Juan Cruz Viotti --- src/core/jsonschema/jsonschema.cc | 13 +-- .../jsonschema_frame_2019_09_test.cc | 101 ++++++++++++++++++ .../jsonschema_frame_2020_12_test.cc | 101 ++++++++++++++++++ .../jsonschema_frame_draft3_test.cc | 39 +++++++ .../jsonschema_frame_draft4_test.cc | 39 +++++++ .../jsonschema_frame_draft6_test.cc | 39 +++++++ .../jsonschema_frame_draft7_test.cc | 39 +++++++ .../jsonschema_identify_2019_09_test.cc | 31 ++++++ .../jsonschema_identify_2020_12_test.cc | 31 ++++++ .../jsonschema_identify_draft3_test.cc | 10 ++ .../jsonschema_identify_draft4_test.cc | 10 ++ .../jsonschema_identify_draft6_test.cc | 10 ++ .../jsonschema_identify_draft7_test.cc | 10 ++ 13 files changed, 467 insertions(+), 6 deletions(-) diff --git a/src/core/jsonschema/jsonschema.cc b/src/core/jsonschema/jsonschema.cc index 6dc3f86ce..6c120f231 100644 --- a/src/core/jsonschema/jsonschema.cc +++ b/src/core/jsonschema/jsonschema.cc @@ -156,7 +156,7 @@ auto sourcemeta::core::identify(const JSON &schema, } const auto &identifier{schema.at(keyword)}; - if (!identifier.is_string() || identifier.empty()) { + if (!identifier.is_string()) { std::ostringstream value; sourcemeta::core::stringify(identifier, value); throw sourcemeta::core::SchemaKeywordError( @@ -180,15 +180,16 @@ auto sourcemeta::core::identify(const JSON &schema, return default_id; } - // An identifier consisting solely of the empty-fragment marker "#" - // canonicalizes to the empty string and resolves to the parent base. - // It carries no information, so we treat it as if no identifier was - // declared at all (i.e. no new schema resource is introduced). + // An empty string identifier and an identifier consisting solely of the + // empty-fragment marker "#" are both valid URI-references that resolve to + // the parent base, carrying no information. We treat them as if no + // identifier was declared at all (i.e. no new schema resource is + // introduced). // See // https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#rfc.section.8.2.2 // See // https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#section-8.2.1-5 - if (identifier.to_string() == "#") { + if (identifier.to_string().empty() || identifier.to_string() == "#") { return default_id; } diff --git a/test/jsonschema/jsonschema_frame_2019_09_test.cc b/test/jsonschema/jsonschema_frame_2019_09_test.cc index 1202ad03a..180754d8b 100644 --- a/test/jsonschema/jsonschema_frame_2019_09_test.cc +++ b/test/jsonschema/jsonschema_frame_2019_09_test.cc @@ -3409,3 +3409,104 @@ TEST(JSONSchema_frame_2019_09, nested_id_empty_fragment_only) { EXPECT_FRAME_LOCATION_REACHABLE( frame, Static, "https://www.sourcemeta.com/schema", frame.root()); } + +TEST(JSONSchema_frame_2019_09, top_level_id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "https://json-schema.org/draft/2019-09/schema" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "https://json-schema.org/draft/2019-09/schema", + JSON_Schema_2019_09, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$id", "/$id", "https://json-schema.org/draft/2019-09/schema", + JSON_Schema_2019_09, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", + "https://json-schema.org/draft/2019-09/schema", JSON_Schema_2019_09, "", + false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "https://json-schema.org/draft/2019-09/schema", + "https://json-schema.org/draft/2019-09/schema", std::nullopt, + "https://json-schema.org/draft/2019-09/schema"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} + +TEST(JSONSchema_frame_2019_09, nested_id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.sourcemeta.com/schema", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$defs": { + "foo": { + "$id": "" + } + } + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_EQ(frame.root(), "https://www.sourcemeta.com/schema"); + + EXPECT_EQ(frame.locations().size(), 6); + + EXPECT_FRAME_STATIC_2019_09_RESOURCE( + frame, "https://www.sourcemeta.com/schema", + "https://www.sourcemeta.com/schema", "", + "https://www.sourcemeta.com/schema", "", std::nullopt, false, false); + EXPECT_FRAME_STATIC_2019_09_POINTER( + frame, "https://www.sourcemeta.com/schema#/$id", + "https://www.sourcemeta.com/schema", "/$id", + "https://www.sourcemeta.com/schema", "/$id", "", false, false); + EXPECT_FRAME_STATIC_2019_09_POINTER( + frame, "https://www.sourcemeta.com/schema#/$schema", + "https://www.sourcemeta.com/schema", "/$schema", + "https://www.sourcemeta.com/schema", "/$schema", "", false, false); + EXPECT_FRAME_STATIC_2019_09_POINTER( + frame, "https://www.sourcemeta.com/schema#/$defs", + "https://www.sourcemeta.com/schema", "/$defs", + "https://www.sourcemeta.com/schema", "/$defs", "", false, false); + EXPECT_FRAME_STATIC_2019_09_SUBSCHEMA( + frame, "https://www.sourcemeta.com/schema#/$defs/foo", + "https://www.sourcemeta.com/schema", "/$defs/foo", + "https://www.sourcemeta.com/schema", "/$defs/foo", "", false, true); + EXPECT_FRAME_STATIC_2019_09_POINTER( + frame, "https://www.sourcemeta.com/schema#/$defs/foo/$id", + "https://www.sourcemeta.com/schema", "/$defs/foo/$id", + "https://www.sourcemeta.com/schema", "/$defs/foo/$id", "/$defs/foo", + false, true); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "https://json-schema.org/draft/2019-09/schema", + "https://json-schema.org/draft/2019-09/schema", std::nullopt, + "https://json-schema.org/draft/2019-09/schema"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE( + frame, Static, "https://www.sourcemeta.com/schema", frame.root()); +} diff --git a/test/jsonschema/jsonschema_frame_2020_12_test.cc b/test/jsonschema/jsonschema_frame_2020_12_test.cc index c159e9ae9..1e64b3f7c 100644 --- a/test/jsonschema/jsonschema_frame_2020_12_test.cc +++ b/test/jsonschema/jsonschema_frame_2020_12_test.cc @@ -7848,3 +7848,104 @@ TEST(JSONSchema_frame_2020_12, nested_id_empty_fragment_only) { EXPECT_FRAME_LOCATION_REACHABLE( frame, Static, "https://www.sourcemeta.com/schema", frame.root()); } + +TEST(JSONSchema_frame_2020_12, top_level_id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "https://json-schema.org/draft/2020-12/schema", + JSON_Schema_2020_12, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$id", "/$id", "https://json-schema.org/draft/2020-12/schema", + JSON_Schema_2020_12, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", + "https://json-schema.org/draft/2020-12/schema", JSON_Schema_2020_12, "", + false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "https://json-schema.org/draft/2020-12/schema", + "https://json-schema.org/draft/2020-12/schema", std::nullopt, + "https://json-schema.org/draft/2020-12/schema"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} + +TEST(JSONSchema_frame_2020_12, nested_id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.sourcemeta.com/schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "foo": { + "$id": "" + } + } + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_EQ(frame.root(), "https://www.sourcemeta.com/schema"); + + EXPECT_EQ(frame.locations().size(), 6); + + EXPECT_FRAME_STATIC_2020_12_RESOURCE( + frame, "https://www.sourcemeta.com/schema", + "https://www.sourcemeta.com/schema", "", + "https://www.sourcemeta.com/schema", "", std::nullopt, false, false); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.sourcemeta.com/schema#/$id", + "https://www.sourcemeta.com/schema", "/$id", + "https://www.sourcemeta.com/schema", "/$id", "", false, false); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.sourcemeta.com/schema#/$schema", + "https://www.sourcemeta.com/schema", "/$schema", + "https://www.sourcemeta.com/schema", "/$schema", "", false, false); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.sourcemeta.com/schema#/$defs", + "https://www.sourcemeta.com/schema", "/$defs", + "https://www.sourcemeta.com/schema", "/$defs", "", false, false); + EXPECT_FRAME_STATIC_2020_12_SUBSCHEMA( + frame, "https://www.sourcemeta.com/schema#/$defs/foo", + "https://www.sourcemeta.com/schema", "/$defs/foo", + "https://www.sourcemeta.com/schema", "/$defs/foo", "", false, true); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.sourcemeta.com/schema#/$defs/foo/$id", + "https://www.sourcemeta.com/schema", "/$defs/foo/$id", + "https://www.sourcemeta.com/schema", "/$defs/foo/$id", "/$defs/foo", + false, true); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "https://json-schema.org/draft/2020-12/schema", + "https://json-schema.org/draft/2020-12/schema", std::nullopt, + "https://json-schema.org/draft/2020-12/schema"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE( + frame, Static, "https://www.sourcemeta.com/schema", frame.root()); +} diff --git a/test/jsonschema/jsonschema_frame_draft3_test.cc b/test/jsonschema/jsonschema_frame_draft3_test.cc index 16f79dc97..e871997db 100644 --- a/test/jsonschema/jsonschema_frame_draft3_test.cc +++ b/test/jsonschema/jsonschema_frame_draft3_test.cc @@ -862,3 +862,42 @@ TEST(JSONSchema_frame_draft3, top_level_id_empty_fragment_only) { EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); } + +TEST(JSONSchema_frame_draft3, top_level_id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "", + "$schema": "http://json-schema.org/draft-03/schema#" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "http://json-schema.org/draft-03/schema#", + JSON_Schema_Draft_3, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/id", "/id", "http://json-schema.org/draft-03/schema#", + JSON_Schema_Draft_3, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", "http://json-schema.org/draft-03/schema#", + JSON_Schema_Draft_3, "", false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "http://json-schema.org/draft-03/schema", + "http://json-schema.org/draft-03/schema", std::nullopt, + "http://json-schema.org/draft-03/schema#"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} diff --git a/test/jsonschema/jsonschema_frame_draft4_test.cc b/test/jsonschema/jsonschema_frame_draft4_test.cc index 14460ca04..473a85ff3 100644 --- a/test/jsonschema/jsonschema_frame_draft4_test.cc +++ b/test/jsonschema/jsonschema_frame_draft4_test.cc @@ -1339,3 +1339,42 @@ TEST(JSONSchema_frame_draft4, top_level_id_empty_fragment_only) { EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); } + +TEST(JSONSchema_frame_draft4, top_level_id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "", + "$schema": "http://json-schema.org/draft-04/schema#" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "http://json-schema.org/draft-04/schema#", + JSON_Schema_Draft_4, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/id", "/id", "http://json-schema.org/draft-04/schema#", + JSON_Schema_Draft_4, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", "http://json-schema.org/draft-04/schema#", + JSON_Schema_Draft_4, "", false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "http://json-schema.org/draft-04/schema", + "http://json-schema.org/draft-04/schema", std::nullopt, + "http://json-schema.org/draft-04/schema#"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} diff --git a/test/jsonschema/jsonschema_frame_draft6_test.cc b/test/jsonschema/jsonschema_frame_draft6_test.cc index 368483836..361877a17 100644 --- a/test/jsonschema/jsonschema_frame_draft6_test.cc +++ b/test/jsonschema/jsonschema_frame_draft6_test.cc @@ -1289,3 +1289,42 @@ TEST(JSONSchema_frame_draft6, top_level_id_empty_fragment_only) { EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); } + +TEST(JSONSchema_frame_draft6, top_level_id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "http://json-schema.org/draft-06/schema#" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "http://json-schema.org/draft-06/schema#", + JSON_Schema_Draft_6, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$id", "/$id", "http://json-schema.org/draft-06/schema#", + JSON_Schema_Draft_6, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", "http://json-schema.org/draft-06/schema#", + JSON_Schema_Draft_6, "", false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "http://json-schema.org/draft-06/schema", + "http://json-schema.org/draft-06/schema", std::nullopt, + "http://json-schema.org/draft-06/schema#"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} diff --git a/test/jsonschema/jsonschema_frame_draft7_test.cc b/test/jsonschema/jsonschema_frame_draft7_test.cc index fe652ed5c..4b23c2437 100644 --- a/test/jsonschema/jsonschema_frame_draft7_test.cc +++ b/test/jsonschema/jsonschema_frame_draft7_test.cc @@ -1283,3 +1283,42 @@ TEST(JSONSchema_frame_draft7, top_level_id_empty_fragment_only) { EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); } + +TEST(JSONSchema_frame_draft7, top_level_id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "http://json-schema.org/draft-07/schema#" + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_TRUE(frame.root().empty()); + + EXPECT_EQ(frame.locations().size(), 3); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "http://json-schema.org/draft-07/schema#", + JSON_Schema_Draft_7, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$id", "/$id", "http://json-schema.org/draft-07/schema#", + JSON_Schema_Draft_7, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", "http://json-schema.org/draft-07/schema#", + JSON_Schema_Draft_7, "", false, false); + + // References + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "http://json-schema.org/draft-07/schema", + "http://json-schema.org/draft-07/schema", std::nullopt, + "http://json-schema.org/draft-07/schema#"); + + // Reachability + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); +} diff --git a/test/jsonschema/jsonschema_identify_2019_09_test.cc b/test/jsonschema/jsonschema_identify_2019_09_test.cc index 106d608ff..0bd8aa195 100644 --- a/test/jsonschema/jsonschema_identify_2019_09_test.cc +++ b/test/jsonschema/jsonschema_identify_2019_09_test.cc @@ -253,3 +253,34 @@ TEST(JSONSchema_identify_2019_09, id_empty_fragment_only_base_dialect) { document, sourcemeta::core::SchemaBaseDialect::JSON_Schema_2019_09)}; EXPECT_TRUE(id.empty()); } + +TEST(JSONSchema_identify_2019_09, id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "https://json-schema.org/draft/2019-09/schema" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +} + +TEST(JSONSchema_identify_2019_09, id_empty_string_with_default) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "https://json-schema.org/draft/2019-09/schema" + })JSON"); + const auto id{sourcemeta::core::identify(document, + sourcemeta::core::schema_resolver, + "", "https://example.com/fallback")}; + EXPECT_EQ(id, "https://example.com/fallback"); +} + +TEST(JSONSchema_identify_2019_09, id_empty_string_base_dialect) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "https://json-schema.org/draft/2019-09/schema" + })JSON"); + const auto id{sourcemeta::core::identify( + document, sourcemeta::core::SchemaBaseDialect::JSON_Schema_2019_09)}; + EXPECT_TRUE(id.empty()); +} diff --git a/test/jsonschema/jsonschema_identify_2020_12_test.cc b/test/jsonschema/jsonschema_identify_2020_12_test.cc index 72a2230e3..e4294fd1c 100644 --- a/test/jsonschema/jsonschema_identify_2020_12_test.cc +++ b/test/jsonschema/jsonschema_identify_2020_12_test.cc @@ -253,3 +253,34 @@ TEST(JSONSchema_identify_2020_12, id_empty_fragment_only_base_dialect) { document, sourcemeta::core::SchemaBaseDialect::JSON_Schema_2020_12)}; EXPECT_TRUE(id.empty()); } + +TEST(JSONSchema_identify_2020_12, id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +} + +TEST(JSONSchema_identify_2020_12, id_empty_string_with_default) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON"); + const auto id{sourcemeta::core::identify(document, + sourcemeta::core::schema_resolver, + "", "https://example.com/fallback")}; + EXPECT_EQ(id, "https://example.com/fallback"); +} + +TEST(JSONSchema_identify_2020_12, id_empty_string_base_dialect) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "https://json-schema.org/draft/2020-12/schema" + })JSON"); + const auto id{sourcemeta::core::identify( + document, sourcemeta::core::SchemaBaseDialect::JSON_Schema_2020_12)}; + EXPECT_TRUE(id.empty()); +} diff --git a/test/jsonschema/jsonschema_identify_draft3_test.cc b/test/jsonschema/jsonschema_identify_draft3_test.cc index 1da4d6403..b96f5214f 100644 --- a/test/jsonschema/jsonschema_identify_draft3_test.cc +++ b/test/jsonschema/jsonschema_identify_draft3_test.cc @@ -240,3 +240,13 @@ TEST(JSONSchema_identify_draft3, id_empty_fragment_only) { sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; EXPECT_TRUE(id.empty()); } + +TEST(JSONSchema_identify_draft3, id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "", + "$schema": "http://json-schema.org/draft-03/schema#" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +} diff --git a/test/jsonschema/jsonschema_identify_draft4_test.cc b/test/jsonschema/jsonschema_identify_draft4_test.cc index 041c19b3a..ff3342311 100644 --- a/test/jsonschema/jsonschema_identify_draft4_test.cc +++ b/test/jsonschema/jsonschema_identify_draft4_test.cc @@ -239,3 +239,13 @@ TEST(JSONSchema_identify_draft4, id_empty_fragment_only) { sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; EXPECT_TRUE(id.empty()); } + +TEST(JSONSchema_identify_draft4, id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "", + "$schema": "http://json-schema.org/draft-04/schema#" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +} diff --git a/test/jsonschema/jsonschema_identify_draft6_test.cc b/test/jsonschema/jsonschema_identify_draft6_test.cc index 4f0ea36b6..d7e563a5d 100644 --- a/test/jsonschema/jsonschema_identify_draft6_test.cc +++ b/test/jsonschema/jsonschema_identify_draft6_test.cc @@ -239,3 +239,13 @@ TEST(JSONSchema_identify_draft6, id_empty_fragment_only) { sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; EXPECT_TRUE(id.empty()); } + +TEST(JSONSchema_identify_draft6, id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "http://json-schema.org/draft-06/schema#" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +} diff --git a/test/jsonschema/jsonschema_identify_draft7_test.cc b/test/jsonschema/jsonschema_identify_draft7_test.cc index 8335a689f..3f409d1ee 100644 --- a/test/jsonschema/jsonschema_identify_draft7_test.cc +++ b/test/jsonschema/jsonschema_identify_draft7_test.cc @@ -239,3 +239,13 @@ TEST(JSONSchema_identify_draft7, id_empty_fragment_only) { sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; EXPECT_TRUE(id.empty()); } + +TEST(JSONSchema_identify_draft7, id_empty_string) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "", + "$schema": "http://json-schema.org/draft-07/schema#" + })JSON"); + const auto id{ + sourcemeta::core::identify(document, sourcemeta::core::schema_resolver)}; + EXPECT_TRUE(id.empty()); +}