python-jsonschema-objects icon indicating copy to clipboard operation
python-jsonschema-objects copied to clipboard

Chained references are not resolved correctly

Open st31ny opened this issue 6 months ago • 0 comments

Describe the bug I only recently tried to switch to version 0.5.5 (from 0.4.6) and noticed that apparently relative $refs in a referenced document are not resolved correctly anymore with the new referencing lib.

So, when I reference a definition d1 within a a schema schema-a from a schema-b, and d1 references another definition d2 within the very same schema-a using a local JSON pointer, the resolution of d2 fails as it searches in schema-b. With version 0.4.6 this exact use case worked using the legacy RefResolver.

Example Schema and code

import python_jsonschema_objects as pjs
import referencing

SCHEMA_A = {
        '$id': 'schema-a',
        '$schema': 'http://json-schema.org/draft-04/schema#',
        'title': "Schema A",
        'definitions': {
                'myint': {
                        'type': 'integer',
                        'minimum': 42
                    },
                'myintarray': {
                        'type': 'array',
                        'items': {
                                '$ref': '#/definitions/myint', # using 'schema-a#/definitions/myint' would work
                            },
                    },
                'myintref': {
                        '$ref': '#/definitions/myint', # using 'schema-a#/definitions/myint' would work
                    },
            },
        'type': 'object',
        'properties': {
                'theint': {
                        '$ref': '#/definitions/myint', # using 'schema-a#/definitions/myint' would work
                    },
            },
    }

SCHEMA_B = {
        '$id': 'schema-b',
        '$schema': 'http://json-schema.org/draft-04/schema#',
        'title': "Schema B",
        'type': 'object',
        'definitions': {
            'myintref': {
                    '$ref': 'schema-a#/definitions/myintref',
                },
        },
        'properties': {
                # all three properties cause the failure
                'obja': {
                        '$ref': 'schema-a',
                    },
                'theintarray': {
                        '$ref': 'schema-a#/definitions/myintarray',
                    },
                'theintref': {
                        '$ref': '#/definitions/myintref',
                    },
            },
    }

registry = referencing.Registry().with_resources([
        ('schema-a', referencing.Resource.from_contents(SCHEMA_A)),
        ('schema-b', referencing.Resource.from_contents(SCHEMA_B)),
    ])

# works fine
builder_a = pjs.ObjectBuilder(SCHEMA_A, registry=registry)
namespace_a = builder_a.build_classes(named_only=False)

# fails
builder_b = pjs.ObjectBuilder(SCHEMA_B, registry=registry)
namespace_b = builder_b.build_classes(named_only=False) # referencing.exceptions.PointerToNowhere: '/definitions/myint' does not exist within SCHEMA_B

b = namespace_b.SchemaB()
b.obja = {'theint': 42}
b.theintarray = [42, 43, 44]
b.theintref = 42
print(b.for_json())

The exception throws by build_classes() actually references SCHEMA_B, although the given pointer /definitions/myint is only ever used in SCHEMA_A.

Expected behavior I would expect that local JSON pointers are resolved within the current document not within any referencing document.

Workaround Make all $refs absolute.

Fix I believe that there needs to be a new ClassBuilder instance with an adapted resolver when recursing in ClassBuilder.resolve_type(). My quick and dirty patch seems to work and the above code prints {'obja': {'theint': 42}, 'theintarray': [42, 43, 44], 'theintref': 42}:

--- a/python_jsonschema_objects/classbuilder.py
+++ b/python_jsonschema_objects/classbuilder.py
@@ -492,7 +492,8 @@
                 )
             )
             resolved = self.resolver.lookup(ref)
-            self.resolved[uri] = self.construct(uri, resolved.contents, (ProtocolBase,))
+            sub_cb = ClassBuilder(referencing._core.Resolver(base_uri=uri.split('#')[0], registry=self.resolver._registry))
+            self.resolved[uri] = sub_cb.construct(uri, resolved.contents, (ProtocolBase,))
             return self.resolved[uri]
 
     def construct(self, uri, *args, **kw):

st31ny avatar Aug 22 '24 07:08 st31ny