Repository: omni-us/jsonargparse

Issue:
Parsing fails with mixture of classes and dataclasses
<!-- If you like this project, please ⭐ star it https://github.com/omni-us/jsonargparse/ -->

## 🐛 Bug report

<!-- A clear and concise description of the bug. -->

Validation fails when there is a union of dataclass and regular classes.

### To reproduce (Updated Example)

Create `cli.py`:

```
from dataclasses import dataclass
from typing import Dict, Optional, Union

from jsonargparse import CLI


@dataclass
class MyData:
    val_a: Optional[str] = None
    val_b: Optional[str] = None


class MyClass:
    def __init__(self, val_1: str, val_2: str):
        self.val_1 = val_1
        self.val_2 = val_2


class MyContainer:
    def __init__(self, value: Union[MyData, MyClass]):
        self.value = value

def command(mycontainer: Optional[MyContainer] =[]) -> None:
    print("Success")


if __name__ == '__main__':
    CLI(command)
```

And config.yaml file:

```
mycontainer:
  class_path: MyContainer
  init_args:
    value:
      class_path: MyClass
      init_args:
        val_1: "A"
        val_2: "B"
```

Run the command:

```
python cli.py --config config.yaml
```

The following error results:

```
usage: cli.py [-h] [--config CONFIG] [--print_config[=flags]] [--mycontainer.help CLASS_PATH_OR_NAME] [--mycontainer MYCONTAINER]
              [--print_shtab {bash,zsh,tcsh}]
error: Validation failed: Parser key "mycontainer":
  Does not validate against any of the Union subtypes
  Subtypes: (<class '__main__.MyContainer'>, <class 'NoneType'>)
  Errors:
    - Problem with given class_path '__main__.MyContainer':
        Parser key "value":
          Does not validate against any of the Union subtypes
          Subtypes: (<class '__main__.MyData'>, <class '__main__.MyClass'>)
          Errors:
            - No action for key "class_path" to set its default.
            - Not a valid subclass of MyClass
              Subclass types expect one of:
              - a class path (str)
              - a dict with class_path entry
              - a dict without class_path but with init_args entry (class path given previously)
          Given value type: <class 'jsonargparse._namespace.Namespace'>
          Given value: Namespace(val_a=None, val_b=None, class_path='MyClass', init_args=Namespace(val_1='A', val_2='B'))
    - Expected a <class 'NoneType'>
  Given value type: <class 'jsonargparse._namespace.Namespace'>
  Given value: Namespace(class_path='__main__.MyContainer', init_args=Namespace(value=Namespace(val_a=None, val_b=None, class_path='MyClass', init_args=Namespace(val_1='A', val_2='B'))))
```

It should print "Success".

If the `Union[MyData, MyClass]` is replaced with `Union[MyClass]`, then it is successful.

### Environment

<!-- Fill in the list below. -->

- jsonargparse version: 4.32.1
- Python version: 3.10.13
- How jsonargparse was installed: ` pip install "jsonargparse[all]>=4.27.7"`
- OS: Ubuntu 24.04 LTS


Discussion:
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=581) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=581&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=581&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=581&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=581&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=581&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=581)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=581) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=581&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=581&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=581&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=581&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=581&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=581)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`80a5418`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/80a5418f5785ae6a0ab17357305b8935ae091008?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) to head [(`4285c23`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/4285c23b8c21358a20a48dc31b442e120996d925?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main      #581   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           22        22           
  Lines         6466      6472    +6     
=========================================
+ Hits          6466      6472    +6     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [py3.10](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `85.74% <100.00%> (+0.01%)` | :arrow_up: |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.70% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_pydantic1](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `49.14% <63.63%> (+0.26%)` | :arrow_up: |
| [py3.10_pydantic2](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.93% <63.63%> (+0.26%)` | :arrow_up: |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.71% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `85.73% <100.00%> (+0.01%)` | :arrow_up: |
| [py3.11_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.67% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.68% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `85.87% <100.00%> (+0.01%)` | :arrow_up: |
| [py3.12_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.12_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.7](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.37% <100.00%> (+0.01%)` | :arrow_up: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.32% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.37% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.38% <100.00%> (+0.01%)` | :arrow_up: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.33% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.36% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.30% <100.00%> (+0.01%)` | :arrow_up: |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.28% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.30% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/581?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9ebc945..4a13a54 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -56,6 +56,7 @@ repos:
       [
         types-PyYAML,
         types-requests,
+        types-setuptools,
       ]
     verbose: true
 
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 2b076e4..0eb388d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -21,6 +21,8 @@ Added
   <https://github.com/omni-us/jsonargparse/pull/554>`__).
 - Support for ``NotRequired`` and ``Required`` annotations for ``TypedDict``
   keys (`#571 <https://github.com/omni-us/jsonargparse/pull/571>`__).
+- ``dataclass`` types now accept ``class_path`` and ``init_args`` as value
+  (`#581 <https://github.com/omni-us/jsonargparse/pull/581>`__).
 
 Fixed
 ^^^^^
@@ -38,8 +40,7 @@ Changed
 - Removed shtab experimental warning (`#561
   <https://github.com/omni-us/jsonargparse/pull/561>`__).
 - For consistency ``add_subclass_arguments`` now sets default ``None`` instead
-  of ``SUPPRESS`` (`lightning#20103
-  <https://github.com/Lightning-AI/pytorch-lightning/issue/20103>`__).
+  of ``SUPPRESS`` (`#568 <https://github.com/omni-us/jsonargparse/pull/568>`__).
 
 
 v4.32.1 (2024-08-23)
diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py
index 9ec653b..2bee852 100644
--- a/jsonargparse/_core.py
+++ b/jsonargparse/_core.py
@@ -321,7 +321,7 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
             except Exception as ex:
                 self.error(str(ex), ex)
 
-            if not skip_check and not lenient_check.get():
+            if not skip_check:
                 self.check_config(cfg, skip_required=skip_required)
 
         if not (with_meta or (with_meta is None and self._default_meta)):
@@ -675,8 +675,10 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
         """
         if "description" not in kwargs:
             kwargs["description"] = "For more details of each subcommand, add it as an argument followed by --help."
-        with parser_context(parent_parser=self, lenient_check=True):
-            subcommands: _ActionSubCommands = super().add_subparsers(dest=dest, **kwargs)  # type: ignore[assignment]
+        default_config_files = self.default_config_files
+        self.default_config_files = []
+        subcommands: _ActionSubCommands = super().add_subparsers(dest=dest, **kwargs)  # type: ignore[assignment]
+        self.default_config_files = default_config_files
         if required:
             self.required_args.add(dest)
         subcommands._required = required  # type: ignore[attr-defined]
@@ -1069,7 +1071,7 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
                             continue
                 val = cfg[key]
                 if action is not None:
-                    if val is None and skip_none:
+                    if (val is None and skip_none) or lenient_check.get():
                         continue
                     try:
                         self._check_value_key(action, val, key, ccfg)
@@ -1397,7 +1399,7 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
         return self._default_config_files
 
     @default_config_files.setter
-    def default_config_files(self, default_config_files: Optional[List[Union[str, os.PathLike]]]):
+    def default_config_files(self, default_config_files: Optional[Sequence[Union[str, os.PathLike]]]):
         if default_config_files is None:
             self._default_config_files = []
         elif isinstance(default_config_files, list) and all(
diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py
index 3a2f854..249159a 100644
--- a/jsonargparse/_typehints.py
+++ b/jsonargparse/_typehints.py
@@ -633,6 +633,8 @@ class ActionTypeHint(Action):
         kwargs = dict(sub_add_kwargs) if sub_add_kwargs else {}
         if skip_args:
             kwargs.setdefault("skip", set()).add(skip_args)
+        if is_subclass_spec(kwargs.get("default")):
+            kwargs["default"] = kwargs["default"].get("init_args")
         parser = parent_parser.get()
         parser = type(parser)(exit_on_error=False, logger=parser.logger, parser_mode=parser.parser_mode)
         remove_actions(parser, (ActionConfigFile, _ActionPrintConfig))
@@ -1010,6 +1012,8 @@ def adapt_typehints(
         if serialize:
             val = load_value(parser.dump(val, **dump_kwargs.get()))
         elif isinstance(val, (dict, Namespace)):
+            if is_subclass_spec(val) and get_import_path(typehint) == val.get("class_path"):
+                val = val.get("init_args")
             val = parser.parse_object(val, defaults=sub_defaults.get() or list_item)
         elif isinstance(val, NestedArg):
             prev_val = prev_val if isinstance(prev_val, Namespace) else None


Test patch:
diff --git a/jsonargparse_tests/test_dataclass_like.py b/jsonargparse_tests/test_dataclass_like.py
index 818eb3b..67a820e 100644
--- a/jsonargparse_tests/test_dataclass_like.py
+++ b/jsonargparse_tests/test_dataclass_like.py
@@ -502,6 +502,46 @@ def test_nested_generic_dataclass(parser):
     assert "--x.y.g4 g4          (required, type: dict[str, union[float, bool]])" in help_str
 
 
+# union mixture tests
+
+
+@dataclasses.dataclass
+class UnionData:
+    data_a: int = 1
+    data_b: Optional[str] = None
+
+
+class UnionClass:
+    def __init__(self, prm_1: float, prm_2: bool):
+        self.prm_1 = prm_1
+        self.prm_2 = prm_2
+
+
+@pytest.mark.parametrize(
+    "union_type",
+    [
+        Union[UnionData, UnionClass],
+        Union[UnionClass, UnionData],
+    ],
+)
+def test_class_path_union_mixture_dataclass_and_class(parser, union_type):
+    parser.add_argument("--union", type=union_type, enable_path=True)
+
+    value = {"class_path": f"{__name__}.UnionData", "init_args": {"data_a": 2, "data_b": "x"}}
+    cfg = parser.parse_args([f"--union={json.dumps(value)}"])
+    init = parser.instantiate_classes(cfg)
+    assert isinstance(init.union, UnionData)
+    assert dataclasses.asdict(init.union) == {"data_a": 2, "data_b": "x"}
+    assert yaml.safe_load(parser.dump(cfg))["union"] == value["init_args"]
+
+    value = {"class_path": f"{__name__}.UnionClass", "init_args": {"prm_1": 1.2, "prm_2": False}}
+    cfg = parser.parse_args([f"--union={json.dumps(value)}"])
+    init = parser.instantiate_classes(cfg)
+    assert isinstance(init.union, UnionClass)
+    assert init.union.prm_1 == 1.2
+    assert yaml.safe_load(parser.dump(cfg))["union"] == value
+
+
 # final classes tests