Describe the bug
flatten_entity_reference inlines the body of ucp.json#/$defs/entity into every schema that composes it — capability.json, payment_handler.json and service.json. The body is deep-copied verbatim:
e_copy = copy.deepcopy(entity_definition)
e_copy.pop("title", None)
e_copy.pop("description", None)
filtered_all_of.append(e_copy)
It contains a reference written relative to its own document:
"version": { "$ref": "#/$defs/version" }
In ucp.json that is correct — ucp.json defines $defs/version. After the copy the same text resolves against the destination document, which defines no such $def, so the reference points nowhere.
The preprocessed output contains 24 of these: 4 in capability.json, 4 in payment_handler.json, 16 in service.json.
To Reproduce
Verified against ucp 5a5d9e1 and python-sdk a0d8308.
git clone --depth 1 https://github.com/Universal-Commerce-Protocol/ucp
git clone --depth 1 https://github.com/Universal-Commerce-Protocol/python-sdk
cd python-sdk && python3 preprocess_schemas.py ../ucp/source/schemas
pip install jsonschema
import json, glob
from jsonschema import Draft202012Validator
from referencing import Registry, Resource
root = "../ucp/source/schemas"
reg = Registry()
for p in glob.glob(root + "/**/*.json", recursive=True):
d = json.load(open(p))
if "$id" in d:
reg = reg.with_resource(d["$id"], Resource.from_contents(d))
for t in ["capability.json", "service.json", "payment_handler.json"]:
v = Draft202012Validator(
{"$ref": f"https://ucp.dev/schemas/{t}#/$defs/base"}, registry=reg
)
list(v.iter_errors({"version": "2026-04-08"}))
Each raises:
referencing.exceptions.PointerToNowhere: '/$defs/version' does not exist
Expected behavior
A fragment-only reference is relative to the document it was written in. When the body moves to another document, the reference should be rebased onto its origin:
"$ref": "#/$defs/version" -> "$ref": "ucp.json#/$defs/version"
Copying the referenced $def into each destination alongside the body would also work, at the cost of duplicating it into three files.
Additional context
This looks like the third site of a pattern, and the complementary half of a fix already made. The preprocessor relocates schema fragments between documents in several places, and each site handles references independently:
| Site |
Issue |
Status |
Variant $ref rewriting |
#63 |
fixed in 3e804cd |
Variant oneOf/anyOf branches |
#34 / PR #35 |
open |
Entity inlining (flatten_entity_reference) |
this issue |
— |
The 3e804cd fix handles a reference that has a file part which must change, and deliberately skips fragment-only references:
ref_file, separator, fragment = ref.partition("#")
if not ref_file:
continue
That is correct in its own context. This issue is the complementary case: a reference with no file part that now needs one. PR #35 keeps the same "#" not in ref guard, so it does not cover this either. A shared rebasing helper used by every relocation site would close all three and prevent a fourth.
Why it may not have surfaced. jsonschema resolves references lazily, so nothing fails until a value is validated against a schema that reaches the broken pointer. Validators that compile eagerly reject these documents on load, before any data is involved — which is how this was found, while building a Go SDK from the same preprocessed schemas. Code generators that never resolve the pointer do not notice at all.
The spec's source schemas are correct; the defect is introduced by preprocessing.
Describe the bug
flatten_entity_referenceinlines the body ofucp.json#/$defs/entityinto every schema that composes it —capability.json,payment_handler.jsonandservice.json. The body is deep-copied verbatim:It contains a reference written relative to its own document:
In
ucp.jsonthat is correct —ucp.jsondefines$defs/version. After the copy the same text resolves against the destination document, which defines no such$def, so the reference points nowhere.The preprocessed output contains 24 of these: 4 in
capability.json, 4 inpayment_handler.json, 16 inservice.json.To Reproduce
Verified against ucp
5a5d9e1and python-sdka0d8308.Each raises:
Expected behavior
A fragment-only reference is relative to the document it was written in. When the body moves to another document, the reference should be rebased onto its origin:
Copying the referenced
$definto each destination alongside the body would also work, at the cost of duplicating it into three files.Additional context
This looks like the third site of a pattern, and the complementary half of a fix already made. The preprocessor relocates schema fragments between documents in several places, and each site handles references independently:
$refrewriting3e804cdoneOf/anyOfbranchesflatten_entity_reference)The
3e804cdfix handles a reference that has a file part which must change, and deliberately skips fragment-only references:That is correct in its own context. This issue is the complementary case: a reference with no file part that now needs one. PR #35 keeps the same
"#" not in refguard, so it does not cover this either. A shared rebasing helper used by every relocation site would close all three and prevent a fourth.Why it may not have surfaced.
jsonschemaresolves references lazily, so nothing fails until a value is validated against a schema that reaches the broken pointer. Validators that compile eagerly reject these documents on load, before any data is involved — which is how this was found, while building a Go SDK from the same preprocessed schemas. Code generators that never resolve the pointer do not notice at all.The spec's source schemas are correct; the defect is introduced by preprocessing.