[FEATURE] Implementation of $ref JSON Schema Keyword #20

Merged
HideyoshiNakazone merged 6 commits from feature/ref-type-parser into main 2025-06-20 01:09:11 +00:00
3 changed files with 61 additions and 22 deletions
Showing only changes of commit 188cd28586 - Show all commits

View File

@@ -4,7 +4,7 @@ from jambo.types.type_parser_options import TypeParserOptions
from typing_extensions import Any, ForwardRef, TypeVar, Union, Unpack from typing_extensions import Any, ForwardRef, TypeVar, Union, Unpack
RefType = TypeVar("RefType", bound=Union[int, str]) RefType = TypeVar("RefType", bound=Union[type, ForwardRef])
class RefTypeParser(GenericTypeParser): class RefTypeParser(GenericTypeParser):
@@ -30,16 +30,13 @@ class RefTypeParser(GenericTypeParser):
"Look into $defs and # for recursive references." "Look into $defs and # for recursive references."
) )
ref_type = None
mapped_properties = {}
if properties["$ref"] == "#": if properties["$ref"] == "#":
if "title" not in context: if "title" not in context:
raise ValueError( raise ValueError(
"RefTypeParser: Missing title in properties for $ref #" "RefTypeParser: Missing title in properties for $ref #"
) )
ref_type = ForwardRef(context["title"]) return ForwardRef(context["title"]), {}
elif properties["$ref"].startswith("#/$defs/"): elif properties["$ref"].startswith("#/$defs/"):
target_name = None target_name = None
@@ -56,17 +53,8 @@ class RefTypeParser(GenericTypeParser):
if target_name is None or target_property is None: if target_name is None or target_property is None:
raise ValueError(f"RefTypeParser: Invalid $ref {properties['$ref']}") raise ValueError(f"RefTypeParser: Invalid $ref {properties['$ref']}")
ref_type, mapped_properties = GenericTypeParser.type_from_properties( return GenericTypeParser.type_from_properties(
target_name, target_property, **kwargs target_name, target_property, **kwargs
) )
else: raise ValueError(f"RefTypeParser: Unsupported $ref {properties['$ref']}")
raise ValueError(
"RefTypeParser: Invalid $ref format. "
"Only local references are supported."
)
if not required:
mapped_properties["default"] = None
return ref_type, mapped_properties

View File

@@ -1,7 +1,9 @@
from typing_extensions import Any, NotRequired, TypedDict from jambo.types.json_schema_type import JSONSchema
from typing_extensions import NotRequired, TypedDict
class TypeParserOptions(TypedDict): class TypeParserOptions(TypedDict):
required: bool required: bool
context: dict[str, Any] context: JSONSchema
ref_cache: NotRequired[dict[str, type]] ref_cache: NotRequired[dict[str, type]]

View File

@@ -1,10 +1,12 @@
from jambo.parser import RefTypeParser from jambo.parser import ObjectTypeParser, RefTypeParser
from typing_extensions import ForwardRef, get_type_hints
from unittest import TestCase from unittest import TestCase
class TestRefTypeParser(TestCase): class TestRefTypeParser(TestCase):
def test_ref_type_parser_local_ref(self): def test_ref_type_parser_with_def(self):
properties = { properties = {
"title": "person", "title": "person",
"$ref": "#/$defs/person", "$ref": "#/$defs/person",
@@ -20,8 +22,8 @@ class TestRefTypeParser(TestCase):
} }
type_parsing, type_validator = RefTypeParser().from_properties( type_parsing, type_validator = RefTypeParser().from_properties(
properties=properties, "person",
name="placeholder", properties,
context=properties, context=properties,
required=True, required=True,
) )
@@ -32,3 +34,50 @@ class TestRefTypeParser(TestCase):
self.assertEqual(obj.name, "John") self.assertEqual(obj.name, "John")
self.assertEqual(obj.age, 30) self.assertEqual(obj.age, 30)
def test_ref_type_parser_with_forward_ref(self):
properties = {
"title": "person",
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"emergency_contact": {
"$ref": "#",
},
},
}
type_parsing, type_validator = ObjectTypeParser().from_properties(
"person",
properties,
context=properties,
required=True,
)
type_parsing.update_forward_refs(person=type_parsing)
self.assertIsInstance(type_parsing, type)
type_hints = get_type_hints(type_parsing, globals(), locals())
self.assertIsInstance(type_hints["emergency_contact"], ForwardRef)
"""
This is a example of how to resolve ForwardRef in a dynamic model:
```python
from typing import get_type_hints
# Make sure your dynamic model has a name
model = type_parsing
model.update_forward_refs(person=model) # 👈 resolve the ForwardRef("person")
# Inject into globals manually
globalns = globals().copy()
globalns['person'] = model
# Now you can get the resolved hints
type_hints = get_type_hints(model, globalns=globalns)
```
Use `TypeParserOptions.ref_cache` option to cache and resolve ForwardRefs
inside the ObjectTypeParser.to_model method.
"""