11from typing import Any
22
3+ from fastmcp .utilities .json_schema import dereference_refs
4+
35from quickapp .config .tools .base import (
46 ConfigurableSchemaArray ,
57 ConfigurableSchemaObject ,
1315
1416
1517class JsonSchemaConverter :
16- """Utility class for converting JSON schema dictionaries to ConfigurableSchema objects."""
18+ """Utility class for converting JSON schema dictionaries to ConfigurableSchema objects.
19+
20+ Handles $ref resolution internally via fastmcp.utilities.json_schema.dereference_refs().
21+ """
1722
1823 @staticmethod
19- def _normalize_type (type_field : str | list [str ] | None ) -> tuple [ str | None , bool ] :
24+ def _normalize_type (type_field : str | list [str ] | None ) -> str | None :
2025 """
2126 Normalize the 'type' field which can be a str or a list (e.g. ['string', 'null']).
22- Returns (primary_type_or_None, is_nullable) .
27+ Returns the primary non-null type, or None if no non-null type is present .
2328 """
2429 if isinstance (type_field , list ):
25- is_nullable = 'null' in type_field
2630 non_null = [t for t in type_field if t != 'null' ]
27- primary = non_null [0 ] if non_null else None
28- return primary , is_nullable
29- return (type_field , False ) if type_field is not None else (None , False )
30-
31- @staticmethod
32- def _resolve_ref (ref : str , root_schema : dict [str , Any ]) -> dict [str , Any ]:
33- """
34- Resolve an internal JSON Pointer ref like '#/$defs/Name' or '#/definitions/Name'
35- by traversing the root_schema. Returns the referenced dictionary.
36- """
37- if not ref .startswith ('#/' ):
38- raise ValueError (f"Only local refs are supported in this converter: { ref !r} " )
39- pointer = ref [2 :].split ('/' ) # remove leading '#/'
40- node : Any = root_schema
41- for token in pointer :
42- # JSON Pointer uses ~1 for '/' and ~0 for '~' (not expected here but handle minimally)
43- token = token .replace ('~1' , '/' ).replace ('~0' , '~' )
44- if isinstance (node , dict ) and token in node :
45- node = node [token ]
46- else :
47- raise KeyError (f"Could not resolve ref { ref !r} at token { token !r} " )
48- if not isinstance (node , dict ):
49- raise ValueError (f"Referenced ref { ref !r} does not point to an object" )
50- return node
31+ return non_null [0 ] if non_null else None
32+ return type_field
5133
5234 @staticmethod
53- def _pick_variant_from_anyof (
54- variants : list [dict [str , Any ]], root_schema : dict [str , Any ]
55- ) -> dict [str , Any ]:
35+ def _pick_variant_from_anyof (variants : list [dict [str , Any ]]) -> dict [str , Any ]:
5636 """
5737 Pick the most relevant variant from anyOf/oneOf:
5838 - prefer a non-null typed variant
59- - resolve $ref variants
6039 - fallback to the first variant
6140 """
6241 for v in variants :
6342 if v .get ("type" ) == "null" :
6443 continue
65- # if it's a ref, try to resolve and return resolved (prefer resolved non-null)
66- if "$ref" in v :
67- try :
68- resolved = JsonSchemaConverter ._resolve_ref (v ["$ref" ], root_schema )
69- # if resolved is not null, return it
70- if resolved .get ("type" ) != "null" :
71- return {** resolved , ** {k : vv for k , vv in v .items () if k != "$ref" }}
72- except Exception :
73- # fall through to treat v as-is
74- return v
75- # otherwise return the first non-null typed variant
76- if v .get ("type" ) and v .get ("type" ) != "null" :
44+ if v .get ("type" ):
7745 return v
78- # fallback: return first variant (could be null)
7946 return variants [0 ]
8047
8148 @staticmethod
8249 def _build_schema_from_definition (
83- def_dict : dict [str , Any ], name : str | None = None , root_schema : dict [ str , Any ] | None = None
50+ def_dict : dict [str , Any ], name : str | None = None
8451 ) -> _ConfigurableSchema :
8552 """
8653 Build and return a ConfigurableSchema* instance from a single property/items definition.
8754 This centralizes the handling for simple types, objects and arrays (including nested arrays).
8855 """
89- # If there's a $ref, resolve it against root_schema (if provided).
9056 if "$ref" in def_dict :
91- if root_schema is None :
92- raise ValueError ("Root schema is required to resolve $ref" )
93- resolved = JsonSchemaConverter ._resolve_ref (def_dict ["$ref" ], root_schema )
94- # Merge resolved with local overrides (local keys take precedence)
95- merged = {** resolved , ** {k : v for k , v in def_dict .items () if k != "$ref" }}
96- def_dict = merged
57+ raise ValueError (
58+ f"Unresolved $ref '{ def_dict ['$ref' ]} ' in property { name !r} . "
59+ "Schema must be dereferenced before conversion."
60+ )
9761
9862 # Handle anyOf / oneOf by selecting a representative variant
9963 if "anyOf" in def_dict or "oneOf" in def_dict :
10064 variants = def_dict .get ("anyOf" ) or def_dict .get ("oneOf" )
10165 if not isinstance (variants , list ) or not variants :
10266 raise ValueError ("anyOf/oneOf must be a non-empty list" )
103- picked = JsonSchemaConverter ._pick_variant_from_anyof (variants , root_schema or {} )
67+ picked = JsonSchemaConverter ._pick_variant_from_anyof (variants )
10468 # Merge picked with def_dict to preserve top-level default/description etc.
10569 merged = {
10670 ** picked ,
@@ -109,7 +73,7 @@ def _build_schema_from_definition(
10973 def_dict = merged
11074
11175 raw_type = def_dict .get ("type" )
112- prop_type , is_nullable = JsonSchemaConverter ._normalize_type (raw_type )
76+ prop_type = JsonSchemaConverter ._normalize_type (raw_type )
11377
11478 description = def_dict .get ("description" )
11579 default_value = def_dict .get ("default" )
@@ -123,19 +87,14 @@ def _build_schema_from_definition(
12387 prop_type = "string"
12488
12589 if prop_type in ["string" , "number" , "integer" , "boolean" ]:
126- # ConfigurableSchemaSimpleType doesn't track nullable explicitly;
127- # default_value may already be None when schema allowed null.
12890 return ConfigurableSchemaSimpleType (
12991 type = getattr (JsonTypeEnum , prop_type ),
13092 description = description ,
13193 enum = enum_values ,
13294 default = default_value ,
13395 )
13496 elif prop_type == "object" :
135- # For nested objects, pass the original root_schema so nested refs can be resolved
136- nested_properties = JsonSchemaConverter .convert_schema_to_properties (
137- def_dict , root_schema = root_schema or def_dict
138- )
97+ nested_properties = JsonSchemaConverter .convert_schema_to_properties (def_dict )
13998 return ConfigurableSchemaObject (
14099 type = JsonTypeEnum .object ,
141100 properties = nested_properties ,
@@ -145,10 +104,7 @@ def _build_schema_from_definition(
145104 )
146105 elif prop_type == "array" :
147106 items_def = def_dict .get ("items" , {})
148- # recursively build items schema (handles array of arrays, objects, simple types)
149- items_schema = JsonSchemaConverter ._build_schema_from_definition (
150- items_def , name = name , root_schema = root_schema
151- )
107+ items_schema = JsonSchemaConverter ._build_schema_from_definition (items_def , name = name )
152108 return ConfigurableSchemaArray (
153109 type = JsonTypeEnum .array ,
154110 items = items_schema ,
@@ -160,46 +116,38 @@ def _build_schema_from_definition(
160116
161117 @staticmethod
162118 def convert_schema_to_properties (
163- schema_dict : dict [str , Any ], root_schema : dict [ str , Any ] | None = None
119+ schema_dict : dict [str , Any ],
164120 ) -> dict [str , _ConfigurableSchema ]:
165121 """
166122 Convert a JSON schema dictionary to ConfigurableSchema* properties.
167123
124+ Resolves any $ref pointers via dereference_refs() before conversion.
125+
168126 Args:
169127 schema_dict: The JSON schema dictionary containing properties definition
170- root_schema: optional root schema used to resolve internal $ref pointers.
171- If not provided, schema_dict is used as root.
172128
173129 Returns:
174130 Dictionary of converted properties compatible with OpenAiToolFunctionParameters
175131 """
176- properties : dict [str , _ConfigurableSchema ] = {}
177-
178- # Preserve the original root for resolving internal refs
179- original_root = root_schema or schema_dict
180-
181- # Resolve top-level $ref if present
182- if "$ref" in schema_dict :
183- resolved = JsonSchemaConverter ._resolve_ref (schema_dict ["$ref" ], original_root )
184- # Merge resolved with local overrides (local keys take precedence)
185- schema_dict = {** resolved , ** {k : v for k , v in schema_dict .items () if k != "$ref" }}
132+ schema_dict = dereference_refs (schema_dict )
186133
187134 # Handle top-level anyOf/oneOf
188135 if "anyOf" in schema_dict or "oneOf" in schema_dict :
189136 variants = schema_dict .get ("anyOf" ) or schema_dict .get ("oneOf" )
190137 if not isinstance (variants , list ) or not variants :
191138 raise ValueError ("anyOf/oneOf must be a non-empty list" )
192- picked = JsonSchemaConverter ._pick_variant_from_anyof (variants , original_root )
139+ picked = JsonSchemaConverter ._pick_variant_from_anyof (variants )
193140 schema_dict = {
194141 ** picked ,
195142 ** {k : v for k , v in schema_dict .items () if k not in ("anyOf" , "oneOf" )},
196143 }
197144
198145 schema_properties = schema_dict .get ("properties" , {})
199146
147+ properties : dict [str , _ConfigurableSchema ] = {}
200148 for prop_name , prop_def in schema_properties .items ():
201149 properties [prop_name ] = JsonSchemaConverter ._build_schema_from_definition (
202- prop_def , name = prop_name , root_schema = original_root
150+ prop_def , name = prop_name
203151 )
204152
205153 return properties
0 commit comments