jsonref icon indicating copy to clipboard operation
jsonref copied to clipboard

A reference to the same file inside a referenced file works when a loaded relatively but not by urn:

Open ghost opened this issue 11 months ago • 2 comments

I'm very puzzled here - thanks for any info anyone can add!

libs.json:

{
  "$id": "urn:library",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$defs": {
    "address": {
      "title": "An Address",
      "description": "A Description",
      "type": "object",
      "properties": {
        "address": {
          "type": "string"
        },
        "country": {
          "$ref": "#/$defs/country"
        }
      }
    },
    "country": {
      "type": "string"
    }
  }
}

start.json:

{
  "$id": "urn:start",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
    "properties": {
      "home_address": {
        "title": "Home Address",
        "description": "Where the person lives",
        "$ref": "libs.json#/$defs/address"
      }
    }
}

test.py:

import os
import jsonref
import pathlib
import json

base_uri = pathlib.Path(
    os.path.dirname(__file__) + "/"
).as_uri() + "/"

with open("libs.json") as fp:
    lib = json.load(fp)

def _jsonref_loader(uri):
    global lib
    print("LOADING {}".format(uri))
    if uri == "urn:library":
        return lib
    return jsonref.jsonloader(uri)

with open("start.json") as fp:
    start = json.load(fp)

print("TEST 1 - WORKS GREAT!")
print(jsonref.JsonRef.replace_refs(start, base_uri=base_uri, loader=_jsonref_loader))

print("TEST 2 - FAILS!")
start["properties"]["home_address"]["$ref"] = "urn:library#/$defs/address"
print(jsonref.JsonRef.replace_refs(start, base_uri=base_uri, loader=_jsonref_loader))

As comments say, just changing the ref from start.json to lib.json to a urn breaks it. You get:

TEST 1 - WORKS GREAT!
LOADING file:///.../libs.json
{'$id': 'urn:start', '$schema': 'https://json-schema.org/draft/2020-12/schema', 'properties': {'home_address': {'title': 'An Address', 'description': 'A Description', 'type': 'object', 'properties': {'address': {'type': 'string'}, 'country': {'type': 'string'}}}}}
TEST 2 - FAILS!
LOADING urn:library
LOADING 
Traceback (most recent call last):
...
ValueError: unknown url type: ''

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
...    
jsonref.JsonRefError: Error while resolving `#/$defs/country`: ValueError: unknown url type: ''

_jsonref_loader is called with an empty uri so "unknown url type" is fair enought, but why is it trying to load that in the first place?

(I also have a version of the test script that uses the refrencing library and loads libs.json into a registry, but that breaks the same way)

ghost avatar Dec 18 '24 16:12 ghost

This appears to be due to the use of urllib.urlparse.urljoin to prepend the base_uri onto the reference. Unfortunately it doesn't support urn: prefixes and worse silently drops the base_uri if it doesn't match the "known" format. So for example:

file:///directory + #/$defs/country -> file:///directory/#/$defs/country
http:///example.com + #/$defs/country -> http:///example.com/#/$defs/country
urn:example + #/$defs/country -> #/$defs/country

which then causes the error in the loader.

If you change this method in jsonref:

    @property
    def full_uri(self):
        return urlparse.urljoin(self.base_uri, self.__reference__["$ref"])

to:

    @property
    def full_uri(self):
        if self.base_uri.startswith("urn:"):
             return f"{self.base_uri}{self.__reference__['$ref']}"
        else:
             return urlparse.urljoin(self.base_uri, self.__reference__["$ref"])

then the test case above works, but this doesn't cover the case where a urn: is already in the reference, so needs to be more like:

    @property
    def full_uri(self):
        if self.base_uri.startswith("urn:"):
            if self.__reference__['$ref'].startswith("urn:"):
                 return self.__reference__['$ref']
            else:
                 return f"{self.base_uri}{self.__reference__['$ref']}"
        else:
            return urlparse.urljoin(self.base_uri, self.__reference__["$ref"])

This hasn't been thoroughly tested though.

radix0000 avatar Dec 19 '24 16:12 radix0000

I did a PR for this: https://github.com/gazpachoking/jsonref/pull/78

jarofgreen avatar Apr 29 '25 15:04 jarofgreen