Skip to content

[Bug]: Inlined entity $refs are not rebased onto their origin document #72

Description

@chaz8081

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions