@@ -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,268 @@ 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" , "allOf" ):
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+ ref = _find_schema_ref (option )
1857+ if ref :
1858+ return ref
1859+
1860+ return None
1861+
1862+
1863+ def _cacheable_schema_ref (field_schema : Dict [str , Any ]) -> Optional [str ]:
1864+ """查找可安全复用类型缓存的单一 $ref"""
1865+ ref = field_schema .get ("$ref" )
1866+ if isinstance (ref , str ):
1867+ return ref if set (field_schema ) <= {"$ref" } else None
1868+
1869+ for union_key in ("anyOf" , "oneOf" ):
1870+ options = field_schema .get (union_key )
1871+ if not isinstance (options , list ):
1872+ continue
1873+
1874+ refs : List [str ] = []
1875+ for option in options :
1876+ if not isinstance (option , dict ):
1877+ continue
1878+ if option .get ("type" ) == "null" :
1879+ continue
1880+ option_ref = _cacheable_schema_ref (option )
1881+ if not option_ref :
1882+ return None
1883+ refs .append (option_ref )
1884+
1885+ if len (refs ) == 1 :
1886+ return refs [0 ]
1887+
1888+ return None
1889+
1890+
1891+ def _collect_direct_refs (schema : Any ) -> Set [str ]:
1892+ """收集 schema 子树中直接写出的 $ref"""
1893+ refs : Set [str ] = set ()
1894+ if not isinstance (schema , dict ):
1895+ return refs
1896+
1897+ ref = schema .get ("$ref" )
1898+ if isinstance (ref , str ):
1899+ refs .add (ref )
1900+
1901+ for key , value in schema .items ():
1902+ if key == "$ref" :
1903+ continue
1904+ if isinstance (value , dict ):
1905+ refs .update (_collect_direct_refs (value ))
1906+ elif isinstance (value , list ):
1907+ for item in value :
1908+ refs .update (_collect_direct_refs (item ))
18291909
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 ,
1910+ return refs
1911+
1912+
1913+ def _reachable_refs_for_ref (
1914+ ref : str ,
1915+ root_schema : Dict [str , Any ],
1916+ ref_dependency_cache : Optional [Dict [str , Set [str ]]] = None ,
1917+ ) -> Set [str ]:
1918+ """收集某个 $ref 目标 schema 可达的引用集合"""
1919+ if ref_dependency_cache is None :
1920+ ref_dependency_cache = {}
1921+ if ref in ref_dependency_cache :
1922+ return set (ref_dependency_cache [ref ])
1923+
1924+ visited : Set [str ] = set ()
1925+ direct_ref_map : Dict [str , Set [str ]] = {}
1926+ stack = [ref ]
1927+ while stack :
1928+ current_ref = stack .pop ()
1929+ if current_ref in visited :
1930+ continue
1931+ visited .add (current_ref )
1932+
1933+ cached_refs = ref_dependency_cache .get (current_ref )
1934+ if cached_refs is not None :
1935+ continue
1936+
1937+ ref_schema = _resolve_ref_schema (current_ref , root_schema )
1938+ if not ref_schema :
1939+ continue
1940+
1941+ direct_refs = _collect_direct_refs (ref_schema )
1942+ direct_ref_map [current_ref ] = direct_refs
1943+ for direct_ref in direct_refs :
1944+ if (
1945+ direct_ref not in visited
1946+ and direct_ref not in ref_dependency_cache
1947+ ):
1948+ stack .append (direct_ref )
1949+
1950+ closures = {
1951+ current_ref : set (direct_refs )
1952+ for current_ref , direct_refs in direct_ref_map .items ()
18451953 }
1846- return mapping .get (schema_type , str )
1954+ changed = True
1955+ while changed :
1956+ changed = False
1957+ for current_ref , direct_refs in direct_ref_map .items ():
1958+ expanded_refs = set (direct_refs )
1959+ for direct_ref in direct_refs :
1960+ if direct_ref in closures :
1961+ expanded_refs .update (closures [direct_ref ])
1962+ else :
1963+ expanded_refs .update (
1964+ ref_dependency_cache .get (direct_ref , set ())
1965+ )
1966+ if expanded_refs != closures [current_ref ]:
1967+ closures [current_ref ] = expanded_refs
1968+ changed = True
1969+
1970+ ref_dependency_cache .update (closures )
1971+ return set (ref_dependency_cache .get (ref , set ()))
1972+
1973+
1974+ def _schema_cache_key (
1975+ ref : Optional [str ],
1976+ active_refs : Set [str ],
1977+ root_schema : Optional [Dict [str , Any ]],
1978+ ref_dependency_cache : Optional [Dict [str , Set [str ]]],
1979+ ) -> Optional [Tuple [str , Tuple [str , ...]]]:
1980+ """根据 ref 和当前递归上下文生成类型缓存键"""
1981+ if not ref :
1982+ return None
1983+ relevant_refs = active_refs
1984+ if root_schema is not None :
1985+ reachable_refs = _reachable_refs_for_ref (
1986+ ref , root_schema , ref_dependency_cache
1987+ )
1988+ relevant_refs = active_refs .intersection (reachable_refs )
1989+ return ref , tuple (sorted (relevant_refs ))
1990+
1991+
1992+ def _json_type_to_python (
1993+ field_schema : Dict [str , Any ],
1994+ root_schema : Optional [Dict [str , Any ]] = None ,
1995+ active_refs : Optional [Set [str ]] = None ,
1996+ ref_type_cache : Optional [Dict [Tuple [str , Tuple [str , ...]], type ]] = None ,
1997+ ref_dependency_cache : Optional [Dict [str , Set [str ]]] = None ,
1998+ ) -> type :
1999+ """映射 JSON Schema 类型到 Python 类型
2000+
2001+ Args:
2002+ field_schema: 字段 schema, 可能是裸类型或 anyOf/oneOf/$ref 包装
2003+ root_schema: 顶层 schema, 用于解析 ``$ref`` 指向的 ``$defs``
2004+ active_refs: 当前递归栈中的 ``$ref``,用于防止自引用无限展开
2005+ ref_type_cache: 已构建的 ``$ref`` 类型缓存, 避免共享引用重复展开
2006+ ref_dependency_cache: ``$ref`` 可达引用缓存, 避免重复遍历共享 DAG
2007+ """
2008+ if active_refs is None :
2009+ active_refs = set ()
2010+ if ref_type_cache is None :
2011+ ref_type_cache = {}
2012+ if ref_dependency_cache is None :
2013+ ref_dependency_cache = {}
2014+
2015+ ref = _find_schema_ref (field_schema )
2016+ if ref and ref in active_refs :
2017+ return dict
2018+ cache_ref = _cacheable_schema_ref (field_schema )
2019+ cache_key = _schema_cache_key (
2020+ cache_ref , active_refs , root_schema , ref_dependency_cache
2021+ )
2022+ if cache_key and cache_key in ref_type_cache :
2023+ return ref_type_cache [cache_key ]
2024+
2025+ if root_schema is not None and (
2026+ "anyOf" in field_schema
2027+ or "oneOf" in field_schema
2028+ or "allOf" in field_schema
2029+ or "$ref" in field_schema
2030+ ):
2031+ core_schema , _ = _extract_core_schema (field_schema , root_schema )
2032+ if core_schema :
2033+ field_schema = core_schema
2034+
2035+ if ref :
2036+ active_refs .add (ref )
2037+
2038+ try :
2039+ schema_type = field_schema .get ("type" )
2040+ if not schema_type :
2041+ if "properties" in field_schema :
2042+ schema_type = "object"
2043+ elif "items" in field_schema :
2044+ schema_type = "array"
2045+ else :
2046+ schema_type = "string"
2047+
2048+ # 处理嵌套对象和数组
2049+ if schema_type == "object" :
2050+ # 如果有 properties,递归创建嵌套模型
2051+ properties = field_schema .get ("properties" )
2052+ if properties :
2053+ nested_model = _json_schema_to_pydantic (
2054+ "NestedObject" ,
2055+ field_schema ,
2056+ root_schema ,
2057+ active_refs ,
2058+ ref_type_cache ,
2059+ ref_dependency_cache ,
2060+ )
2061+ python_type = nested_model if nested_model else dict
2062+ if cache_key :
2063+ ref_type_cache [cache_key ] = python_type
2064+ return python_type
2065+ python_type = dict
2066+ if cache_key :
2067+ ref_type_cache [cache_key ] = python_type
2068+ return python_type
2069+
2070+ if schema_type == "array" :
2071+ items_schema = field_schema .get ("items" , {})
2072+ if isinstance (items_schema , dict ):
2073+ item_type = _json_type_to_python (
2074+ items_schema ,
2075+ root_schema ,
2076+ active_refs ,
2077+ ref_type_cache ,
2078+ ref_dependency_cache ,
2079+ )
2080+ from typing import List as TypingList
2081+
2082+ python_type = TypingList [item_type ] # type: ignore
2083+ if cache_key :
2084+ ref_type_cache [cache_key ] = python_type
2085+ return python_type
2086+ python_type = list
2087+ if cache_key :
2088+ ref_type_cache [cache_key ] = python_type
2089+ return python_type
2090+
2091+ # 基本类型映射
2092+ mapping = {
2093+ "string" : str ,
2094+ "integer" : int ,
2095+ "number" : float ,
2096+ "boolean" : bool ,
2097+ }
2098+ python_type = mapping .get (schema_type , str )
2099+ if cache_key :
2100+ ref_type_cache [cache_key ] = python_type
2101+ return python_type
2102+ finally :
2103+ if ref :
2104+ active_refs .discard (ref )
18472105
18482106
18492107def _build_args_model_from_parameters (
0 commit comments