Repository: omni-us/jsonargparse (python)

Issue:
Linked arguments are not validated correctly on config dump
## 🕰️ Regression report

Starting from v4.37.0 (including the latest v.4.38.0), saving a config using ArgumentParser.save triggers an internal validation error when linked arguments are used.

### To reproduce

Using LightningCLI, follow the tutorial on argument linking.
https://lightning.ai/docs/pytorch/stable/cli/lightning_cli_expert.html#argument-linking

The issue happens when the SaveConfigCallback of LightningCLI tries to save the configuration. It is not triggered when using parse_args (the config is parsed correctly and the objects such as the model, loggers, datamodules, ... are correctly instantiated).

Version v4.36.0 works correctly when using the very same code.

Investigating a bit I found that in v4.36.0 the "target" value is correctly found in the ` val` and `ccfg` variables of [this part of the code](https://github.com/omni-us/jsonargparse/blob/b7eed539f71b6c1be3be40d3d1148af2d28e8dec/jsonargparse/_core.py#L1134), while the target value is not present in those vars starting from version v4.37.0.

### Prior behavior

Version v4.36.0 works correctly and SaveConfigCallback is able to save the configuration correctly.

### Env info
PyTorch Lightning version: 2.5.1 (pip install)
jsonargparse: affected versions are v4.37/38.0 (pip install), v.36.0 works correctly
Python: 3.12.9 (conda-forge)

### Stack trace (v4.38.0)

<details>
```
Traceback (most recent call last):
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_typehints.py", line 838, in adapt_typehints
    vals.append(adapt_typehints(val, subtype, **adapt_kwargs))
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_typehints.py", line 787, in adapt_typehints
    raise_unexpected_value(f"Expected a {typehint}", val)
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_typehints.py", line 718, in raise_unexpected_value
    raise ValueError(message) from exception
ValueError: Expected a <class 'NoneType'>. Got value: Namespace(class_path='lightning_data_modules.AIGenBenchPipeline', init_args=Namespace(dataset_loader=Namespace(class_path='lightning_data_modules.datasets.AIGenBenchDatasetLoader', init_args=Namespace(dataset_path='/home/lorenzo/deepfake_fast/deepfake_benchmark_raw/')), train_batch_size=512, eval_batch_size=512, num_workers=8, train_subset_size=0.2, validation_subset_size=0.2, move_data_to_local_storage=False, deterministic_augmentations=True, augmentation_factor=4, augmentations_base_seed=4321, maximum_processing_size=1080))

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_typehints.py", line 597, in _check_type
    raise ex
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_typehints.py", line 582, in _check_type
    val = adapt_typehints(val, self._typehint, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_typehints.py", line 846, in adapt_typehints
    raise_union_unexpected_value(sorted_subtypes, val, vals)
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_typehints.py", line 725, in raise_union_unexpected_value
    raise ValueError(
ValueError: Does not validate against any of the Union subtypes
Subtypes: [<class 'NoneType'>, <class 'lightning.pytorch.core.datamodule.LightningDataModule'>]
Errors:
  - Expected a <class 'NoneType'>
  - Problem with given class_path 'lightning_data_modules.AIGenBenchPipeline':
      Validation failed: Key "sliding_windows_definition.benchmark_type" is required but not included in config object or its value is None.
Given value type: <class 'jsonargparse._namespace.Namespace'>
Given value: Namespace(class_path='lightning_data_modules.AIGenBenchPipeline', init_args=Namespace(dataset_loader=Namespace(class_path='lightning_data_modules.datasets.AIGenBenchDatasetLoader', init_args=Namespace(dataset_path='/home/lorenzo/deepfake_fast/deepfake_benchmark_raw/')), train_batch_size=512, eval_batch_size=512, num_workers=8, train_subset_size=0.2, validation_subset_size=0.2, move_data_to_local_storage=False, deterministic_augmentations=True, augmentation_factor=4, augmentations_base_seed=4321, maximum_processing_size=1080))

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_core.py", line 1147, in validate
    check_values(cfg)
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_core.py", line 1134, in check_values
    raise ex
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_core.py", line 1129, in check_values
    self._check_value_key(action, val, key, ccfg)
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_core.py", line 1424, in _check_value_key
    value = action._check_type_(value, cfg=cfg)  # type: ignore[attr-defined]
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_common.py", line 333, in _check_type_
    return self._check_type(value, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_typehints.py", line 610, in _check_type
    raise TypeError(f'Parser key "{self.dest}"{elem}:\n{error}') from ex
TypeError: Parser key "data":
  Does not validate against any of the Union subtypes
  Subtypes: [<class 'NoneType'>, <class 'lightning.pytorch.core.datamodule.LightningDataModule'>]
  Errors:
    - Expected a <class 'NoneType'>
    - Problem with given class_path 'lightning_data_modules.AIGenBenchPipeline':
        Validation failed: Key "sliding_windows_definition.benchmark_type" is required but not included in config object or its value is None.
  Given value type: <class 'jsonargparse._namespace.Namespace'>
  Given value: Namespace(class_path='lightning_data_modules.AIGenBenchPipeline', init_args=Namespace(dataset_loader=Namespace(class_path='lightning_data_modules.datasets.AIGenBenchDatasetLoader', init_args=Namespace(dataset_path='/home/lorenzo/deepfake_fast/deepfake_benchmark_raw/')), train_batch_size=512, eval_batch_size=512, num_workers=8, train_subset_size=0.2, validation_subset_size=0.2, move_data_to_local_storage=False, deterministic_augmentations=True, augmentation_factor=4, augmentations_base_seed=4321, maximum_processing_size=1080))

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/lorenzo/Desktop/VCS/deepfake-benchmark/training_and_evaluation/lightning_main.py", line 215, in <module>
    CheckpointableTrainingSlidingWindows()()
  File "/home/lorenzo/Desktop/VCS/deepfake-benchmark/training_and_evaluation/lightning_main.py", line 111, in __call__
    cli._proceed_with_subcommand()
  File "/home/lorenzo/Desktop/VCS/deepfake-benchmark/training_and_evaluation/training_utils/lightning_cli_sliding_windows.py", line 45, in _proceed_with_subcommand
    return self._run_subcommand(self.subcommand)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzo/Desktop/VCS/deepfake-benchmark/training_and_evaluation/training_utils/lightning_cli_sliding_windows.py", line 39, in _run_subcommand
    return super()._run_subcommand(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/lightning/pytorch/cli.py", line 708, in _run_subcommand
    fn(**fn_kwargs)
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/lightning/pytorch/trainer/trainer.py", line 561, in fit
    call._call_and_handle_interrupt(
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/lightning/pytorch/trainer/call.py", line 48, in _call_and_handle_interrupt
    return trainer_fn(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/lightning/pytorch/trainer/trainer.py", line 599, in _fit_impl
    self._run(model, ckpt_path=ckpt_path)
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/lightning/pytorch/trainer/trainer.py", line 974, in _run
    call._call_setup_hook(self)  # allow user to set up LightningModule in accelerator environment
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/lightning/pytorch/trainer/call.py", line 108, in _call_setup_hook
    _call_callback_hooks(trainer, "setup", stage=fn)
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/lightning/pytorch/trainer/call.py", line 227, in _call_callback_hooks
    fn(trainer, trainer.lightning_module, *args, **kwargs)
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/lightning/pytorch/cli.py", line 275, in setup
    self.parser.save(
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_deprecated.py", line 159, in patched_save
    return self._unpatched_save(cfg, *args, multifile=multifile, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_core.py", line 911, in save
    f.write(self.dump(cfg, **dump_kwargs))  # type: ignore[arg-type]
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_deprecated.py", line 150, in patched_dump
    return self._unpatched_dump(cfg, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_core.py", line 792, in dump
    self.validate(cfg)
  File "/home/lorenzo/miniconda3/envs/deepfake_benchmark_cu124/lib/python3.12/site-packages/jsonargparse/_core.py", line 1155, in validate
    raise type(ex)(message) from ex
TypeError: Parser key "data":
  Does not validate against any of the Union subtypes
  Subtypes: [<class 'NoneType'>, <class 'lightning.pytorch.core.datamodule.LightningDataModule'>]
  Errors:
    - Expected a <class 'NoneType'>
    - Problem with given class_path 'lightning_data_modules.AIGenBenchPipeline':
        Validation failed: Key "sliding_windows_definition.benchmark_type" is required but not included in config object or its value is None.
  Given value type: <class 'jsonargparse._namespace.Namespace'>
  Given value: Namespace(class_path='lightning_data_modules.AIGenBenchPipeline', init_args=Namespace(dataset_loader=Namespace(class_path='lightning_data_modules.datasets.AIGenBenchDatasetLoader', init_args=Namespace(dataset_path='/home/lorenzo/deepfake_fast/deepfake_benchmark_raw/')), train_batch_size=512, eval_batch_size=512, num_workers=8, train_subset_size=0.2, validation_subset_size=0.2, move_data_to_local_storage=False, deterministic_augmentations=True, augmentation_factor=4, augmentations_base_seed=4321, maximum_processing_size=1080))
```
</details>

Pull request:
Fixed regression causing dump/save to fail when skip_link_targets=True and target being an entire required dataclass
## What does this PR do?

Fixes #714

## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)?
- [n/a] Did you update **the documentation**? (readme and public docstrings)
- [x] Did you write **unit tests** such that there is 100% coverage on related code? (required for bug fixes and new features)
- [x] Did you verify that new and existing **tests pass locally**?
- [x] Did you make sure that all changes preserve **backward compatibility**?
- [x] Did you update **the CHANGELOG**? (not for typos, docs, test updates, or minor internal changes/refactors)


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 0b1ebe3..25a077f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -19,6 +19,9 @@ Fixed
 ^^^^^
 - ``set_parsing_settings(validate_defaults=True)`` fails when the parser has a
   config action (`#718 <https://github.com/omni-us/jsonargparse/pull/718>`__).
+- Regression causing dump/save to fail when ``skip_link_targets=True`` and target
+  being an entire required dataclass (`#717
+  <https://github.com/omni-us/jsonargparse/pull/717>`__).
 
 
 v4.39.0 (2025-04-29)
diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py
index 2614144..6c3f252 100644
--- a/jsonargparse/_core.py
+++ b/jsonargparse/_core.py
@@ -786,13 +786,14 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
         check_valid_dump_format(format)
 
         cfg = strip_meta(cfg)
-        if skip_link_targets:
-            ActionLink.strip_link_target_keys(self, cfg)
 
         with parser_context(load_value_mode=self.parser_mode):
             if not skip_validation:
                 self.validate(cfg)
 
+            if skip_link_targets:
+                ActionLink.strip_link_target_keys(self, cfg)
+
             dump_kwargs = {"skip_validation": skip_validation, "skip_none": skip_none}
             self._dump_cleanup_actions(cfg, self._actions, dump_kwargs)
 
@@ -826,7 +827,7 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
             if isinstance(action, ActionTypeHint):
                 value = cfg.get(action_dest)
                 if value is not None:
-                    with parser_context(parent_parser=self):
+                    with parser_context(parent_parser=self, lenient_check=True):
                         if dump_kwargs.get("skip_validation"):
                             with suppress(ValueError):
                                 value = action.serialize(value, dump_kwargs=dump_kwargs)
@@ -914,12 +915,13 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
 
         else:
             cfg = cfg.clone()
-            ActionLink.strip_link_target_keys(self, cfg)
 
             if not skip_validation:
                 with parser_context(load_value_mode=self.parser_mode):
                     self.validate(strip_meta(cfg), branch=branch)
 
+            ActionLink.strip_link_target_keys(self, cfg)
+
             def save_paths(cfg):
                 for key in cfg.get_sorted_keys():
                     val = cfg[key]


Test patch:
diff --git a/jsonargparse_tests/test_link_arguments.py b/jsonargparse_tests/test_link_arguments.py
index a0e4361..0be7cf0 100644
--- a/jsonargparse_tests/test_link_arguments.py
+++ b/jsonargparse_tests/test_link_arguments.py
@@ -442,6 +442,21 @@ def test_on_parse_save_required_target_subclass_param(parser, tmp_cwd):
     assert saved == {"a": {"a": 1}, "c": {"class_path": f"{__name__}.RequiredTargetC", "init_args": {}}}
 
 
+def test_on_parse_save_required_target_entire_dataclass(parser, tmp_cwd):
+    parser.add_class_arguments(RequiredTargetA, "a")
+    parser.add_subclass_arguments(RequiredTargetC, "c")
+    parser.link_arguments("a", "c.init_args.b")
+    cfg = parser.parse_args(["--a.a=1", f"--c={__name__}.RequiredTargetC"])
+    expected = {"a": {"a": 1}, "c": {"class_path": f"{__name__}.RequiredTargetC"}}
+    parser.save(cfg, "config.yaml", multifile=False)
+    saved = json_or_yaml_load((tmp_cwd / "config.yaml").read_text())
+    assert saved == expected
+    (tmp_cwd / "config.yaml").unlink()
+    parser.save(cfg, "config.yaml", multifile=True)
+    saved = json_or_yaml_load((tmp_cwd / "config.yaml").read_text())
+    assert saved == expected
+
+
 class Optimizer:
     def __init__(self, params: List[int], lr: float):
         self.params = params