@@ -144,6 +144,65 @@ def _extract_field_info(self, field: FieldInfo, field_name: str) -> List[Tuple[s
144144
145145 return field_info
146146
147+ def _replace_field_names_case_insensitively (self , field : FieldInfo , field_values : Dict [str , Any ]) -> Dict [str , Any ]:
148+ """
149+ Replace field names in values dict by looking in models fields insensitively.
150+
151+ By having the following models:
152+
153+ ```py
154+ class SubSubSub(BaseModel):
155+ VaL3: str
156+
157+ class SubSub(BaseModel):
158+ Val2: str
159+ SUB_sub_SuB: SubSubSub
160+
161+ class Sub(BaseModel):
162+ VAL1: str
163+ SUB_sub: SubSub
164+
165+ class Settings(BaseSettings):
166+ nested: Sub
167+
168+ model_config = ConfigDict(env_nested_delimiter='__')
169+ ```
170+
171+ Then:
172+ _replace_field_names_case_insensitively(
173+ field,
174+ {"val1": "v1", "sub_SUB": {"VAL2": "v2", "sub_SUB_sUb": {"vAl3": "v3"}}}
175+ )
176+ Returns {'VAL1': 'v1', 'SUB_sub': {'Val2': 'v2', 'SUB_sub_SuB': {'VaL3': 'v3'}}}
177+ """
178+ values : Dict [str , Any ] = {}
179+
180+ for name , value in field_values .items ():
181+ sub_model_field : Optional [FieldInfo ] = None
182+
183+ # This is here to make mypy happy
184+ # Item "None" of "Optional[Type[Any]]" has no attribute "model_fields"
185+ if not field .annotation or not hasattr (field .annotation , 'model_fields' ):
186+ values [name ] = value
187+ continue
188+
189+ # Find field in sub model by looking in fields case insensitively
190+ for sub_model_field_name , f in field .annotation .model_fields .items ():
191+ if not f .validation_alias and sub_model_field_name .lower () == name .lower ():
192+ sub_model_field = f
193+ break
194+
195+ if not sub_model_field :
196+ values [name ] = value
197+ continue
198+
199+ if lenient_issubclass (sub_model_field .annotation , BaseModel ):
200+ values [sub_model_field_name ] = self ._replace_field_names_case_insensitively (sub_model_field , value )
201+ else :
202+ values [sub_model_field_name ] = value
203+
204+ return values
205+
147206 def __call__ (self ) -> Dict [str , Any ]:
148207 d : Dict [str , Any ] = {}
149208
@@ -163,7 +222,10 @@ def __call__(self) -> Dict[str, Any]:
163222 ) from e
164223
165224 if field_value is not None :
166- d [field_key ] = field_value
225+ if not self .config .get ('case_sensitive' , False ) and lenient_issubclass (field .annotation , BaseModel ):
226+ d [field_key ] = self ._replace_field_names_case_insensitively (field , field_value )
227+ else :
228+ d [field_key ] = field_value
167229
168230 return d
169231
@@ -300,6 +362,7 @@ def next_field(field: Optional[FieldInfo], key: str) -> Optional[FieldInfo]:
300362
301363 By having the following models:
302364
365+ ```py
303366 class SubSubModel(BaseSettings):
304367 dvals: Dict
305368
@@ -309,6 +372,7 @@ class SubModel(BaseSettings):
309372
310373 class Cfg(BaseSettings):
311374 sub_model: SubModel
375+ ```
312376
313377 Then:
314378 next_field(sub_model, 'vals') Returns the `vals` field of `SubModel` class
0 commit comments