@@ -1731,11 +1731,33 @@ def _sanitize_python_identifier(name: str) -> str:
17311731def _json_schema_to_pydantic (
17321732 name : str ,
17331733 schema : Optional [Dict [str , Any ]],
1734+ root_schema : Optional [Dict [str , Any ]] = None ,
1735+ active_refs : Optional [Set [str ]] = None ,
1736+ ref_type_cache : Optional [Dict [Tuple [str , Tuple [str , ...]], type ]] = None ,
1737+ ref_dependency_cache : Optional [Dict [str , Set [str ]]] = None ,
17341738) -> Optional [Type [BaseModel ]]:
1735- """将 JSON Schema 转换为 Pydantic 模型"""
1739+ """将 JSON Schema 转换为 Pydantic 模型
1740+
1741+ Args:
1742+ name: 模型名称
1743+ schema: 待转换的 JSON Schema 片段
1744+ root_schema: 顶层 schema, 用于解析深层 ``$ref`` 指向的 ``$defs``
1745+ active_refs: 当前递归栈中的 ``$ref``,用于防止自引用无限展开
1746+ ref_type_cache: 已构建的 ``$ref`` 类型缓存, 避免共享引用重复展开
1747+ ref_dependency_cache: ``$ref`` 可达引用缓存, 避免重复遍历共享 DAG
1748+ """
17361749 if not schema or not isinstance (schema , dict ):
17371750 return None
17381751
1752+ if root_schema is None :
1753+ root_schema = schema
1754+ if active_refs is None :
1755+ active_refs = set ()
1756+ if ref_type_cache is None :
1757+ ref_type_cache = {}
1758+ if ref_dependency_cache is None :
1759+ ref_dependency_cache = {}
1760+
17391761 properties = schema .get ("properties" , {})
17401762 if not properties :
17411763 return None
@@ -1768,7 +1790,13 @@ def _json_schema_to_pydantic(
17681790 needs_populate_by_name = True
17691791
17701792 # 映射类型
1771- field_type = _json_type_to_python (field_schema )
1793+ field_type = _json_type_to_python (
1794+ field_schema ,
1795+ root_schema ,
1796+ active_refs ,
1797+ ref_type_cache ,
1798+ ref_dependency_cache ,
1799+ )
17721800 description = field_schema .get ("description" , "" )
17731801 default = field_schema .get ("default" )
17741802 aliases = field_schema .get ("x-aliases" )
@@ -1812,38 +1840,278 @@ def _json_schema_to_pydantic(
18121840 )
18131841
18141842
1815- def _json_type_to_python (field_schema : Dict [str , Any ]) -> type :
1816- """映射 JSON Schema 类型到 Python 类型"""
1817- schema_type = field_schema .get ("type" , "string" )
1843+ def _find_schema_ref (field_schema : Dict [str , Any ]) -> Optional [str ]:
1844+ """查找当前 schema 片段中用于防环的第一个 $ref"""
1845+ ref = field_schema .get ("$ref" )
1846+ if isinstance (ref , str ):
1847+ return ref
18181848
1819- # 处理嵌套对象和数组
1820- if schema_type == "object" :
1821- # 如果有 properties,递归创建嵌套模型
1822- properties = field_schema .get ("properties" )
1823- if properties :
1824- nested_model = _json_schema_to_pydantic (
1825- "NestedObject" , field_schema
1826- )
1827- return nested_model if nested_model else dict
1828- return dict
1849+ for union_key in ("anyOf" , "oneOf" ):
1850+ options = field_schema .get (union_key )
1851+ if not isinstance (options , list ):
1852+ continue
1853+ for option in options :
1854+ if not isinstance (option , dict ):
1855+ continue
1856+ if option .get ("type" ) == "null" :
1857+ continue
1858+ ref = _find_schema_ref (option )
1859+ return ref
1860+
1861+ options = field_schema .get ("allOf" )
1862+ if isinstance (options , list ):
1863+ for option in options :
1864+ if not isinstance (option , dict ):
1865+ continue
1866+ ref = _find_schema_ref (option )
1867+ if ref :
1868+ return ref
1869+
1870+ return None
1871+
1872+
1873+ def _cacheable_schema_ref (field_schema : Dict [str , Any ]) -> Optional [str ]:
1874+ """查找可安全复用类型缓存的单一 $ref"""
1875+ ref = field_schema .get ("$ref" )
1876+ if isinstance (ref , str ):
1877+ return ref if set (field_schema ) <= {"$ref" } else None
1878+
1879+ for union_key in ("anyOf" , "oneOf" ):
1880+ options = field_schema .get (union_key )
1881+ if not isinstance (options , list ):
1882+ continue
1883+
1884+ refs : List [str ] = []
1885+ for option in options :
1886+ if not isinstance (option , dict ):
1887+ continue
1888+ if option .get ("type" ) == "null" :
1889+ continue
1890+ option_ref = _cacheable_schema_ref (option )
1891+ if not option_ref :
1892+ return None
1893+ refs .append (option_ref )
1894+
1895+ if len (refs ) == 1 :
1896+ return refs [0 ]
1897+
1898+ return None
1899+
1900+
1901+ def _collect_direct_refs (schema : Any ) -> Set [str ]:
1902+ """收集 schema 子树中直接写出的 $ref"""
1903+ refs : Set [str ] = set ()
1904+ if not isinstance (schema , dict ):
1905+ return refs
1906+
1907+ ref = schema .get ("$ref" )
1908+ if isinstance (ref , str ):
1909+ refs .add (ref )
1910+
1911+ for key , value in schema .items ():
1912+ if key == "$ref" :
1913+ continue
1914+ if isinstance (value , dict ):
1915+ refs .update (_collect_direct_refs (value ))
1916+ elif isinstance (value , list ):
1917+ for item in value :
1918+ refs .update (_collect_direct_refs (item ))
1919+
1920+ return refs
1921+
1922+
1923+ def _reachable_refs_for_ref (
1924+ ref : str ,
1925+ root_schema : Dict [str , Any ],
1926+ ref_dependency_cache : Optional [Dict [str , Set [str ]]] = None ,
1927+ ) -> Set [str ]:
1928+ """收集某个 $ref 目标 schema 可达的引用集合"""
1929+ if ref_dependency_cache is None :
1930+ ref_dependency_cache = {}
1931+ if ref in ref_dependency_cache :
1932+ return set (ref_dependency_cache [ref ])
1933+
1934+ visited : Set [str ] = set ()
1935+ direct_ref_map : Dict [str , Set [str ]] = {}
1936+ stack = [ref ]
1937+ while stack :
1938+ current_ref = stack .pop ()
1939+ if current_ref in visited :
1940+ continue
1941+ visited .add (current_ref )
1942+
1943+ cached_refs = ref_dependency_cache .get (current_ref )
1944+ if cached_refs is not None :
1945+ continue
1946+
1947+ ref_schema = _resolve_ref_schema (current_ref , root_schema )
1948+ if not ref_schema :
1949+ continue
18291950
1830- if schema_type == "array" :
1831- items_schema = field_schema .get ("items" , {})
1832- if isinstance (items_schema , dict ):
1833- item_type = _json_type_to_python (items_schema )
1834- from typing import List as TypingList
1835-
1836- return TypingList [item_type ] # type: ignore
1837- return list
1838-
1839- # 基本类型映射
1840- mapping = {
1841- "string" : str ,
1842- "integer" : int ,
1843- "number" : float ,
1844- "boolean" : bool ,
1951+ direct_refs = _collect_direct_refs (ref_schema )
1952+ direct_ref_map [current_ref ] = direct_refs
1953+ for direct_ref in direct_refs :
1954+ if (
1955+ direct_ref not in visited
1956+ and direct_ref not in ref_dependency_cache
1957+ ):
1958+ stack .append (direct_ref )
1959+
1960+ closures = {
1961+ current_ref : set (direct_refs )
1962+ for current_ref , direct_refs in direct_ref_map .items ()
18451963 }
1846- return mapping .get (schema_type , str )
1964+ changed = True
1965+ while changed :
1966+ changed = False
1967+ for current_ref , direct_refs in direct_ref_map .items ():
1968+ expanded_refs = set (direct_refs )
1969+ for direct_ref in direct_refs :
1970+ if direct_ref in closures :
1971+ expanded_refs .update (closures [direct_ref ])
1972+ else :
1973+ expanded_refs .update (
1974+ ref_dependency_cache .get (direct_ref , set ())
1975+ )
1976+ if expanded_refs != closures [current_ref ]:
1977+ closures [current_ref ] = expanded_refs
1978+ changed = True
1979+
1980+ ref_dependency_cache .update (closures )
1981+ return set (ref_dependency_cache .get (ref , set ()))
1982+
1983+
1984+ def _schema_cache_key (
1985+ ref : Optional [str ],
1986+ active_refs : Set [str ],
1987+ root_schema : Optional [Dict [str , Any ]],
1988+ ref_dependency_cache : Optional [Dict [str , Set [str ]]],
1989+ ) -> Optional [Tuple [str , Tuple [str , ...]]]:
1990+ """根据 ref 和当前递归上下文生成类型缓存键"""
1991+ if not ref :
1992+ return None
1993+ relevant_refs = active_refs
1994+ if root_schema is not None :
1995+ reachable_refs = _reachable_refs_for_ref (
1996+ ref , root_schema , ref_dependency_cache
1997+ )
1998+ relevant_refs = active_refs .intersection (reachable_refs )
1999+ return ref , tuple (sorted (relevant_refs ))
2000+
2001+
2002+ def _json_type_to_python (
2003+ field_schema : Dict [str , Any ],
2004+ root_schema : Optional [Dict [str , Any ]] = None ,
2005+ active_refs : Optional [Set [str ]] = None ,
2006+ ref_type_cache : Optional [Dict [Tuple [str , Tuple [str , ...]], type ]] = None ,
2007+ ref_dependency_cache : Optional [Dict [str , Set [str ]]] = None ,
2008+ ) -> type :
2009+ """映射 JSON Schema 类型到 Python 类型
2010+
2011+ Args:
2012+ field_schema: 字段 schema, 可能是裸类型或 anyOf/oneOf/$ref 包装
2013+ root_schema: 顶层 schema, 用于解析 ``$ref`` 指向的 ``$defs``
2014+ active_refs: 当前递归栈中的 ``$ref``,用于防止自引用无限展开
2015+ ref_type_cache: 已构建的 ``$ref`` 类型缓存, 避免共享引用重复展开
2016+ ref_dependency_cache: ``$ref`` 可达引用缓存, 避免重复遍历共享 DAG
2017+ """
2018+ if active_refs is None :
2019+ active_refs = set ()
2020+ if ref_type_cache is None :
2021+ ref_type_cache = {}
2022+ if ref_dependency_cache is None :
2023+ ref_dependency_cache = {}
2024+
2025+ ref = _find_schema_ref (field_schema )
2026+ if ref and ref in active_refs :
2027+ return dict
2028+ cache_ref = _cacheable_schema_ref (field_schema )
2029+ cache_key = _schema_cache_key (
2030+ cache_ref , active_refs , root_schema , ref_dependency_cache
2031+ )
2032+ if cache_key and cache_key in ref_type_cache :
2033+ return ref_type_cache [cache_key ]
2034+
2035+ if root_schema is not None and (
2036+ "anyOf" in field_schema
2037+ or "oneOf" in field_schema
2038+ or "allOf" in field_schema
2039+ or "$ref" in field_schema
2040+ ):
2041+ core_schema , _ = _extract_core_schema (field_schema , root_schema )
2042+ if core_schema :
2043+ field_schema = core_schema
2044+
2045+ if ref :
2046+ active_refs .add (ref )
2047+
2048+ try :
2049+ schema_type = field_schema .get ("type" )
2050+ if not schema_type :
2051+ if "properties" in field_schema :
2052+ schema_type = "object"
2053+ elif "items" in field_schema :
2054+ schema_type = "array"
2055+ else :
2056+ schema_type = "string"
2057+
2058+ # 处理嵌套对象和数组
2059+ if schema_type == "object" :
2060+ # 如果有 properties,递归创建嵌套模型
2061+ properties = field_schema .get ("properties" )
2062+ if properties :
2063+ nested_model = _json_schema_to_pydantic (
2064+ "NestedObject" ,
2065+ field_schema ,
2066+ root_schema ,
2067+ active_refs ,
2068+ ref_type_cache ,
2069+ ref_dependency_cache ,
2070+ )
2071+ python_type = nested_model if nested_model else dict
2072+ if cache_key :
2073+ ref_type_cache [cache_key ] = python_type
2074+ return python_type
2075+ python_type = dict
2076+ if cache_key :
2077+ ref_type_cache [cache_key ] = python_type
2078+ return python_type
2079+
2080+ if schema_type == "array" :
2081+ items_schema = field_schema .get ("items" , {})
2082+ if isinstance (items_schema , dict ):
2083+ item_type = _json_type_to_python (
2084+ items_schema ,
2085+ root_schema ,
2086+ active_refs ,
2087+ ref_type_cache ,
2088+ ref_dependency_cache ,
2089+ )
2090+ from typing import List as TypingList
2091+
2092+ python_type = TypingList [item_type ] # type: ignore
2093+ if cache_key :
2094+ ref_type_cache [cache_key ] = python_type
2095+ return python_type
2096+ python_type = list
2097+ if cache_key :
2098+ ref_type_cache [cache_key ] = python_type
2099+ return python_type
2100+
2101+ # 基本类型映射
2102+ mapping = {
2103+ "string" : str ,
2104+ "integer" : int ,
2105+ "number" : float ,
2106+ "boolean" : bool ,
2107+ }
2108+ python_type = mapping .get (schema_type , str )
2109+ if cache_key :
2110+ ref_type_cache [cache_key ] = python_type
2111+ return python_type
2112+ finally :
2113+ if ref :
2114+ active_refs .discard (ref )
18472115
18482116
18492117def _build_args_model_from_parameters (
0 commit comments