Repository: omni-us/jsonargparse

Issue:
Unable to correctly link two deep level arguments sharing the same root class
I've been working on trying to make this code function in a clean and logical manner, but despite multiple attempts, I'm uncertain whether it's feasible or not, and could not find a similar example online.
I've come across a few issues related to similar challenges, but I haven't been able to achieve the desired behavior with my code.

Thanks!

**To Reproduce**
Use this `config.yaml`:
```yaml
model:
  class_path: __main__.LitDetector
  init_args:
    detector:
      class_path: __main__.DetectionModel
      init_args:
        encoder:
          class_path: __main__.Encoder
          init_args:
            model_name: modelA
        decoder:
          class_path: __main__.Decoder
          init_args:
            output_channels: 256
        det_const: 10
    lit_const: 12
```

and this code:
```python
from jsonargparse import CLI
from jsonargparse import ArgumentParser

class Encoder():
    def __init__(self, model_name: str):
        self.output_channels = dict(modelA=16,
                                    modelB=32)[model_name]

class Decoder():
    def __init__(self,
                 input_channels: int,
                 output_channels: int = 256):
        assert input_channels is not None, "Linking Failed"
        self.input_channels = input_channels
        self.output_channels = output_channels

class DetectionModel():
    def __init__(self,
                 encoder: Encoder,
                 decoder: Decoder,
                 det_const: int = 10) -> None:
        self.encoder = encoder
        self.decoder = decoder
        self.det_const = det_const

class LitDetector:
    def __init__(self,
                 detector: DetectionModel,
                 lit_const: int = 12) -> None:
        self.detector = detector
        self.lit_const = lit_const

class DetectorCLI:
    def __init__(self, model: LitDetector) -> None:
        self.model = model
        self.data = None
        print('>>> Done.')

if __name__ == '__main__':
    parser = ArgumentParser(prog='nested_classes_example',
                            description='Example of nested classes')
    parser.link_arguments('model.detector.encoder.output_channels',
                          'model.init_args.detector.init_args.decoder.init_args.input_channels',
                          apply_on='instantiate')
    args = parser.parse_args()
    CLI(DetectorCLI)
```

**Expected Behavior**
The goal is to successfully init `LitDetector`. For that we need to init `Encoder` and `Decoder` while the second is dependent in the first. Enough information is supplied in order to init the first. After the `Encoder` had been initiated we can deduce the relevant information for the initiation of the `Decoder`, using `link_arguments`.

For the clarity, In most attempts it fails before executing the "assert" line.

**Environment**
- jsonargparse version: 4.21.2
- Python version: 3.11.3
- How jsonargparse was installed: pip3 install jsonargparse[signatures]
- OS: Mac

Discussion:
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=412)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=412&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=412) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=412&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=412&metric=new_duplicated_lines_density&view=list)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412?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:
> Comparison is base [(`b571fc1`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/b571fc1ad8da6016b8bcfe216524eddaf16fb470?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) 100.00% compared to head [(`d315ea2`)](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) 100.00%.

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


```diff
@@            Coverage Diff            @@
##              main      #412   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           21        21           
  Lines         5892      5919   +27     
=========================================
+ Hits          5892      5919   +27     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/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/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.24% <100.00%> (+0.05%)` | :arrow_up: |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/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.10_pydantic1](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.30% <38.77%> (+0.01%)` | :arrow_up: |
| [py3.10_pydantic2](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.03% <38.77%> (+0.01%)` | :arrow_up: |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/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/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.11_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/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/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.81% <100.00%> (+0.04%)` | :arrow_up: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.48% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.47% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.96% <100.00%> (+0.05%)` | :arrow_up: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.51% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.49% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.81% <100.00%> (+0.05%)` | :arrow_up: |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/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.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412/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: |

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.

| [Files](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [jsonargparse/\_common.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19jb21tb24ucHk=) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/\_core.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19jb3JlLnB5) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/\_link\_arguments.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19saW5rX2FyZ3VtZW50cy5weQ==) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/\_typehints.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL190eXBlaGludHMucHk=) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/412?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/CHANGELOG.rst b/CHANGELOG.rst
index 18b805f..b9ee514 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -20,6 +20,8 @@ Fixed
 - Failure to parse subclass added via add_argument and required arg as link
   target.
 - ``choices`` working incorrectly when ``nargs`` is ``+``, ``*`` or number.
+- Unable link two deep level arguments sharing the same root class (`#297
+  <https://github.com/omni-us/jsonargparse/issues/297>`__).
 
 
 v4.26.1 (2023-10-23)
diff --git a/jsonargparse/_common.py b/jsonargparse/_common.py
index da59bce..9c17c7a 100644
--- a/jsonargparse/_common.py
+++ b/jsonargparse/_common.py
@@ -3,7 +3,7 @@ import inspect
 import sys
 from contextlib import contextmanager
 from contextvars import ContextVar
-from typing import Dict, Generic, Optional, Tuple, Type, TypeVar, Union, _GenericAlias  # type: ignore
+from typing import Dict, Generic, List, Optional, Tuple, Type, TypeVar, Union, _GenericAlias  # type: ignore
 
 from ._namespace import Namespace
 from ._type_checking import ArgumentParser
@@ -31,6 +31,7 @@ defaults_cache: ContextVar[Optional[Namespace]] = ContextVar("defaults_cache", d
 lenient_check: ContextVar[Union[bool, str]] = ContextVar("lenient_check", default=False)
 load_value_mode: ContextVar[Optional[str]] = ContextVar("load_value_mode", default=None)
 class_instantiators: ContextVar[Optional[InstantiatorsDictType]] = ContextVar("class_instantiators")
+nested_links: ContextVar[List[dict]] = ContextVar("nested_links", default=[])
 
 
 parser_context_vars = dict(
@@ -40,6 +41,7 @@ parser_context_vars = dict(
     lenient_check=lenient_check,
     load_value_mode=load_value_mode,
     class_instantiators=class_instantiators,
+    nested_links=nested_links,
 )
 
 
diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py
index 43cc172..0cad3fb 100644
--- a/jsonargparse/_core.py
+++ b/jsonargparse/_core.py
@@ -955,7 +955,7 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
             ):
                 cfg[action.dest] = recreate_branches(action.default)
 
-        self._logger.debug("Loaded default values from parser")
+        self._logger.debug("Loaded parser defaults: %s", cfg)
 
         default_config_files = self._get_default_config_files()
         for key, default_config_file in default_config_files:
@@ -1165,7 +1165,11 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
                     pass
                 else:
                     if value is not None:
-                        with parser_context(parent_parser=self, class_instantiators=self._get_instantiators()):
+                        with parser_context(
+                            parent_parser=self,
+                            nested_links=ActionLink.get_nested_links(self, component),
+                            class_instantiators=self._get_instantiators(),
+                        ):
                             parent[key] = component.instantiate_classes(value)
             else:
                 with parser_context(load_value_mode=self.parser_mode, class_instantiators=self._get_instantiators()):
diff --git a/jsonargparse/_link_arguments.py b/jsonargparse/_link_arguments.py
index 831291b..a54acc7 100644
--- a/jsonargparse/_link_arguments.py
+++ b/jsonargparse/_link_arguments.py
@@ -118,13 +118,14 @@ class ActionLink(Action):
         if not hasattr(parser, "_links_group"):
             parser._links_group = parser.add_argument_group("Linked arguments")
         self.parser = parser
+        self._target = target
+        self._source = source = (source,) if isinstance(source, str) else source
         self.apply_on = apply_on
         self.compute_fn = compute_fn
         self._initial_input_checks(source, target)
 
         # Set and check source actions or group
         exclude = (ActionLink, _ActionConfigLoad, _ActionSubCommands, ActionConfigFile)
-        source = (source,) if isinstance(source, str) else source
         if apply_on == "instantiate":
             self.source = [(s, find_subclass_action_or_class_group(parser, s, exclude=exclude)) for s in source]
             for key, action in self.source:
@@ -219,13 +220,21 @@ class ActionLink(Action):
             help=help_str,
         )
 
+    def get_kwargs(self) -> dict:
+        return {
+            "source": self._source,
+            "target": self._target,
+            "apply_on": self.apply_on,
+            "compute_fn": self.compute_fn,
+        }
+
     def _initial_input_checks(self, source, target):
         # Check apply_on
         if self.apply_on not in {"parse", "instantiate"}:
             raise ValueError("apply_on must be 'parse' or 'instantiate'.")
 
         # Check compute function
-        if self.compute_fn is None and not isinstance(source, str):
+        if self.compute_fn is None and not (isinstance(source, str) or len(source) == 1):
             raise ValueError("Multiple source keys requires a compute function.")
 
         if self.apply_on == "parse":
@@ -266,21 +275,19 @@ class ActionLink(Action):
             ActionLink.apply_parsing_links(subparser, cfg[subcommand])  # type: ignore
         if not hasattr(parser, "_links_group"):
             return
-        for action in parser._links_group._group_actions:
-            if action.apply_on != "parse":
-                continue
+        for action in get_link_actions(parser, "parse"):
             from ._typehints import ActionTypeHint
 
             args = []
             skip_link = False
             for source_key, source_action in action.source:
-                if ActionTypeHint.is_subclass_typehint(source_action[0]) and source_key not in cfg:
+                if ActionTypeHint.is_subclass_typehint(source_action[0]) and source_key not in cfg:  # type: ignore
                     parser.logger.debug(
                         f"Link '{action.option_strings[0]}' ignored since source '{source_key}' not found in namespace."
                     )
                     skip_link = True
                     break
-                for source_action_n in [a for a in source_action if a.dest in cfg]:
+                for source_action_n in [a for a in source_action if a.dest in cfg]:  # type: ignore
                     parser._check_value_key(source_action_n, cfg[source_action_n.dest], source_action_n.dest, None)
                 args.append(cfg[source_key])
             if skip_link:
@@ -309,6 +316,7 @@ class ActionLink(Action):
                 # Compute value
                 value = action.call_compute_fn(args)
             ActionLink.set_target_value(action, value, cfg, parser.logger)
+            parser.logger.debug(f"Applied link '{action.option_strings[0]}'.")
 
     @staticmethod
     def apply_instantiation_links(parser, cfg, target=None, order=None):
@@ -317,14 +325,15 @@ class ActionLink(Action):
 
         applied_key = "__applied_instantiation_links__"
         applied_links = cfg.pop(applied_key) if applied_key in cfg else set()
-        link_actions = [
-            a for a in parser._links_group._group_actions if a.apply_on == "instantiate" and a not in applied_links
-        ]
+        link_actions = get_link_actions(parser, "instantiate", skip=applied_links)
         if order and link_actions:
             link_actions = ActionLink.reorder(order, link_actions)
 
         for action in link_actions:
-            if not (order or action.target[0] == target or action.target[0].startswith(target + ".")):
+            target_key = action.target[0]
+            if not (
+                order or target_key == target or target_key.startswith(f"{target}.")
+            ) or is_nested_instantiation_link(action):
                 continue
             source_objects = []
             for source_key, source_action in action.source:
@@ -350,10 +359,25 @@ class ActionLink(Action):
                 value = action.call_compute_fn(source_objects)
             ActionLink.set_target_value(action, value, cfg, parser.logger)
             applied_links.add(action)
+            parser.logger.debug(f"Applied link '{action.option_strings[0]}'.")
 
         if target:
             cfg[applied_key] = applied_links
 
+    @staticmethod
+    def get_nested_links(parser, action):
+        def trim_param_keys(params: dict):
+            params = params.copy()
+            params["source"] = tuple(k[len(f"{action.dest}.") :] for k in params["source"])
+            params["target"] = params["target"][len(f"{action.dest}.init_args.") :]
+            return params
+
+        links = []
+        for link in get_link_actions(parser, "instantiate"):
+            if link.target[1] is action and is_nested_instantiation_link(link):
+                links.append(trim_param_keys(link.get_kwargs()))
+        return links
+
     @staticmethod
     def set_target_value(action: "ActionLink", value: Any, cfg: Namespace, logger) -> None:
         target_key, target_action = action.target
@@ -378,15 +402,14 @@ class ActionLink(Action):
 
     @staticmethod
     def instantiation_order(parser):
-        if hasattr(parser, "_links_group"):
-            actions = [a for a in parser._links_group._group_actions if a.apply_on == "instantiate"]
-            if len(actions) > 0:
-                graph = DirectedGraph()
-                for action in actions:
-                    target = re.sub(r"\.init_args$", "", split_key_leaf(action.target[0])[0])
-                    for _, source_action in action.source:
-                        graph.add_edge(source_action.dest, target)
-                return graph.get_topological_order()
+        actions = get_link_actions(parser, "instantiate")
+        if actions:
+            graph = DirectedGraph()
+            for action in actions:
+                target = re.sub(r"\.init_args$", "", split_key_leaf(action.target[0])[0])
+                for _, source_action in action.source:
+                    graph.add_edge(source_action.dest, target)
+            return graph.get_topological_order()
         return []
 
     @staticmethod
@@ -428,6 +451,25 @@ class ActionLink(Action):
                     ActionLink.strip_link_target_keys(subparsers[num], cfg[subcommand])
 
 
+def get_link_actions(parser: "ArgumentParser", apply_on: str, skip=set()) -> List[ActionLink]:
+    if not hasattr(parser, "_links_group"):
+        return []
+    return [a for a in parser._links_group._group_actions if a.apply_on == apply_on and a not in skip]
+
+
+def is_nested_instantiation_link(action: ActionLink) -> bool:
+    from ._typehints import ActionTypeHint
+
+    target_key, target_action = action.target
+    assert target_action
+    return (
+        target_key.startswith(f"{target_action.dest}.init_args.")
+        and ActionTypeHint.is_subclass_typehint(target_action)
+        and all(a is target_action for _, a in action.source)
+        and all(k.startswith(f"{target_action.dest}.") for k, _ in action.source)
+    )
+
+
 class ArgumentLinking:
     """Method for linking arguments."""
 
diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py
index b6e31ff..0cd7afe 100644
--- a/jsonargparse/_typehints.py
+++ b/jsonargparse/_typehints.py
@@ -41,7 +41,7 @@ from ._actions import (
     _is_action_value_list,
     remove_actions,
 )
-from ._common import get_class_instantiator, is_dataclass_like, is_subclass, parent_parser, parser_context
+from ._common import get_class_instantiator, is_dataclass_like, is_subclass, nested_links, parent_parser, parser_context
 from ._loaders_dumpers import (
     get_loader_exceptions,
     load_value,
@@ -528,10 +528,15 @@ class ActionTypeHint(Action):
         parser = type(parser)(exit_on_error=False, logger=parser.logger)
         remove_actions(parser, (ActionConfigFile, _ActionPrintConfig))
         parser.add_class_arguments(val_class, **kwargs)
+
         if "linked_targets" in kwargs and parser.required_args:
             for key in kwargs["linked_targets"]:
                 if key in parser.required_args:
                     parser.required_args.remove(key)
+
+        for link_kwargs in nested_links.get():
+            parser.link_arguments(**link_kwargs)
+
         return parser
 
     def extra_help(self):


Test patch:
diff --git a/jsonargparse_tests/test_link_arguments.py b/jsonargparse_tests/test_link_arguments.py
index 6096745..53b44a5 100644
--- a/jsonargparse_tests/test_link_arguments.py
+++ b/jsonargparse_tests/test_link_arguments.py
@@ -669,6 +669,103 @@ def test_on_instantiate_add_argument_subclass_required_params(parser):
     assert init.cls2.a == 1
 
 
+class WithinDeepSource:
+    def __init__(self, model_name: str):
+        self.output_channels = dict(
+            modelA=16,
+            modelB=32,
+        )[model_name]
+
+
+class WithinDeepTarget:
+    def __init__(self, input_channels: int):
+        self.input_channels = input_channels
+
+
+class WithinDeepModel:
+    def __init__(
+        self,
+        encoder: WithinDeepSource,
+        decoder: WithinDeepTarget,
+    ):
+        self.encoder = encoder
+        self.decoder = decoder
+
+
+within_deep_config = {
+    "model": {
+        "class_path": f"{__name__}.WithinDeepModel",
+        "init_args": {
+            "encoder": {
+                "class_path": f"{__name__}.WithinDeepSource",
+                "init_args": {
+                    "model_name": "modelA",
+                },
+            },
+            "decoder": {
+                "class_path": f"{__name__}.WithinDeepTarget",
+            },
+        },
+    },
+}
+
+
+def test_on_instantiate_within_deep_subclass(parser, caplog):
+    parser.logger = {"level": "DEBUG"}
+    parser.logger.handlers = [caplog.handler]
+
+    parser.add_argument("--cfg", action=ActionConfigFile)
+    parser.add_argument("--model", type=WithinDeepModel)
+    parser.link_arguments(
+        "model.encoder.output_channels",
+        "model.init_args.decoder.init_args.input_channels",
+        apply_on="instantiate",
+    )
+
+    cfg = parser.parse_args([f"--cfg={within_deep_config}"])
+    init = parser.instantiate_classes(cfg)
+    assert isinstance(init.model, WithinDeepModel)
+    assert isinstance(init.model.encoder, WithinDeepSource)
+    assert isinstance(init.model.decoder, WithinDeepTarget)
+    assert init.model.decoder.input_channels == 16
+    assert "Applied link 'encoder.output_channels --> decoder.init_args.input_channels'" in caplog.text
+
+
+class WithinDeeperSystem:
+    def __init__(self, model: WithinDeepModel):
+        self.model = model
+
+
+within_deeper_config = {
+    "system": {
+        "class_path": f"{__name__}.WithinDeeperSystem",
+        "init_args": within_deep_config,
+    },
+}
+
+
+def test_on_instantiate_within_deeper_subclass(parser, caplog):
+    parser.logger = {"level": "DEBUG"}
+    parser.logger.handlers = [caplog.handler]
+
+    parser.add_argument("--cfg", action=ActionConfigFile)
+    parser.add_subclass_arguments(WithinDeeperSystem, "system")
+    parser.link_arguments(
+        "system.model.encoder.output_channels",
+        "system.init_args.model.init_args.decoder.init_args.input_channels",
+        apply_on="instantiate",
+    )
+
+    cfg = parser.parse_args([f"--cfg={within_deeper_config}"])
+    init = parser.instantiate_classes(cfg)
+    assert isinstance(init.system, WithinDeeperSystem)
+    assert isinstance(init.system.model, WithinDeepModel)
+    assert isinstance(init.system.model.encoder, WithinDeepSource)
+    assert isinstance(init.system.model.decoder, WithinDeepTarget)
+    assert init.system.model.decoder.input_channels == 16
+    assert "Applied link 'encoder.output_channels --> decoder.init_args.input_channels'" in caplog.text
+
+
 # link creation failures