@@ -1696,6 +1696,10 @@ class JSONSchema(_common.BaseModel):
16961696 ' matches the instance successfully.'
16971697 ),
16981698 )
1699+ additional_properties: Optional[Any] = Field(
1700+ default=None,
1701+ description="""Can either be a boolean or an object; controls the presence of additional properties.""",
1702+ )
16991703 any_of: Optional[list['JSONSchema']] = Field(
17001704 default=None,
17011705 description=(
@@ -1704,6 +1708,20 @@ class JSONSchema(_common.BaseModel):
17041708 ' keyword’s value.'
17051709 ),
17061710 )
1711+ unique_items: Optional[bool] = Field(
1712+ default=None,
1713+ description="""Boolean value that indicates whether the items in an array are unique.""",
1714+ )
1715+ ref: Optional[str] = Field(
1716+ default=None,
1717+ alias='$ref',
1718+ description="""Allows indirect references between schema nodes.""",
1719+ )
1720+ defs: Optional[dict[str, 'JSONSchema']] = Field(
1721+ default=None,
1722+ alias='$defs',
1723+ description="""Schema definitions to be used with $ref.""",
1724+ )
17071725
17081726
17091727class Schema(_common.BaseModel):
@@ -1915,7 +1933,7 @@ def from_json_schema(
19151933 list_schema_field_names: tuple[str, ...] = (
19161934 'any_of', # 'one_of', 'all_of', 'not' to come
19171935 )
1918- dict_schema_field_names: tuple[str, ...] = ('properties',) # 'defs' to come
1936+ dict_schema_field_names: tuple[str, ...] = ('properties',)
19191937
19201938 related_field_names_by_type: dict[str, tuple[str, ...]] = {
19211939 JSONSchemaType.NUMBER.value: (
@@ -1964,6 +1982,23 @@ def from_json_schema(
19641982 # placeholder for potential gemini api unsupported fields
19651983 gemini_api_unsupported_field_names: tuple[str, ...] = ()
19661984
1985+ def _resolve_ref(
1986+ ref_path: str, root_schema_dict: dict[str, Any]
1987+ ) -> dict[str, Any]:
1988+ """Helper to resolve a $ref path."""
1989+ current = root_schema_dict
1990+ for part in ref_path.lstrip('#/').split('/'):
1991+ if part == '$defs':
1992+ part = 'defs'
1993+ current = current[part]
1994+ current.pop('title', None)
1995+ if 'properties' in current and current['properties'] is not None:
1996+ for prop_schema in current['properties'].values():
1997+ if isinstance(prop_schema, dict):
1998+ prop_schema.pop('title', None)
1999+
2000+ return current
2001+
19672002 def normalize_json_schema_type(
19682003 json_schema_type: Optional[
19692004 Union[JSONSchemaType, Sequence[JSONSchemaType], str, Sequence[str]]
@@ -1972,11 +2007,16 @@ def normalize_json_schema_type(
19722007 """Returns (non_null_types, nullable)"""
19732008 if json_schema_type is None:
19742009 return [], False
1975- if not isinstance(json_schema_type, Sequence):
1976- json_schema_type = [json_schema_type]
2010+ type_sequence: Sequence[Union[JSONSchemaType, str]]
2011+ if isinstance(json_schema_type, str) or not isinstance(
2012+ json_schema_type, Sequence
2013+ ):
2014+ type_sequence = [json_schema_type]
2015+ else:
2016+ type_sequence = json_schema_type
19772017 non_null_types = []
19782018 nullable = False
1979- for type_value in json_schema_type :
2019+ for type_value in type_sequence :
19802020 if isinstance(type_value, JSONSchemaType):
19812021 type_value = type_value.value
19822022 if type_value == JSONSchemaType.NULL.value:
@@ -1996,7 +2036,10 @@ def raise_error_if_cannot_convert(
19962036 for field_name, field_value in json_schema_dict.items():
19972037 if field_value is None:
19982038 continue
1999- if field_name not in google_schema_field_names:
2039+ if field_name not in google_schema_field_names and field_name not in [
2040+ 'ref',
2041+ 'defs',
2042+ ]:
20002043 raise ValueError(
20012044 f'JSONSchema field "{field_name}" is not supported by the '
20022045 'Schema object. And the "raise_error_on_unsupported_field" '
@@ -2026,12 +2069,19 @@ def copy_schema_fields(
20262069 )
20272070
20282071 def convert_json_schema(
2029- json_schema: JSONSchema,
2072+ current_json_schema: JSONSchema,
2073+ root_json_schema_dict: dict[str, Any],
20302074 api_option: Literal['VERTEX_AI', 'GEMINI_API'],
20312075 raise_error_on_unsupported_field: bool,
20322076 ) -> 'Schema':
20332077 schema = Schema()
2034- json_schema_dict = json_schema.model_dump()
2078+ json_schema_dict = current_json_schema.model_dump()
2079+
2080+ if json_schema_dict.get('ref'):
2081+ json_schema_dict = _resolve_ref(
2082+ json_schema_dict['ref'], root_json_schema_dict
2083+ )
2084+
20352085 raise_error_if_cannot_convert(
20362086 json_schema_dict=json_schema_dict,
20372087 api_option=api_option,
@@ -2057,6 +2107,7 @@ def convert_json_schema(
20572107 non_null_types, nullable = normalize_json_schema_type(
20582108 json_schema_dict.get('type', None)
20592109 )
2110+ is_union_like_type = len(non_null_types) > 1
20602111 if len(non_null_types) > 1:
20612112 logger.warning(
20622113 'JSONSchema type is union-like, e.g. ["null", "string", "array"]. '
@@ -2086,50 +2137,89 @@ def convert_json_schema(
20862137 # Pass 2: the JSONSchema.type is not union-like,
20872138 # e.g. 'string', ['string'], ['null', 'string'].
20882139 for field_name, field_value in json_schema_dict.items():
2089- if field_value is None:
2140+ if field_value is None or field_name == 'defs' :
20902141 continue
20912142 if field_name in schema_field_names:
2143+ if field_name == 'items' and not field_value:
2144+ continue
20922145 schema_field_value: 'Schema' = convert_json_schema(
2093- json_schema=JSONSchema(**field_value),
2146+ current_json_schema=JSONSchema(**field_value),
2147+ root_json_schema_dict=root_json_schema_dict,
20942148 api_option=api_option,
20952149 raise_error_on_unsupported_field=raise_error_on_unsupported_field,
20962150 )
20972151 setattr(schema, field_name, schema_field_value)
20982152 elif field_name in list_schema_field_names:
20992153 list_schema_field_value: list['Schema'] = [
21002154 convert_json_schema(
2101- json_schema=JSONSchema(**this_field_value),
2155+ current_json_schema=JSONSchema(**this_field_value),
2156+ root_json_schema_dict=root_json_schema_dict,
21022157 api_option=api_option,
21032158 raise_error_on_unsupported_field=raise_error_on_unsupported_field,
21042159 )
21052160 for this_field_value in field_value
21062161 ]
21072162 setattr(schema, field_name, list_schema_field_value)
2163+ if not schema.type and not is_union_like_type:
2164+ schema.type = Type('OBJECT')
21082165 elif field_name in dict_schema_field_names:
21092166 dict_schema_field_value: dict[str, 'Schema'] = {
21102167 key: convert_json_schema(
2111- json_schema=JSONSchema(**value),
2168+ current_json_schema=JSONSchema(**value),
2169+ root_json_schema_dict=root_json_schema_dict,
21122170 api_option=api_option,
21132171 raise_error_on_unsupported_field=raise_error_on_unsupported_field,
21142172 )
21152173 for key, value in field_value.items()
21162174 }
21172175 setattr(schema, field_name, dict_schema_field_value)
21182176 elif field_name == 'type':
2119- # non_null_types can only be empty or have one element.
2120- # because already handled union-like case above.
21212177 non_null_types, nullable = normalize_json_schema_type(field_value)
21222178 if nullable:
21232179 schema.nullable = True
21242180 if non_null_types:
21252181 schema.type = Type(non_null_types[0])
21262182 else:
2127- setattr(schema, field_name, field_value)
2183+ if (
2184+ hasattr(schema, field_name)
2185+ and field_name != 'additional_properties'
2186+ ):
2187+ setattr(schema, field_name, field_value)
2188+
2189+ if (
2190+ schema.type == 'ARRAY'
2191+ and schema.items
2192+ and not schema.items.model_dump(exclude_unset=True)
2193+ ):
2194+ schema.items = None
2195+
2196+ if schema.any_of and len(schema.any_of) == 2:
2197+ nullable_part = None
2198+ type_part = None
2199+ for part in schema.any_of:
2200+ # A schema representing `None` will either be of type NULL or just be nullable.
2201+ part_dict = part.model_dump(exclude_unset=True)
2202+ if part_dict == {'nullable': True} or part_dict == {'type': 'NULL'}:
2203+ nullable_part = part
2204+ else:
2205+ type_part = part
2206+
2207+ # If we found both parts, unwrap them into a single schema.
2208+ if nullable_part and type_part:
2209+ default_value = schema.default
2210+ schema = type_part
2211+ schema.nullable = True
2212+ # Carry the default value over to the unwrapped schema
2213+ if default_value is not None:
2214+ schema.default = default_value
21282215
21292216 return schema
21302217
2218+ # This is the initial call to the recursive function.
2219+ root_schema_dict = json_schema.model_dump()
21312220 return convert_json_schema(
2132- json_schema=json_schema,
2221+ current_json_schema=json_schema,
2222+ root_json_schema_dict=root_schema_dict,
21332223 api_option=api_option,
21342224 raise_error_on_unsupported_field=raise_error_on_unsupported_field,
21352225 )
@@ -2371,7 +2461,32 @@ def from_callable_with_api_option(
23712461 json_schema_dict = _automatic_function_calling_util._add_unevaluated_items_to_fixed_len_tuple_schema(
23722462 json_schema_dict
23732463 )
2374- parameters_json_schema[name] = json_schema_dict
2464+ if 'prefixItems' in json_schema_dict:
2465+ parameters_json_schema[name] = json_schema_dict
2466+ continue
2467+
2468+ union_args = typing.get_args(param.annotation)
2469+ has_primitive = any(
2470+ _automatic_function_calling_util._is_builtin_primitive_or_compound(
2471+ arg
2472+ )
2473+ for arg in union_args
2474+ )
2475+ if (
2476+ '$ref' in json_schema_dict or '$defs' in json_schema_dict
2477+ ) and has_primitive:
2478+ # This is a complex schema with a primitive (e.g., str | MyModel)
2479+ # that is better represented by raw JSON schema.
2480+ parameters_json_schema[name] = json_schema_dict
2481+ continue
2482+
2483+ schema = Schema.from_json_schema(
2484+ json_schema=JSONSchema(**json_schema_dict),
2485+ api_option=api_option,
2486+ )
2487+ if param.default is not inspect.Parameter.empty:
2488+ schema.default = param.default
2489+ parameters_properties[name] = schema
23752490 except Exception as e:
23762491 _automatic_function_calling_util._raise_for_unsupported_param(
23772492 param, callable.__name__, e
0 commit comments