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
65 changes: 65 additions & 0 deletions src/core/jsonschema/bundle.cc
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,63 @@ auto rebase_reference(const JSON::String &base,
return result.recompose();
}

// The absolute form of a URI spelled against a base, or nothing where what is
// spelled is no URI reference at all
auto absolute_uri(const std::string_view uri, const std::string_view base)
-> std::optional<JSON::String> {
try {
URI result{uri};
result.resolve_from(URI{base});
result.canonicalize();
return result.recompose();
} catch (const URIParseError &) {
return std::nullopt;
}
}

// OpenAPI Specification 3.1.1, Section 4.8.24 puts an External Documentation
// Object on a Schema Object, and Section 4.8.11 makes its `url` "The URI for
// the target documentation". Section 4.6 has every URI a description writes
// resolve against the base in effect where it sits, so one spelled relatively
// names a place of whatever document the schema lands in once the embedding
// takes that base away. Restating it as the URI it resolves to is what keeps
// it naming what it named
//
// RFC 3986, section 4.4 spares an empty or fragment-only reference from the
// same treatment elsewhere, a same-document reference needing no base beyond
// the one already in effect. That reasoning does not reach here, as the
// document one of those leads back to is the one the schema came from rather
// than the one it lands in, which is exactly what the embedding takes away
auto restate_external_documentation(JSON &subschema, const SchemaFrame &frame,
const SchemaWalker &walker,
const SchemaResolver &resolver) -> void {
frame.for_each_subschema([&subschema, &frame, &walker,
&resolver](const auto &location) -> void {
assert(!location.base.empty());

// A dialect that gives the keyword no meaning leaves it holding whatever
// the schema put there, which moving cannot break
if (!dialect_defines(walker, frame.vocabularies(location, resolver),
"externalDocs")) {
return;
}

const auto held{
to_pointer(location.pointer).concat(Pointer{"externalDocs", "url"})};
const auto *written{try_get(subschema, held)};
if (written == nullptr || !written->is_string()) {
return;
}

// What a description spells there is its own to answer for, as bundling is
// not where a Schema Object is checked
const auto resolved{absolute_uri(written->to_string(), location.base)};
if (resolved.has_value()) {
set(subschema, held, JSON{resolved.value()});
}
});
}

// The dialect a schema declares, falling back to the given default
auto declared_dialect(const JSON &schema,
const std::string_view default_dialect)
Expand Down Expand Up @@ -712,6 +769,14 @@ auto embed_references(
depth == 0 ? default_base : std::string_view{}, remaining};
charge(remaining, frame);

// The document the caller handed over stays where it already sits, so
// nothing takes a base away from it. Every remote reaches here having been
// resolved against an identity of its own, which the embedding is about to
// drop wherever the dialect reserves no keyword to carry it
if (depth > 0) {
restate_external_documentation(subschema, frame, walker, resolver);

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

If the entry $defs and a fetched document both contain an identical OpenAPI resource keyed by its absolute $id, src/core/jsonschema/bundle.cc:776-777 rewrites externalDocs.url: "docs.html" only in the fetched copy. Could you check this against elevate_embedded_resources() (src/core/jsonschema/bundle.cc:696-710), whose structural comparison then throws Conflicting embedded resources with the same identifier despite the resources originally being identical?

Severity: medium

Fix This in Augment

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

}

// Framing holds the base it resolves against to its canonical spelling, so
// every reference destination it reports carries that spelling rather than
// the one the caller wrote. Settling it once here is what lets a reference
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"pet": {
"$ref": "https://www.sourcemeta.com/pet"
}
}
},
"resolver": {
"https://www.sourcemeta.com/pet": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"externalDocs": {
"url": "docs/pet.html"
}
}
},
"result": {
"nonOfficialMetaschemas": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"pet": {
"$ref": "https://www.sourcemeta.com/pet"
}
},
"$defs": {
"https://www.sourcemeta.com/pet": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://www.sourcemeta.com/pet",
"type": "object",
"externalDocs": {
"url": "docs/pet.html"
}
}
}
},
"references": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"pet": {
"$ref": "https://www.sourcemeta.com/pet"
}
},
"$defs": {
"https://www.sourcemeta.com/pet": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://www.sourcemeta.com/pet",
"type": "object",
"externalDocs": {
"url": "docs/pet.html"
}
}
}
}
},
"inserted": {
"nonOfficialMetaschemas": [
{
"from": "https://www.sourcemeta.com/pet",
"at": "/$defs/https:~1~1www.sourcemeta.com~1pet"
}
],
"references": [
{
"from": "https://www.sourcemeta.com/pet",
"at": "/$defs/https:~1~1www.sourcemeta.com~1pet"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
"schema": {
"properties": {
"pet": {
"$ref": "https://www.sourcemeta.com/pet"
}
}
},
"resolver": {
"https://www.sourcemeta.com/pet": {
"type": "object",
"externalDocs": {
"url": "docs/pet.html"
},
"properties": {
"kind": {
"type": "string",
"externalDocs": {
"url": "docs/kind.html"
}
}
}
}
},
"defaultDialect": "tag:spec.openapis.org,2024-10-18:oas/3.0/dialect",
"result": {
"nonOfficialMetaschemas": {
"properties": {
"pet": {
"$ref": "#/x-definitions/https:~1~1www.sourcemeta.com~1pet"
}
},
"x-definitions": {
"https://www.sourcemeta.com/pet": {
"type": "object",
"externalDocs": {
"url": "https://www.sourcemeta.com/docs/pet.html"
},
"properties": {
"kind": {
"type": "string",
"externalDocs": {
"url": "https://www.sourcemeta.com/docs/kind.html"
}
}
}
}
}
},
"references": {
"properties": {
"pet": {
"$ref": "#/x-definitions/https:~1~1www.sourcemeta.com~1pet"
}
},
"x-definitions": {
"https://www.sourcemeta.com/pet": {
"type": "object",
"externalDocs": {
"url": "https://www.sourcemeta.com/docs/pet.html"
},
"properties": {
"kind": {
"type": "string",
"externalDocs": {
"url": "https://www.sourcemeta.com/docs/kind.html"
}
}
}
}
}
}
},
"inserted": {
"nonOfficialMetaschemas": [
{
"from": "https://www.sourcemeta.com/pet",
"at": "/x-definitions/https:~1~1www.sourcemeta.com~1pet"
}
],
"references": [
{
"from": "https://www.sourcemeta.com/pet",
"at": "/x-definitions/https:~1~1www.sourcemeta.com~1pet"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"schema": {
"properties": {
"pet": {
"$ref": "https://www.sourcemeta.com/pet"
}
}
},
"resolver": {
"https://www.sourcemeta.com/pet": {
"type": "object",
"externalDocs": {
"url": "https://docs.sourcemeta.com/pet.html"
}
},
"https://example.com/oas-3-1-dialect": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/oas-3-1-dialect",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://spec.openapis.org/oas/3.1/vocab/base": false
}
}
},
"defaultDialect": "https://example.com/oas-3-1-dialect",
"result": {
"nonOfficialMetaschemas": {
"properties": {
"pet": {
"$ref": "https://www.sourcemeta.com/pet"
}
},
"$defs": {
"https://www.sourcemeta.com/pet": {
"$schema": "https://example.com/oas-3-1-dialect",
"$id": "https://www.sourcemeta.com/pet",
"type": "object",
"externalDocs": {
"url": "https://docs.sourcemeta.com/pet.html"
}
},
"https://example.com/oas-3-1-dialect": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/oas-3-1-dialect",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://spec.openapis.org/oas/3.1/vocab/base": false
}
}
}
},
"references": {
"properties": {
"pet": {
"$ref": "https://www.sourcemeta.com/pet"
}
},
"$defs": {
"https://www.sourcemeta.com/pet": {
"$schema": "https://example.com/oas-3-1-dialect",
"$id": "https://www.sourcemeta.com/pet",
"type": "object",
"externalDocs": {
"url": "https://docs.sourcemeta.com/pet.html"
}
}
}
}
},
"inserted": {
"nonOfficialMetaschemas": [
{
"from": "https://example.com/oas-3-1-dialect",
"at": "/$defs/https:~1~1example.com~1oas-3-1-dialect"
},
{
"from": "https://www.sourcemeta.com/pet",
"at": "/$defs/https:~1~1www.sourcemeta.com~1pet"
}
],
"references": [
{
"from": "https://www.sourcemeta.com/pet",
"at": "/$defs/https:~1~1www.sourcemeta.com~1pet"
}
]
}
}
Loading
Loading