diff --git a/pydantic_settings/sources/providers/env.py b/pydantic_settings/sources/providers/env.py index f165825b..3d3997f8 100644 --- a/pydantic_settings/sources/providers/env.py +++ b/pydantic_settings/sources/providers/env.py @@ -186,7 +186,10 @@ class Cfg(BaseSettings): type_has_key = self.next_field(type_, key, case_sensitive) if type_has_key: return type_has_key - if is_model_class(annotation) or is_pydantic_dataclass(annotation): # type: ignore[arg-type] + if _lenient_issubclass(get_origin(annotation), dict): + # get value type if it's a dict + return get_args(annotation)[-1] + elif is_model_class(annotation) or is_pydantic_dataclass(annotation): # type: ignore[arg-type] fields = _get_model_fields(annotation) # `case_sensitive is None` is here to be compatible with the old behavior. # Has to be removed in V3. diff --git a/tests/test_settings.py b/tests/test_settings.py index 8c7c3780..9f807def 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -3282,3 +3282,23 @@ class StrictSettings(BaseSettings, env_nested_delimiter='__', strict=True): 'my_int': 1, }, } + + +def test_env_source_when_load_multi_nested_config(env): + class EmbeddingModel(BaseModel): + model: str = 'text-embedding-3-small' + keys: list[str] = Field(default_factory=list) + + class LLM(BaseModel): + embeddings: dict[str, EmbeddingModel] = Field(default_factory=dict) + + class LLMSettings(BaseSettings): + llm: LLM = Field(default_factory=lambda: LLM()) + + model_config = SettingsConfigDict(env_prefix='my_prefix_', env_nested_delimiter='__') + + env.set('my_prefix_llm__embeddings__openai__keys', '["sk-..."]') + env.set('my_prefix_llm__embeddings__qwen__keys', '["sk-..."]') + llm_setting = LLMSettings() + assert llm_setting.llm.embeddings['openai'].keys == ['sk-...'] + assert llm_setting.llm.embeddings['qwen'].keys == ['sk-...']