Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
accepts but ignores extra fields during instantiation.
accepts but ignores extra fields during instantiation (`#732
<https://github.com/omni-us/jsonargparse/pull/732>`__).


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:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if hasattr(group, "group_class") and group.group_class:
if getattr(group, "group_class", None):

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