Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ paths are considered internals and can change in minor and patch releases.
v4.40.1 (2025-05-??)
--------------------

Added
^^^^^
- Support for Pydantic models with ``extra`` field configuration (``allow``,
``forbid``, ``ignore``). Models with ``extra="allow"`` now accept additional
fields, while ``extra="forbid"`` properly rejects them and ``extra="ignore"``
accepts but ignores extra fields during instantiation.

Fixed
^^^^^
- ``print_shtab`` incorrectly parsed from environment variable (`#725
Expand Down
20 changes: 18 additions & 2 deletions jsonargparse/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1147,8 +1147,24 @@ def check_values(cfg):
group_key = next((g for g in self.groups if key.startswith(g + ".")), None)
if group_key:
subkey = key[len(group_key) + 1 :]
raise NSKeyError(f"Group '{group_key}' does not accept nested key '{subkey}'")
raise NSKeyError(f"Key '{key}' is not expected")
# Check if this is a Pydantic model with extra configuration
group = self.groups[group_key]
should_raise_error = True
if hasattr(group, "group_class") and group.group_class:
from ._optionals import get_pydantic_extra_config

extra_config = get_pydantic_extra_config(group.group_class)
if extra_config == "allow":
# Allow extra fields - don't raise an error
should_raise_error = False
elif extra_config == "ignore":
# Ignore extra fields - don't raise an error, Pydantic will ignore during instantiation
should_raise_error = False
Comment on lines 1152 to 1162
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this logic seems to be pydantic-specific. So it might be better to have all of it in a function imported from optionals, e.g. is_allowed_by_pydantic_extra(...). The function get_pydantic_extra_config could still exist just to not have one single big function.

# For 'forbid' or None (default), raise error
if should_raise_error:
raise NSKeyError(f"Group '{group_key}' does not accept nested key '{subkey}'")
else:
raise NSKeyError(f"Key '{key}' is not expected")

try:
with parser_context(load_value_mode=self.parser_mode):
Expand Down
55 changes: 55 additions & 0 deletions jsonargparse/_optionals.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,58 @@ def validate_annotated(value, typehint: type):
from pydantic import TypeAdapter

return TypeAdapter(typehint).validate_python(value)


def get_pydantic_extra_config(class_type) -> Optional[str]:
"""Get the 'extra' configuration from a Pydantic model.

Args:
class_type: The class to check for Pydantic extra configuration.

Returns:
The extra configuration ('allow', 'forbid', 'ignore') or None if not a Pydantic model.
"""
pydantic_model_version = is_pydantic_model(class_type)
if not pydantic_model_version:
return None

try:

# Handle Pydantic v2 models
if pydantic_model_version > 1:
# Check for model_config attribute (Pydantic v2 style)
if hasattr(class_type, "model_config"):
config = class_type.model_config
if hasattr(config, "get"):
# ConfigDict case
return config.get("extra")
elif hasattr(config, "extra"):
# Direct attribute access
return config.extra

# Check for __config__ attribute (legacy support in v2)
if hasattr(class_type, "__config__"):
config = class_type.__config__
if hasattr(config, "extra"):
return config.extra

# Handle Pydantic v1 models (including v1 compatibility mode in v2)
else:
if hasattr(class_type, "__config__"):
config = class_type.__config__
if hasattr(config, "extra"):
extra_value = config.extra
# Handle Pydantic v1 Extra enum
if hasattr(extra_value, "value"):
return extra_value.value
elif isinstance(extra_value, str):
return extra_value
else:
# Convert enum to string by taking the last part after the dot
return str(extra_value).split(".")[-1]

except Exception:
# If anything goes wrong, return None to fall back to default behavior
pass

return None
Loading