@@ -219,6 +219,23 @@ def model_rebuild(cls) -> None:
219219 "uniqueItems" ,
220220 }
221221 __extra_key__ : str = SPECIAL_PATH_FORMAT .format ("extras" )
222+ __metadata_only_fields__ : set [str ] = { # noqa: RUF012
223+ "title" ,
224+ "description" ,
225+ "id" ,
226+ "$id" ,
227+ "$schema" ,
228+ "$comment" ,
229+ "examples" ,
230+ "example" ,
231+ "x_enum_varnames" ,
232+ "definitions" ,
233+ "$defs" ,
234+ "default" ,
235+ "readOnly" ,
236+ "writeOnly" ,
237+ "deprecated" ,
238+ }
222239
223240 @model_validator (mode = "before" )
224241 def validate_exclusive_maximum_and_exclusive_minimum (cls , values : Any ) -> Any : # noqa: N805
@@ -413,6 +430,23 @@ def has_multiple_types(self) -> bool:
413430 non_null_types = [t for t in self .type if t != "null" ]
414431 return len (non_null_types ) > 1
415432
433+ @cached_property
434+ def has_ref_with_schema_keywords (self ) -> bool :
435+ """Check if schema has $ref combined with schema-affecting keywords.
436+
437+ Metadata-only keywords (title, description, etc.) are excluded
438+ as they don't affect the schema structure.
439+ """
440+ if not self .ref :
441+ return False
442+ other_fields = self .__fields_set__ - {"ref" }
443+ schema_affecting_fields = other_fields - self .__metadata_only_fields__ - {"extras" }
444+ if self .extras :
445+ schema_affecting_extras = {k for k in self .extras if k not in self .__metadata_only_fields__ }
446+ if schema_affecting_extras :
447+ schema_affecting_fields |= {"extras" }
448+ return bool (schema_affecting_fields )
449+
416450
417451@lru_cache
418452def get_ref_type (ref : str ) -> JSONReference :
@@ -1043,6 +1077,25 @@ def _load_ref_schema_object(self, ref: str) -> JsonSchemaObject:
10431077
10441078 return self .SCHEMA_OBJECT_TYPE .parse_obj (target_schema )
10451079
1080+ def _merge_ref_with_schema (self , obj : JsonSchemaObject ) -> JsonSchemaObject :
1081+ """Merge $ref schema with current schema's additional keywords.
1082+
1083+ JSON Schema 2020-12 allows $ref alongside other keywords,
1084+ which should be merged together.
1085+
1086+ The local keywords take precedence over referenced schema.
1087+ """
1088+ if not obj .ref :
1089+ return obj
1090+
1091+ ref_schema = self ._load_ref_schema_object (obj .ref )
1092+ ref_dict = ref_schema .dict (exclude_unset = True , by_alias = True )
1093+ current_dict = obj .dict (exclude = {"ref" }, exclude_unset = True , by_alias = True )
1094+ merged = self ._deep_merge (ref_dict , current_dict )
1095+ merged .pop ("$ref" , None )
1096+
1097+ return self .SCHEMA_OBJECT_TYPE .parse_obj (merged )
1098+
10461099 def _merge_primitive_schemas (self , items : list [JsonSchemaObject ]) -> JsonSchemaObject :
10471100 """Merge multiple primitive schemas by computing the intersection of their constraints."""
10481101 if len (items ) == 1 :
@@ -1323,9 +1376,16 @@ def parse_combined_schema(
13231376 refs = []
13241377 for index , target_attribute in enumerate (getattr (obj , target_attribute_name , [])):
13251378 if target_attribute .ref :
1326- combined_schemas .append (target_attribute )
1327- refs .append (index )
1328- # TODO: support partial ref
1379+ if target_attribute .has_ref_with_schema_keywords :
1380+ merged_attr = self ._merge_ref_with_schema (target_attribute )
1381+ combined_schemas .append (
1382+ self .SCHEMA_OBJECT_TYPE .parse_obj (
1383+ self ._deep_merge (base_object , merged_attr .dict (exclude_unset = True , by_alias = True ))
1384+ )
1385+ )
1386+ else :
1387+ combined_schemas .append (target_attribute )
1388+ refs .append (index )
13291389 else :
13301390 combined_schemas .append (
13311391 self .SCHEMA_OBJECT_TYPE .parse_obj (
@@ -1878,6 +1938,8 @@ def parse_item( # noqa: PLR0911, PLR0912
18781938 item ,
18791939 root_type_path ,
18801940 )
1941+ if item .has_ref_with_schema_keywords :
1942+ item = self ._merge_ref_with_schema (item )
18811943 if item .ref :
18821944 return self .get_ref_data_type (item .ref )
18831945 if item .custom_type_path : # pragma: no cover
@@ -2540,6 +2602,9 @@ def parse_obj( # noqa: PLR0912
25402602 path : list [str ],
25412603 ) -> None :
25422604 """Parse a JsonSchemaObject by dispatching to appropriate parse methods."""
2605+ if obj .has_ref_with_schema_keywords :
2606+ obj = self ._merge_ref_with_schema (obj )
2607+
25432608 if obj .is_array :
25442609 self .parse_array (name , obj , path )
25452610 elif obj .allOf :
0 commit comments