Repository: gruntwork-io/terragrunt (language: go)

Issue:
Deep merge when using `mock_outputs_merge_with_state` in a `dependency` block
Following from https://github.com/gruntwork-io/terragrunt/pull/1765 , it's possible to merge the existing (applied) state into the mocks (state overwrites mocks). However, this is only a shallow merge.

Let's say you have a module that outputs a `map(map(string))`. An existing output of that module could be

```json
{
  "security_groups": {
    "sensitive": false,
    "type": [
      "object",
      {
        "first": [
          "object",
          {
            "arn": "string",
            "description": "string",
            "id": "string",
            "name": "string"
          }
        ]
      }
    ],
    "value": {
      "first": {
        "arn": "arn:aws:ec2:ap-southeast-1:000000000000:security-group/sg-aaaaaaaaaaaaaaaaa",
        "description": "My first security group",
        "id": "sg-aaaaaaaaaaaaaaaaa",
        "name": "my-first-sg"
      }
    }
  }
}
```

and let's say the usage of that module is like so:

```hcl
dependency "security_groups" {
  config_path = "../security_groups"

  mock_outputs_allowed_terraform_commands = ["validate", "plan", "state"]
  mock_outputs_merge_with_state = true
  mock_outputs = {
    security_groups = {
      first = { id = "sg-mock" }
    }
  }
}

inputs = {
  something = [
    dependency.security_groups.outputs.security_groups.first.id
  ]
}
```

If I were to change the inputs of the dependency (the security_groups module), which adds an additional value to the output map, so that it becomes

```diff
{
  "security_groups": {
    "sensitive": false,
    "type": [
      "object",
      {
        "first": [
          "object",
          {
            "arn": "string",
            "description": "string",
            "id": "string",
            "name": "string"
          }
        ],
+        "second": [
+          "object",
+          {
+            "arn": "string",
+            "description": "string",
+            "id": "string",
+            "name": "string"
+          }
+        ]
      }
    ],
    "value": {
      "first": {
        "arn": "arn:aws:ec2:ap-southeast-1:000000000000:security-group/sg-aaaaaaaaaaaaaaaaa",
        "description": "My first security group",
        "id": "sg-aaaaaaaaaaaaaaaaa",
        "name": "my-first-sg"
      },
+      "second": {
+        "arn": "arn:aws:ec2:ap-southeast-1:000000000000:security-group/sg-bbbbbbbbbbbbbbbbb",
+        "description": "My second security group",
+        "id": "sg-bbbbbbbbbbbbbbbbb",
+        "name": "my-second-sg"
+      }
    }
  }
}
```

and attempt to use it BEFORE the updated dependency has been applied

```diff
dependency "security_groups" {
  config_path = "../security_groups"

  mock_outputs_allowed_terraform_commands = ["validate", "plan", "state"]
  mock_outputs_merge_with_state = true
  mock_outputs = {
    security_groups = {
      first = { id = "sg-mock" },
+      second = { id = "sg-mock" }
    }
  }
}

inputs = {
  something = [
    dependency.security_groups.outputs.security_groups.first.id,
+    dependency.security_groups.outputs.security_groups.second.id
  ]
}
```

We'll receive the following error from terragrunt when trying to plan

```
Unsupported attribute; This object does not have an attribute named "second".
```

This is due to the logic in `getTerragruntOutputIfAppliedElseConfiguredDefault` only checking the output key and not supporting a deep merge of the output values https://github.com/gruntwork-io/terragrunt/blob/cc1927746e8883322a0b37394887db4d8d6810ae/config/dependency.go#L322-L327

Would you be open to a PR or discussion on adding support for deep merging the outputs into the mock?


Discussion / hints:
Allowing deep merge of mocks to state makes sense to me, and a PR to add it would be welcome. I think this probably makes the most sense as a feature flag similar to `merge_strategy` we have on `include` blocks.

I would probably add a new config `mock_outputs_merge_strategy_with_state` that replaces `mock_outputs_merge_with_state`, and can be set to `no_merge`, `shallow`, or `deep`, with `no_merge` being the default. We could also maintain backward compatibility by having `mock_outputs_merge_with_state = true` mean `mock_outputs_merge_strategy_with_state = "shallow"`.

Fix patch:
diff --git a/config/config.go b/config/config.go
index 2330adddcc..fc34007e70 100644
--- a/config/config.go
+++ b/config/config.go
@@ -246,6 +246,8 @@ func (cfg *IncludeConfig) GetMergeStrategy() (MergeStrategyType, error) {
 		return ShallowMerge, nil
 	case string(DeepMerge):
 		return DeepMerge, nil
+	case string(DeepMergeMapOnly):
+		return DeepMergeMapOnly, nil
 	default:
 		return NoMerge, errors.WithStackTrace(InvalidMergeStrategyType(strategy))
 	}
@@ -254,9 +256,10 @@ func (cfg *IncludeConfig) GetMergeStrategy() (MergeStrategyType, error) {
 type MergeStrategyType string
 
 const (
-	NoMerge      MergeStrategyType = "no_merge"
-	ShallowMerge MergeStrategyType = "shallow"
-	DeepMerge    MergeStrategyType = "deep"
+	NoMerge          MergeStrategyType = "no_merge"
+	ShallowMerge     MergeStrategyType = "shallow"
+	DeepMerge        MergeStrategyType = "deep"
+	DeepMergeMapOnly MergeStrategyType = "deep_map_only"
 )
 
 // ModuleDependencies represents the paths to other Terraform modules that must be applied before the current module
@@ -1057,11 +1060,12 @@ type InvalidMergeStrategyType string
 
 func (err InvalidMergeStrategyType) Error() string {
 	return fmt.Sprintf(
-		"Include merge strategy %s is unknown. Valid strategies are: %s, %s, %s",
+		"Include merge strategy %s is unknown. Valid strategies are: %s, %s, %s, %s",
 		string(err),
 		NoMerge,
 		ShallowMerge,
 		DeepMerge,
+		DeepMergeMapOnly,
 	)
 }
 
diff --git a/config/cty_helpers.go b/config/cty_helpers.go
index a1b9583d6a..7fe4a4947f 100644
--- a/config/cty_helpers.go
+++ b/config/cty_helpers.go
@@ -108,10 +108,38 @@ func ctySliceToStringSlice(args []cty.Value) ([]string, error) {
 	return out, nil
 }
 
-// deepMergeCtyMaps implements a deep merge of two cty value objects. We can't directly merge two cty.Value objects, so
+// shallowMergeCtyMaps performs a shallow merge of two cty value objects.
+func shallowMergeCtyMaps(target cty.Value, source cty.Value) (*cty.Value, error) {
+	outMap, err := parseCtyValueToMap(target)
+	if err != nil {
+		return nil, err
+	}
+	SourceMap, err := parseCtyValueToMap(source)
+	if err != nil {
+		return nil, err
+	}
+
+	for key, sourceValue := range SourceMap {
+		if _, ok := outMap[key]; !ok {
+			outMap[key] = sourceValue
+		}
+	}
+
+	outCty, err := convertToCtyWithJson(outMap)
+	if err != nil {
+		return nil, err
+	}
+	return &outCty, nil
+}
+
+func deepMergeCtyMaps(target cty.Value, source cty.Value) (*cty.Value, error) {
+	return deepMergeCtyMapsMapOnly(target, source, mergo.WithAppendSlice)
+}
+
+// deepMergeCtyMapsMapOnly implements a deep merge of two cty value objects. We can't directly merge two cty.Value objects, so
 // we cheat by using map[string]interface{} as an intermediary. Note that this assumes the provided cty value objects
 // are already maps or objects in HCL land.
-func deepMergeCtyMaps(target cty.Value, source cty.Value) (*cty.Value, error) {
+func deepMergeCtyMapsMapOnly(target cty.Value, source cty.Value, opts ...func(*mergo.Config)) (*cty.Value, error) {
 	outMap := make(map[string]interface{})
 	targetMap, err := parseCtyValueToMap(target)
 	if err != nil {
@@ -126,7 +154,7 @@ func deepMergeCtyMaps(target cty.Value, source cty.Value) (*cty.Value, error) {
 		outMap[key] = val
 	}
 
-	if err := mergo.Merge(&outMap, sourceMap, mergo.WithAppendSlice, mergo.WithOverride); err != nil {
+	if err := mergo.Merge(&outMap, sourceMap, append(opts, mergo.WithOverride)...); err != nil {
 		return nil, err
 	}
 
diff --git a/config/dependency.go b/config/dependency.go
index cd072bc8ac..a4ee08e7d8 100644
--- a/config/dependency.go
+++ b/config/dependency.go
@@ -35,7 +35,10 @@ type Dependency struct {
 	SkipOutputs                         *bool      `hcl:"skip_outputs,attr" cty:"skip"`
 	MockOutputs                         *cty.Value `hcl:"mock_outputs,attr" cty:"mock_outputs"`
 	MockOutputsAllowedTerraformCommands *[]string  `hcl:"mock_outputs_allowed_terraform_commands,attr" cty:"mock_outputs_allowed_terraform_commands"`
-	MockOutputsMergeWithState           *bool      `hcl:"mock_outputs_merge_with_state,attr" cty:"mock_outputs_merge_with_state"`
+
+	// MockOutputsMergeWithState is deprecated. Use MockOutputsMergeStrategyWithState
+	MockOutputsMergeWithState         *bool              `hcl:"mock_outputs_merge_with_state,attr" cty:"mock_outputs_merge_with_state"`
+	MockOutputsMergeStrategyWithState *MergeStrategyType `hcl:"mock_outputs_merge_strategy_with_state" cty:"mock_outputs_merge_strategy_with_state"`
 
 	// Used to store the rendered outputs for use when the config is imported or read with `read_terragrunt_config`
 	RenderedOutputs *cty.Value `cty:"outputs"`
@@ -81,6 +84,22 @@ func (targetDepConfig *Dependency) DeepMerge(sourceDepConfig Dependency) error {
 	return nil
 }
 
+// getMockOutputsMergeStrategy returns the MergeStrategyType following the deprecation of mock_outputs_merge_with_state
+// - If mock_outputs_merge_strategy_with_state is not null. The value of mock_outputs_merge_strategy_with_state will be returned
+// - If mock_outputs_merge_strategy_with_state is null and mock_outputs_merge_with_state is not null:
+//   - mock_outputs_merge_with_state being true returns ShallowMerge
+//   - mock_outputs_merge_with_state being false returns NoMerge
+func (dependencyConfig Dependency) getMockOutputsMergeStrategy() MergeStrategyType {
+	if dependencyConfig.MockOutputsMergeStrategyWithState == nil {
+		if dependencyConfig.MockOutputsMergeWithState != nil && (*dependencyConfig.MockOutputsMergeWithState) {
+			return ShallowMerge
+		} else {
+			return NoMerge
+		}
+	}
+	return *dependencyConfig.MockOutputsMergeStrategyWithState
+}
+
 // Given a dependency config, we should only attempt to get the outputs if SkipOutputs is nil or false
 func (dependencyConfig Dependency) shouldGetOutputs() bool {
 	return dependencyConfig.SkipOutputs == nil || !(*dependencyConfig.SkipOutputs)
@@ -88,12 +107,11 @@ func (dependencyConfig Dependency) shouldGetOutputs() bool {
 
 // Given a dependency config, we should only attempt to merge mocks outputs with the outputs if MockOutputsMergeWithState is not nil or true
 func (dependencyConfig Dependency) shouldMergeMockOutputsWithState(terragruntOptions *options.TerragruntOptions) bool {
-	shouldMergeMocksAttr := dependencyConfig.MockOutputsMergeWithState != nil && (*dependencyConfig.MockOutputsMergeWithState)
 	allowedCommand :=
 		dependencyConfig.MockOutputsAllowedTerraformCommands == nil ||
 			len(*dependencyConfig.MockOutputsAllowedTerraformCommands) == 0 ||
 			util.ListContainsElement(*dependencyConfig.MockOutputsAllowedTerraformCommands, terragruntOptions.OriginalTerraformCommand)
-	return shouldMergeMocksAttr && allowedCommand
+	return allowedCommand && dependencyConfig.getMockOutputsMergeStrategy() != NoMerge
 }
 
 func (dependencyConfig *Dependency) setRenderedOutputs(terragruntOptions *options.TerragruntOptions) error {
@@ -301,7 +319,7 @@ func dependencyBlocksToCtyValue(dependencyConfigs []Dependency, terragruntOption
 // This will attempt to get the outputs from the target terragrunt config if it is applied. If it is not applied, the
 // behavior is different depending on the configuration of the dependency:
 // - If the dependency block indicates a mock_outputs attribute, this will return that.
-//   If the dependency block indicates a mock_outputs_merge_with_state attribute, mock_outputs and state outputs will be merged
+//   If the dependency block indicates a mock_outputs_merge_strategy_with_state attribute, mock_outputs and state outputs will be merged following the merge strategy
 // - If the dependency block does NOT indicate a mock_outputs attribute, this will return an error.
 func getTerragruntOutputIfAppliedElseConfiguredDefault(dependencyConfig Dependency, terragruntOptions *options.TerragruntOptions) (*cty.Value, error) {
 	if dependencyConfig.shouldGetOutputs() {
@@ -311,22 +329,18 @@ func getTerragruntOutputIfAppliedElseConfiguredDefault(dependencyConfig Dependen
 		}
 
 		if !isEmpty && dependencyConfig.shouldMergeMockOutputsWithState(terragruntOptions) {
-			stateOutputsMap, err := parseCtyValueToMap(*outputVal)
-			if err != nil {
-				return outputVal, err
-			}
-			mockOutputsMap, err := parseCtyValueToMap(*dependencyConfig.MockOutputs)
-			if err != nil {
-				return outputVal, err
+			mockMergeStrategy := dependencyConfig.getMockOutputsMergeStrategy()
+			switch mockMergeStrategy {
+			case NoMerge:
+				return outputVal, nil
+			case ShallowMerge:
+				return shallowMergeCtyMaps(*outputVal, *dependencyConfig.MockOutputs)
+			case DeepMergeMapOnly:
+				return deepMergeCtyMapsMapOnly(*dependencyConfig.MockOutputs, *outputVal)
+			default:
+				return nil, errors.WithStackTrace(InvalidMergeStrategyType(mockMergeStrategy))
 			}
-			for key, mockValue := range mockOutputsMap {
-				_, ok := stateOutputsMap[key]
-				if !ok {
-					stateOutputsMap[key] = mockValue
-				}
-			}
-			mergeOutputVal, err := convertToCtyWithJson(stateOutputsMap)
-			return &mergeOutputVal, err
+
 		} else if !isEmpty {
 			return outputVal, err
 		}
diff --git a/docs/_docs/02_features/execute-terraform-commands-on-multiple-modules-at-once.md b/docs/_docs/02_features/execute-terraform-commands-on-multiple-modules-at-once.md
index e3c30af6a0..369a33bf66 100644
--- a/docs/_docs/02_features/execute-terraform-commands-on-multiple-modules-at-once.md
+++ b/docs/_docs/02_features/execute-terraform-commands-on-multiple-modules-at-once.md
@@ -217,7 +217,7 @@ When `skip_outputs` is used with `mock_outputs`, mocked outputs will be returned
       skip_outputs = true
     }
 
-You can also use `mock_outputs_merge_with_state` on the `dependency` block to merge the mocked outputs and the state outputs :
+You can also use `mock_outputs_merge_strategy_with_state` on the `dependency` block to merge the mocked outputs and the state outputs :
 
     dependency "vpc" {
       config_path = "../vpc"
@@ -226,7 +226,7 @@ You can also use `mock_outputs_merge_with_state` on the `dependency` block to me
         new_output = "temporary-dummy-value"
       }
 
-      mock_outputs_merge_with_state = true
+      mock_outputs_merge_strategy_with_state = "shallow"
     }
 
 If the state outputs only contains `vpc_id`, this value will be preserved. And `new_output` which is not existing, the mock value will be used.
diff --git a/docs/_docs/04_reference/config-blocks-and-attributes.md b/docs/_docs/04_reference/config-blocks-and-attributes.md
index c91e5944da..a7d28a44db 100644
--- a/docs/_docs/04_reference/config-blocks-and-attributes.md
+++ b/docs/_docs/04_reference/config-blocks-and-attributes.md
@@ -887,8 +887,17 @@ The `dependency` block supports the following arguments:
 - `mock_outputs_allowed_terraform_commands` (attribute): A list of Terraform commands for which `mock_outputs` are
   allowed. If a command is used where `mock_outputs` is not allowed, and no outputs are available in the target module,
   Terragrunt will throw an error when processing this dependency.
-- `mock_outputs_merge_with_state` (attribute): When `true`, `mock_outputs` and the state outputs will be merged. That is,
-  the `mock_outputs` will be treated as defaults and the real state outputs will overwrite them if the keys clash.
+- `mock_outputs_merge_with_state` (attribute): DEPRECATED. Use `mock_outputs_merge_strategy_with_state`. When `true`, 
+  `mock_outputs` and the state outputs will be merged. That is, the `mock_outputs` will be treated as defaults and the
+  real state outputs will overwrite them if the keys clash.
+- `mock_outputs_merge_strategy_with_state` (attribute): Specifies how any existing state should be merged into the 
+  mocks. Valid values are
+  - `no_merge` (default) - any existing state will be used as is. If the dependency does not have an existing state (it 
+    hasn't been applied yet), then the mocks will be used
+  - `shallow` - the existing state will be shallow merged into the mocks. Mocks will only be used where the output does 
+    not already exist in the dependency's state
+  - `deep_map_only` - the existing state will be deeply merged into the mocks. If an output is a map, the mock key 
+    will be used where that key does not exist in the state. Lists will not be merged
 
 Example:
 
diff --git a/util/reflect.go b/util/reflect.go
index c45f841959..f487e2fa41 100644
--- a/util/reflect.go
+++ b/util/reflect.go
@@ -1,6 +1,9 @@
 package util
 
-import "reflect"
+import (
+	"reflect"
+	"strconv"
+)
 
 // Return the kind of the type or Invalid if value is nil
 func KindOf(value interface{}) reflect.Kind {
@@ -10,3 +13,51 @@ func KindOf(value interface{}) reflect.Kind {
 	}
 	return valueType.Kind()
 }
+
+// MustWalkTerraformOutput is a helper utility to deeply return a value from a terraform output.
+//   nil will be returned if the path is invalid
+//
+//   Using an example terraform output:
+//     a = {
+//       b = {
+//         c = "foo"
+//       }
+//       "d" = [
+//         1,
+//         2
+//       ]
+//     }
+//
+//   path ["a", "b", "c"] will return "foo"
+//   path ["a", "d", "1"] will return 2
+//   path ["a", "foo"] will return nil
+func MustWalkTerraformOutput(value interface{}, path ...string) interface{} {
+	if value == nil {
+		return nil
+	}
+	found := value
+	for _, p := range path {
+		v := reflect.ValueOf(found)
+		switch reflect.TypeOf(found).Kind() {
+		case reflect.Map:
+			if !v.MapIndex(reflect.ValueOf(p)).IsValid() {
+				return nil
+			}
+			found = v.MapIndex(reflect.ValueOf(p)).Interface()
+
+		case reflect.Slice, reflect.Array:
+			i, err := strconv.Atoi(p)
+			if err != nil {
+				return nil
+			}
+			if v.Len()-1 < i {
+				return nil
+			}
+			found = v.Index(i).Interface()
+
+		default:
+			return found
+		}
+	}
+	return found
+}


Test patch:
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/child/child/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/child/child/terraform.tfstate
new file mode 100644
index 0000000000..eda43ffef8
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/child/child/terraform.tfstate
@@ -0,0 +1,13 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "731d3bc7-9162-8495-b1ce-c64989417ba3",
+  "outputs": {
+    "test_output1_from_parent": {
+      "value": "value1",
+      "type": "string"
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/child/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/child/terragrunt.hcl
new file mode 100644
index 0000000000..5795a59a14
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/child/terragrunt.hcl
@@ -0,0 +1,35 @@
+include {
+  path = find_in_parent_folders()
+}
+
+dependency "x" {
+  config_path = "../parent"
+
+  mock_outputs_merge_with_state = "false"
+  mock_outputs_merge_strategy_with_state = "true"
+  mock_outputs_allowed_terraform_commands = ["plan", "apply", "output"]
+  mock_outputs = {
+    test_output1 = "fake-output1"
+    test_output_map_map_string = {
+      map_root1 = {
+        map_root1_sub1 = "fake-map_root1_sub1"
+      }
+      not_in_state = {
+        abc = "fake-abc"
+      }
+    }
+    test_output_list_string = ["fake-list-data"]
+  }
+}
+
+inputs = {
+  test_input1 = dependency.x.outputs.test_output1
+
+  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string
+
+  test_input_list_string = dependency.x.outputs.test_output_list_string
+}
+
+terraform {
+  source = "../..//modules/child"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/parent/parent/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/parent/parent/terraform.tfstate
new file mode 100644
index 0000000000..dd5a02ca17
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/parent/parent/terraform.tfstate
@@ -0,0 +1,31 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "b52d1b2c-9136-4a6d-a070-752044b76f57",
+  "outputs": {
+    "test_output1": {
+      "value": "value1",
+      "type": "string"
+    },
+    "test_output_map_map_string": {
+      "value": {
+        "map_root1": {
+          "map_root1_sub1": "map_root1_sub1_value"
+        }
+      },
+      "type": [
+        "object",
+        {
+          "map_root1": [
+            "object",
+            {
+              "map_root1_sub1": "string"
+            }
+          ]
+        }
+      ]
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/parent/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/parent/terragrunt.hcl
new file mode 100644
index 0000000000..13b0678135
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/parent/terragrunt.hcl
@@ -0,0 +1,7 @@
+include {
+  path = find_in_parent_folders()
+}
+
+terraform {
+  source = "../..//modules/parent"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/terragrunt.hcl
new file mode 100644
index 0000000000..e57313499b
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/terragrunt.hcl
@@ -0,0 +1,12 @@
+remote_state {
+  backend = "local"
+
+  generate = {
+    path      = "backend.tf"
+    if_exists = "overwrite_terragrunt"
+  }
+
+  config = {
+    path = "${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate"
+  }
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/modules/child/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/modules/child/main.tf
new file mode 100644
index 0000000000..f09da29030
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/modules/child/main.tf
@@ -0,0 +1,23 @@
+variable "test_input1" {
+  type = string
+}
+
+output "test_output1_from_parent" {
+  value = var.test_input1
+}
+
+variable "test_input_map_map_string" {
+  type = map(map(string))
+}
+
+output "test_output_map_map_string_from_parent" {
+  value = var.test_input_map_map_string
+}
+
+variable "test_input_list_string" {
+  type = list(string)
+}
+
+output "test_output_list_string" {
+  value = var.test_input_list_string
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/modules/parent/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/modules/parent/main.tf
new file mode 100644
index 0000000000..5c22ce00b9
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/modules/parent/main.tf
@@ -0,0 +1,22 @@
+output "test_output1" {
+  value = "value1"
+}
+
+output "test_output_map_map_string" {
+  value = {
+    map_root1 = {
+      map_root1_sub1 = "map_root1_sub1_value"
+    }
+    not_in_state = {
+      abc = "123"
+    }
+  }
+}
+
+output "test_output_list_string" {
+  value = [
+    "a",
+    "b",
+    "c"
+  ]
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/child/child/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/child/child/terraform.tfstate
new file mode 100644
index 0000000000..eda43ffef8
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/child/child/terraform.tfstate
@@ -0,0 +1,13 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "731d3bc7-9162-8495-b1ce-c64989417ba3",
+  "outputs": {
+    "test_output1_from_parent": {
+      "value": "value1",
+      "type": "string"
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/child/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/child/terragrunt.hcl
new file mode 100644
index 0000000000..1fb5f72a1c
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/child/terragrunt.hcl
@@ -0,0 +1,34 @@
+include {
+  path = find_in_parent_folders()
+}
+
+dependency "x" {
+  config_path = "../parent"
+
+  mock_outputs_merge_with_state = "false"
+  mock_outputs_allowed_terraform_commands = ["plan", "apply", "output"]
+  mock_outputs = {
+    test_output1 = "fake-output1"
+    test_output_map_map_string = {
+      map_root1 = {
+        map_root1_sub1 = "fake-map_root1_sub1"
+      }
+      not_in_state = {
+        abc = "fake-abc"
+      }
+    }
+    test_output_list_string = ["fake-list-data"]
+  }
+}
+
+inputs = {
+  test_input1 = dependency.x.outputs.test_output1
+
+  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string
+
+  test_input_list_string = dependency.x.outputs.test_output_list_string
+}
+
+terraform {
+  source = "../..//modules/child"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/parent/parent/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/parent/parent/terraform.tfstate
new file mode 100644
index 0000000000..dd5a02ca17
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/parent/parent/terraform.tfstate
@@ -0,0 +1,31 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "b52d1b2c-9136-4a6d-a070-752044b76f57",
+  "outputs": {
+    "test_output1": {
+      "value": "value1",
+      "type": "string"
+    },
+    "test_output_map_map_string": {
+      "value": {
+        "map_root1": {
+          "map_root1_sub1": "map_root1_sub1_value"
+        }
+      },
+      "type": [
+        "object",
+        {
+          "map_root1": [
+            "object",
+            {
+              "map_root1_sub1": "string"
+            }
+          ]
+        }
+      ]
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/parent/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/parent/terragrunt.hcl
new file mode 100644
index 0000000000..13b0678135
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/parent/terragrunt.hcl
@@ -0,0 +1,7 @@
+include {
+  path = find_in_parent_folders()
+}
+
+terraform {
+  source = "../..//modules/parent"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/terragrunt.hcl
new file mode 100644
index 0000000000..e57313499b
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/terragrunt.hcl
@@ -0,0 +1,12 @@
+remote_state {
+  backend = "local"
+
+  generate = {
+    path      = "backend.tf"
+    if_exists = "overwrite_terragrunt"
+  }
+
+  config = {
+    path = "${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate"
+  }
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/modules/child/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/modules/child/main.tf
new file mode 100644
index 0000000000..f09da29030
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/modules/child/main.tf
@@ -0,0 +1,23 @@
+variable "test_input1" {
+  type = string
+}
+
+output "test_output1_from_parent" {
+  value = var.test_input1
+}
+
+variable "test_input_map_map_string" {
+  type = map(map(string))
+}
+
+output "test_output_map_map_string_from_parent" {
+  value = var.test_input_map_map_string
+}
+
+variable "test_input_list_string" {
+  type = list(string)
+}
+
+output "test_output_list_string" {
+  value = var.test_input_list_string
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/modules/parent/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/modules/parent/main.tf
new file mode 100644
index 0000000000..5c22ce00b9
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/modules/parent/main.tf
@@ -0,0 +1,22 @@
+output "test_output1" {
+  value = "value1"
+}
+
+output "test_output_map_map_string" {
+  value = {
+    map_root1 = {
+      map_root1_sub1 = "map_root1_sub1_value"
+    }
+    not_in_state = {
+      abc = "123"
+    }
+  }
+}
+
+output "test_output_list_string" {
+  value = [
+    "a",
+    "b",
+    "c"
+  ]
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/child/child/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/child/child/terraform.tfstate
new file mode 100644
index 0000000000..eda43ffef8
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/child/child/terraform.tfstate
@@ -0,0 +1,13 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "731d3bc7-9162-8495-b1ce-c64989417ba3",
+  "outputs": {
+    "test_output1_from_parent": {
+      "value": "value1",
+      "type": "string"
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/child/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/child/terragrunt.hcl
new file mode 100644
index 0000000000..192aae218f
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/child/terragrunt.hcl
@@ -0,0 +1,34 @@
+include {
+  path = find_in_parent_folders()
+}
+
+dependency "x" {
+  config_path = "../parent"
+
+  mock_outputs_merge_with_state = "true"
+  mock_outputs_allowed_terraform_commands = ["plan", "apply", "output"]
+  mock_outputs = {
+    test_output1 = "fake-output1"
+    test_output_map_map_string = {
+      map_root1 = {
+        map_root1_sub1 = "fake-map_root1_sub1"
+      }
+      not_in_state = {
+        abc = "fake-abc"
+      }
+    }
+    test_output_list_string = ["fake-list-data"]
+  }
+}
+
+inputs = {
+  test_input1 = dependency.x.outputs.test_output1
+
+  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string
+
+  test_input_list_string = dependency.x.outputs.test_output_list_string
+}
+
+terraform {
+  source = "../..//modules/child"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/parent/parent/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/parent/parent/terraform.tfstate
new file mode 100644
index 0000000000..dd5a02ca17
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/parent/parent/terraform.tfstate
@@ -0,0 +1,31 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "b52d1b2c-9136-4a6d-a070-752044b76f57",
+  "outputs": {
+    "test_output1": {
+      "value": "value1",
+      "type": "string"
+    },
+    "test_output_map_map_string": {
+      "value": {
+        "map_root1": {
+          "map_root1_sub1": "map_root1_sub1_value"
+        }
+      },
+      "type": [
+        "object",
+        {
+          "map_root1": [
+            "object",
+            {
+              "map_root1_sub1": "string"
+            }
+          ]
+        }
+      ]
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/parent/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/parent/terragrunt.hcl
new file mode 100644
index 0000000000..13b0678135
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/parent/terragrunt.hcl
@@ -0,0 +1,7 @@
+include {
+  path = find_in_parent_folders()
+}
+
+terraform {
+  source = "../..//modules/parent"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/terragrunt.hcl
new file mode 100644
index 0000000000..e57313499b
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/terragrunt.hcl
@@ -0,0 +1,12 @@
+remote_state {
+  backend = "local"
+
+  generate = {
+    path      = "backend.tf"
+    if_exists = "overwrite_terragrunt"
+  }
+
+  config = {
+    path = "${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate"
+  }
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/modules/child/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/modules/child/main.tf
new file mode 100644
index 0000000000..f09da29030
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/modules/child/main.tf
@@ -0,0 +1,23 @@
+variable "test_input1" {
+  type = string
+}
+
+output "test_output1_from_parent" {
+  value = var.test_input1
+}
+
+variable "test_input_map_map_string" {
+  type = map(map(string))
+}
+
+output "test_output_map_map_string_from_parent" {
+  value = var.test_input_map_map_string
+}
+
+variable "test_input_list_string" {
+  type = list(string)
+}
+
+output "test_output_list_string" {
+  value = var.test_input_list_string
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/modules/parent/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/modules/parent/main.tf
new file mode 100644
index 0000000000..5c22ce00b9
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/modules/parent/main.tf
@@ -0,0 +1,22 @@
+output "test_output1" {
+  value = "value1"
+}
+
+output "test_output_map_map_string" {
+  value = {
+    map_root1 = {
+      map_root1_sub1 = "map_root1_sub1_value"
+    }
+    not_in_state = {
+      abc = "123"
+    }
+  }
+}
+
+output "test_output_list_string" {
+  value = [
+    "a",
+    "b",
+    "c"
+  ]
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/child/child/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/child/child/terraform.tfstate
new file mode 100644
index 0000000000..fe102d337d
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/child/child/terraform.tfstate
@@ -0,0 +1,36 @@
+{
+  "version": 4,
+  "terraform_version": "1.1.2",
+  "serial": 1,
+  "lineage": "f655935f-72fd-89b4-5d55-2a6dc2472e2d",
+  "outputs": {
+    "test_output1_from_parent": {
+      "value": "value1",
+      "type": "string"
+    },
+    "test_output_list_string": {
+      "value": [
+        "a"
+      ],
+      "type": [
+        "list",
+        "string"
+      ]
+    },
+    "test_output_map_map_string_from_parent": {
+      "value": {
+        "map_root1": {
+          "map_root1_sub1": "map_root1_sub1_value"
+        }
+      },
+      "type": [
+        "map",
+        [
+          "map",
+          "string"
+        ]
+      ]
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/child/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/child/terragrunt.hcl
new file mode 100644
index 0000000000..60f16d7282
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/child/terragrunt.hcl
@@ -0,0 +1,35 @@
+include {
+  path = find_in_parent_folders()
+}
+
+dependency "x" {
+  config_path = "../parent"
+
+  mock_outputs_merge_strategy_with_state = "deep_map_only"
+  mock_outputs_allowed_terraform_commands = ["plan", "apply", "output"]
+  mock_outputs = {
+    test_output1 = "fake-output1"
+    test_output_map_map_string = {
+      map_root1 = {
+        map_root1_sub1 = "fake-map_root1_sub1"
+      }
+      not_in_state = {
+        abc = "fake-abc"
+      }
+    }
+    test_output_list_string = ["fake-list-data"]
+  }
+}
+
+inputs = {
+  test_input1 = dependency.x.outputs.test_output1
+  test_input2 = dependency.x.outputs.test_output_map_map_string.not_in_state.abc
+
+  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string
+
+  test_input_list_string = dependency.x.outputs.test_output_list_string
+}
+
+terraform {
+  source = "../..//modules/child"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/parent/parent/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/parent/parent/terraform.tfstate
new file mode 100644
index 0000000000..ca7b700947
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/parent/parent/terraform.tfstate
@@ -0,0 +1,42 @@
+{
+  "version": 4,
+  "terraform_version": "1.1.2",
+  "serial": 1,
+  "lineage": "58286ac6-70c6-c363-fd9b-50cebcab0f3c",
+  "outputs": {
+    "test_output1": {
+      "value": "value1",
+      "type": "string"
+    },
+    "test_output_list_string": {
+      "value": [
+        "a"
+      ],
+      "type": [
+        "tuple",
+        [
+          "string"
+        ]
+      ]
+    },
+    "test_output_map_map_string": {
+      "value": {
+        "map_root1": {
+          "map_root1_sub1": "map_root1_sub1_value"
+        }
+      },
+      "type": [
+        "object",
+        {
+          "map_root1": [
+            "object",
+            {
+              "map_root1_sub1": "string"
+            }
+          ]
+        }
+      ]
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/parent/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/parent/terragrunt.hcl
new file mode 100644
index 0000000000..13b0678135
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/parent/terragrunt.hcl
@@ -0,0 +1,7 @@
+include {
+  path = find_in_parent_folders()
+}
+
+terraform {
+  source = "../..//modules/parent"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/terragrunt.hcl
new file mode 100644
index 0000000000..e57313499b
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/terragrunt.hcl
@@ -0,0 +1,12 @@
+remote_state {
+  backend = "local"
+
+  generate = {
+    path      = "backend.tf"
+    if_exists = "overwrite_terragrunt"
+  }
+
+  config = {
+    path = "${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate"
+  }
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/modules/child/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/modules/child/main.tf
new file mode 100644
index 0000000000..1e3d6b85fb
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/modules/child/main.tf
@@ -0,0 +1,31 @@
+variable "test_input1" {
+  type = string
+}
+
+output "test_output1_from_parent" {
+  value = var.test_input1
+}
+
+variable "test_input2" {
+  type = string
+}
+
+output "test_output2_from_parent" {
+  value = var.test_input2
+}
+
+variable "test_input_map_map_string" {
+  type = map(map(string))
+}
+
+output "test_output_map_map_string_from_parent" {
+  value = var.test_input_map_map_string
+}
+
+variable "test_input_list_string" {
+  type = list(string)
+}
+
+output "test_output_list_string" {
+  value = var.test_input_list_string
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/modules/parent/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/modules/parent/main.tf
new file mode 100644
index 0000000000..5c22ce00b9
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/modules/parent/main.tf
@@ -0,0 +1,22 @@
+output "test_output1" {
+  value = "value1"
+}
+
+output "test_output_map_map_string" {
+  value = {
+    map_root1 = {
+      map_root1_sub1 = "map_root1_sub1_value"
+    }
+    not_in_state = {
+      abc = "123"
+    }
+  }
+}
+
+output "test_output_list_string" {
+  value = [
+    "a",
+    "b",
+    "c"
+  ]
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/child/child/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/child/child/terraform.tfstate
new file mode 100644
index 0000000000..eda43ffef8
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/child/child/terraform.tfstate
@@ -0,0 +1,13 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "731d3bc7-9162-8495-b1ce-c64989417ba3",
+  "outputs": {
+    "test_output1_from_parent": {
+      "value": "value1",
+      "type": "string"
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/child/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/child/terragrunt.hcl
new file mode 100644
index 0000000000..c8c7722a29
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/child/terragrunt.hcl
@@ -0,0 +1,33 @@
+include {
+  path = find_in_parent_folders()
+}
+
+dependency "x" {
+  config_path = "../parent"
+
+  mock_outputs_allowed_terraform_commands = ["plan", "apply", "output"]
+  mock_outputs = {
+    test_output1 = "fake-output1"
+    test_output_map_map_string = {
+      map_root1 = {
+        map_root1_sub1 = "fake-map_root1_sub1"
+      }
+      not_in_state = {
+        abc = "fake-abc"
+      }
+    }
+    test_output_list_string = ["fake-list-data"]
+  }
+}
+
+inputs = {
+  test_input1 = dependency.x.outputs.test_output1
+
+  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string
+
+  test_input_list_string = dependency.x.outputs.test_output_list_string
+}
+
+terraform {
+  source = "../..//modules/child"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/parent/parent/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/parent/parent/terraform.tfstate
new file mode 100644
index 0000000000..dd5a02ca17
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/parent/parent/terraform.tfstate
@@ -0,0 +1,31 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "b52d1b2c-9136-4a6d-a070-752044b76f57",
+  "outputs": {
+    "test_output1": {
+      "value": "value1",
+      "type": "string"
+    },
+    "test_output_map_map_string": {
+      "value": {
+        "map_root1": {
+          "map_root1_sub1": "map_root1_sub1_value"
+        }
+      },
+      "type": [
+        "object",
+        {
+          "map_root1": [
+            "object",
+            {
+              "map_root1_sub1": "string"
+            }
+          ]
+        }
+      ]
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/parent/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/parent/terragrunt.hcl
new file mode 100644
index 0000000000..13b0678135
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/parent/terragrunt.hcl
@@ -0,0 +1,7 @@
+include {
+  path = find_in_parent_folders()
+}
+
+terraform {
+  source = "../..//modules/parent"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/terragrunt.hcl
new file mode 100644
index 0000000000..e57313499b
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/terragrunt.hcl
@@ -0,0 +1,12 @@
+remote_state {
+  backend = "local"
+
+  generate = {
+    path      = "backend.tf"
+    if_exists = "overwrite_terragrunt"
+  }
+
+  config = {
+    path = "${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate"
+  }
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/modules/child/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/modules/child/main.tf
new file mode 100644
index 0000000000..f09da29030
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/modules/child/main.tf
@@ -0,0 +1,23 @@
+variable "test_input1" {
+  type = string
+}
+
+output "test_output1_from_parent" {
+  value = var.test_input1
+}
+
+variable "test_input_map_map_string" {
+  type = map(map(string))
+}
+
+output "test_output_map_map_string_from_parent" {
+  value = var.test_input_map_map_string
+}
+
+variable "test_input_list_string" {
+  type = list(string)
+}
+
+output "test_output_list_string" {
+  value = var.test_input_list_string
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/modules/parent/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/modules/parent/main.tf
new file mode 100644
index 0000000000..5c22ce00b9
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/modules/parent/main.tf
@@ -0,0 +1,22 @@
+output "test_output1" {
+  value = "value1"
+}
+
+output "test_output_map_map_string" {
+  value = {
+    map_root1 = {
+      map_root1_sub1 = "map_root1_sub1_value"
+    }
+    not_in_state = {
+      abc = "123"
+    }
+  }
+}
+
+output "test_output_list_string" {
+  value = [
+    "a",
+    "b",
+    "c"
+  ]
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/child/child/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/child/child/terraform.tfstate
new file mode 100644
index 0000000000..eda43ffef8
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/child/child/terraform.tfstate
@@ -0,0 +1,13 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "731d3bc7-9162-8495-b1ce-c64989417ba3",
+  "outputs": {
+    "test_output1_from_parent": {
+      "value": "value1",
+      "type": "string"
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/child/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/child/terragrunt.hcl
new file mode 100644
index 0000000000..aa952d9ec7
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/child/terragrunt.hcl
@@ -0,0 +1,34 @@
+include {
+  path = find_in_parent_folders()
+}
+
+dependency "x" {
+  config_path = "../parent"
+
+  mock_outputs_merge_strategy_with_state = "no_merge"
+  mock_outputs_allowed_terraform_commands = ["plan", "apply", "output"]
+  mock_outputs = {
+    test_output1 = "fake-output1"
+    test_output_map_map_string = {
+      map_root1 = {
+        map_root1_sub1 = "fake-map_root1_sub1"
+      }
+      not_in_state = {
+        abc = "fake-abc"
+      }
+    }
+    test_output_list_string = ["fake-list-data"]
+  }
+}
+
+inputs = {
+  test_input1 = dependency.x.outputs.test_output1
+
+  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string
+
+  test_input_list_string = dependency.x.outputs.test_output_list_string
+}
+
+terraform {
+  source = "../..//modules/child"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/parent/parent/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/parent/parent/terraform.tfstate
new file mode 100644
index 0000000000..dd5a02ca17
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/parent/parent/terraform.tfstate
@@ -0,0 +1,31 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "b52d1b2c-9136-4a6d-a070-752044b76f57",
+  "outputs": {
+    "test_output1": {
+      "value": "value1",
+      "type": "string"
+    },
+    "test_output_map_map_string": {
+      "value": {
+        "map_root1": {
+          "map_root1_sub1": "map_root1_sub1_value"
+        }
+      },
+      "type": [
+        "object",
+        {
+          "map_root1": [
+            "object",
+            {
+              "map_root1_sub1": "string"
+            }
+          ]
+        }
+      ]
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/parent/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/parent/terragrunt.hcl
new file mode 100644
index 0000000000..13b0678135
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/parent/terragrunt.hcl
@@ -0,0 +1,7 @@
+include {
+  path = find_in_parent_folders()
+}
+
+terraform {
+  source = "../..//modules/parent"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/terragrunt.hcl
new file mode 100644
index 0000000000..e57313499b
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/terragrunt.hcl
@@ -0,0 +1,12 @@
+remote_state {
+  backend = "local"
+
+  generate = {
+    path      = "backend.tf"
+    if_exists = "overwrite_terragrunt"
+  }
+
+  config = {
+    path = "${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate"
+  }
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/modules/child/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/modules/child/main.tf
new file mode 100644
index 0000000000..f09da29030
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/modules/child/main.tf
@@ -0,0 +1,23 @@
+variable "test_input1" {
+  type = string
+}
+
+output "test_output1_from_parent" {
+  value = var.test_input1
+}
+
+variable "test_input_map_map_string" {
+  type = map(map(string))
+}
+
+output "test_output_map_map_string_from_parent" {
+  value = var.test_input_map_map_string
+}
+
+variable "test_input_list_string" {
+  type = list(string)
+}
+
+output "test_output_list_string" {
+  value = var.test_input_list_string
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/modules/parent/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/modules/parent/main.tf
new file mode 100644
index 0000000000..5c22ce00b9
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/modules/parent/main.tf
@@ -0,0 +1,22 @@
+output "test_output1" {
+  value = "value1"
+}
+
+output "test_output_map_map_string" {
+  value = {
+    map_root1 = {
+      map_root1_sub1 = "map_root1_sub1_value"
+    }
+    not_in_state = {
+      abc = "123"
+    }
+  }
+}
+
+output "test_output_list_string" {
+  value = [
+    "a",
+    "b",
+    "c"
+  ]
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/child/child/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/child/child/terraform.tfstate
new file mode 100644
index 0000000000..eda43ffef8
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/child/child/terraform.tfstate
@@ -0,0 +1,13 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "731d3bc7-9162-8495-b1ce-c64989417ba3",
+  "outputs": {
+    "test_output1_from_parent": {
+      "value": "value1",
+      "type": "string"
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/child/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/child/terragrunt.hcl
new file mode 100644
index 0000000000..fcd3fac7f5
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/child/terragrunt.hcl
@@ -0,0 +1,34 @@
+include {
+  path = find_in_parent_folders()
+}
+
+dependency "x" {
+  config_path = "../parent"
+
+  mock_outputs_merge_strategy_with_state = "shallow"
+  mock_outputs_allowed_terraform_commands = ["plan", "apply", "output"]
+  mock_outputs = {
+    test_output1 = "fake-output1"
+    test_output_map_map_string = {
+      map_root1 = {
+        map_root1_sub1 = "fake-map_root1_sub1"
+      }
+      not_in_state = {
+        abc = "fake-abc"
+      }
+    }
+    test_output_list_string = ["fake-list-data"]
+  }
+}
+
+inputs = {
+  test_input1 = dependency.x.outputs.test_output1
+
+  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string
+
+  test_input_list_string = dependency.x.outputs.test_output_list_string
+}
+
+terraform {
+  source = "../..//modules/child"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/parent/parent/terraform.tfstate b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/parent/parent/terraform.tfstate
new file mode 100644
index 0000000000..dd5a02ca17
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/parent/parent/terraform.tfstate
@@ -0,0 +1,31 @@
+{
+  "version": 4,
+  "terraform_version": "1.0.11",
+  "serial": 1,
+  "lineage": "b52d1b2c-9136-4a6d-a070-752044b76f57",
+  "outputs": {
+    "test_output1": {
+      "value": "value1",
+      "type": "string"
+    },
+    "test_output_map_map_string": {
+      "value": {
+        "map_root1": {
+          "map_root1_sub1": "map_root1_sub1_value"
+        }
+      },
+      "type": [
+        "object",
+        {
+          "map_root1": [
+            "object",
+            {
+              "map_root1_sub1": "string"
+            }
+          ]
+        }
+      ]
+    }
+  },
+  "resources": []
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/parent/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/parent/terragrunt.hcl
new file mode 100644
index 0000000000..13b0678135
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/parent/terragrunt.hcl
@@ -0,0 +1,7 @@
+include {
+  path = find_in_parent_folders()
+}
+
+terraform {
+  source = "../..//modules/parent"
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/terragrunt.hcl b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/terragrunt.hcl
new file mode 100644
index 0000000000..e57313499b
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/terragrunt.hcl
@@ -0,0 +1,12 @@
+remote_state {
+  backend = "local"
+
+  generate = {
+    path      = "backend.tf"
+    if_exists = "overwrite_terragrunt"
+  }
+
+  config = {
+    path = "${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate"
+  }
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/modules/child/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/modules/child/main.tf
new file mode 100644
index 0000000000..f09da29030
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/modules/child/main.tf
@@ -0,0 +1,23 @@
+variable "test_input1" {
+  type = string
+}
+
+output "test_output1_from_parent" {
+  value = var.test_input1
+}
+
+variable "test_input_map_map_string" {
+  type = map(map(string))
+}
+
+output "test_output_map_map_string_from_parent" {
+  value = var.test_input_map_map_string
+}
+
+variable "test_input_list_string" {
+  type = list(string)
+}
+
+output "test_output_list_string" {
+  value = var.test_input_list_string
+}
diff --git a/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/modules/parent/main.tf b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/modules/parent/main.tf
new file mode 100644
index 0000000000..5c22ce00b9
--- /dev/null
+++ b/test/fixture-get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/modules/parent/main.tf
@@ -0,0 +1,22 @@
+output "test_output1" {
+  value = "value1"
+}
+
+output "test_output_map_map_string" {
+  value = {
+    map_root1 = {
+      map_root1_sub1 = "map_root1_sub1_value"
+    }
+    not_in_state = {
+      abc = "123"
+    }
+  }
+}
+
+output "test_output_list_string" {
+  value = [
+    "a",
+    "b",
+    "c"
+  ]
+}
diff --git a/test/integration_test.go b/test/integration_test.go
index ef4fd01b7d..e8daaa3f21 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -2216,6 +2216,199 @@ func TestDependencyMockOutputMergeWithStateNoOverride(t *testing.T) {
 	logBufferContentsLineByLine(t, stderr, "show stderr")
 }
 
+// Test when mock_outputs_merge_strategy_with_state or mock_outputs_merge_with_state is not set, the default is no_merge
+func TestDependencyMockOutputMergeStrategyWithStateDefault(t *testing.T) {
+	t.Parallel()
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_GET_OUTPUT)
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_GET_OUTPUT)
+	rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_GET_OUTPUT, "mock-outputs-merge-strategy-with-state", "merge-strategy-with-state-default", "live")
+	childPath := filepath.Join(rootPath, "child")
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", childPath), &stdout, &stderr)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "This object does not have an attribute named \"test_output_list_string\"")
+	logBufferContentsLineByLine(t, stdout, "apply stdout")
+	logBufferContentsLineByLine(t, stderr, "apply stderr")
+}
+
+// Test when mock_outputs_merge_with_state = "false" that MergeStrategyType is set to no_merge
+func TestDependencyMockOutputMergeStrategyWithStateCompatFalse(t *testing.T) {
+	t.Parallel()
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_GET_OUTPUT)
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_GET_OUTPUT)
+	rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_GET_OUTPUT, "mock-outputs-merge-strategy-with-state", "merge-strategy-with-state-compat-false", "live")
+	childPath := filepath.Join(rootPath, "child")
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", childPath), &stdout, &stderr)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "This object does not have an attribute named \"test_output_list_string\"")
+	logBufferContentsLineByLine(t, stdout, "apply stdout")
+	logBufferContentsLineByLine(t, stderr, "apply stderr")
+}
+
+// Test when mock_outputs_merge_with_state = "true" that MergeStrategyType is set to shallow
+func TestDependencyMockOutputMergeStrategyWithStateCompatTrue(t *testing.T) {
+	t.Parallel()
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_GET_OUTPUT)
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_GET_OUTPUT)
+	rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_GET_OUTPUT, "mock-outputs-merge-strategy-with-state", "merge-strategy-with-state-compat-true", "live")
+	childPath := filepath.Join(rootPath, "child")
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", childPath), &stdout, &stderr)
+	assert.NoError(t, err)
+	logBufferContentsLineByLine(t, stdout, "apply stdout")
+	logBufferContentsLineByLine(t, stderr, "apply stderr")
+
+	stdout.Reset()
+	stderr.Reset()
+
+	require.NoError(t, runTerragruntCommand(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", childPath), &stdout, &stderr))
+	outputs := map[string]TerraformOutput{}
+	require.NoError(t, json.Unmarshal([]byte(stdout.String()), &outputs))
+	logBufferContentsLineByLine(t, stdout, "output stdout")
+	logBufferContentsLineByLine(t, stderr, "output stderr")
+
+	assert.Equal(t, "value1", outputs["test_output1_from_parent"].Value)
+	assert.Equal(t, "map_root1_sub1_value", util.MustWalkTerraformOutput(outputs["test_output_map_map_string_from_parent"].Value, "map_root1", "map_root1_sub1", "value"))
+	assert.Equal(t, nil, util.MustWalkTerraformOutput(outputs["test_output_map_map_string_from_parent"].Value, "not_in_state", "abc", "value"))
+	assert.Equal(t, "fake-list-data", util.MustWalkTerraformOutput(outputs["test_output_list_string"].Value, "0"))
+	assert.Equal(t, nil, util.MustWalkTerraformOutput(outputs["test_output_list_string"].Value, "1"))
+}
+
+// Test when both mock_outputs_merge_with_state and mock_outputs_merge_strategy_with_state are set, mock_outputs_merge_strategy_with_state is used
+func TestDependencyMockOutputMergeStrategyWithStateCompatConflict(t *testing.T) {
+	t.Parallel()
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_GET_OUTPUT)
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_GET_OUTPUT)
+	rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_GET_OUTPUT, "mock-outputs-merge-strategy-with-state", "merge-strategy-with-state-compat-true", "live")
+	childPath := filepath.Join(rootPath, "child")
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", childPath), &stdout, &stderr)
+	assert.NoError(t, err)
+	logBufferContentsLineByLine(t, stdout, "apply stdout")
+	logBufferContentsLineByLine(t, stderr, "apply stderr")
+
+	stdout.Reset()
+	stderr.Reset()
+
+	require.NoError(t, runTerragruntCommand(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", childPath), &stdout, &stderr))
+	outputs := map[string]TerraformOutput{}
+	require.NoError(t, json.Unmarshal([]byte(stdout.String()), &outputs))
+	logBufferContentsLineByLine(t, stdout, "output stdout")
+	logBufferContentsLineByLine(t, stderr, "output stderr")
+
+	assert.Equal(t, "value1", outputs["test_output1_from_parent"].Value)
+	assert.Equal(t, "map_root1_sub1_value", util.MustWalkTerraformOutput(outputs["test_output_map_map_string_from_parent"].Value, "map_root1", "map_root1_sub1", "value"))
+	assert.Equal(t, nil, util.MustWalkTerraformOutput(outputs["test_output_map_map_string_from_parent"].Value, "not_in_state", "abc", "value"))
+	assert.Equal(t, "fake-list-data", util.MustWalkTerraformOutput(outputs["test_output_list_string"].Value, "0"))
+	assert.Equal(t, nil, util.MustWalkTerraformOutput(outputs["test_output_list_string"].Value, "1"))
+}
+
+// Test when mock_outputs_merge_strategy_with_state = "no_merge" that mocks are not merged into the current state
+func TestDependencyMockOutputMergeStrategyWithStateNoMerge(t *testing.T) {
+	t.Parallel()
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_GET_OUTPUT)
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_GET_OUTPUT)
+	rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_GET_OUTPUT, "mock-outputs-merge-strategy-with-state", "merge-strategy-with-state-no-merge", "live")
+	childPath := filepath.Join(rootPath, "child")
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", childPath), &stdout, &stderr)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "This object does not have an attribute named \"test_output_list_string\"")
+	logBufferContentsLineByLine(t, stdout, "apply stdout")
+	logBufferContentsLineByLine(t, stderr, "apply stderr")
+}
+
+// Test when mock_outputs_merge_strategy_with_state = "shallow" that only top level outputs are merged.
+// Lists or keys in existing maps will not be merged
+func TestDependencyMockOutputMergeStrategyWithStateShallow(t *testing.T) {
+	t.Parallel()
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_GET_OUTPUT)
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_GET_OUTPUT)
+	rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_GET_OUTPUT, "mock-outputs-merge-strategy-with-state", "merge-strategy-with-state-shallow", "live")
+	childPath := filepath.Join(rootPath, "child")
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", childPath), &stdout, &stderr)
+	assert.NoError(t, err)
+	logBufferContentsLineByLine(t, stdout, "apply stdout")
+	logBufferContentsLineByLine(t, stderr, "apply stderr")
+
+	stdout.Reset()
+	stderr.Reset()
+
+	require.NoError(t, runTerragruntCommand(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", childPath), &stdout, &stderr))
+	outputs := map[string]TerraformOutput{}
+	require.NoError(t, json.Unmarshal([]byte(stdout.String()), &outputs))
+	logBufferContentsLineByLine(t, stdout, "output stdout")
+	logBufferContentsLineByLine(t, stderr, "output stderr")
+
+	assert.Equal(t, "value1", outputs["test_output1_from_parent"].Value)
+	assert.Equal(t, "map_root1_sub1_value", util.MustWalkTerraformOutput(outputs["test_output_map_map_string_from_parent"].Value, "map_root1", "map_root1_sub1", "value"))
+	assert.Equal(t, nil, util.MustWalkTerraformOutput(outputs["test_output_map_map_string_from_parent"].Value, "not_in_state", "abc", "value"))
+	assert.Equal(t, "fake-list-data", util.MustWalkTerraformOutput(outputs["test_output_list_string"].Value, "0"))
+	assert.Equal(t, nil, util.MustWalkTerraformOutput(outputs["test_output_list_string"].Value, "1"))
+}
+
+// Test when mock_outputs_merge_strategy_with_state = "deep" that the existing state is deeply merged into the mocks
+// so that the existing state overwrites the mocks. This allows child modules to use new dependency outputs before the
+// dependency has been applied
+func TestDependencyMockOutputMergeStrategyWithStateDeepMapOnly(t *testing.T) {
+	t.Parallel()
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_GET_OUTPUT)
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_GET_OUTPUT)
+	rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_GET_OUTPUT, "mock-outputs-merge-strategy-with-state", "merge-strategy-with-state-deep-map-only", "live")
+	childPath := filepath.Join(rootPath, "child")
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", childPath), &stdout, &stderr)
+	assert.NoError(t, err)
+	logBufferContentsLineByLine(t, stdout, "apply stdout")
+	logBufferContentsLineByLine(t, stderr, "apply stderr")
+
+	stdout.Reset()
+	stderr.Reset()
+
+	require.NoError(t, runTerragruntCommand(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", childPath), &stdout, &stderr))
+	outputs := map[string]TerraformOutput{}
+	require.NoError(t, json.Unmarshal([]byte(stdout.String()), &outputs))
+	logBufferContentsLineByLine(t, stdout, "output stdout")
+	logBufferContentsLineByLine(t, stderr, "output stderr")
+
+	assert.Equal(t, "value1", outputs["test_output1_from_parent"].Value)
+	assert.Equal(t, "fake-abc", outputs["test_output2_from_parent"].Value)
+	assert.Equal(t, "map_root1_sub1_value", util.MustWalkTerraformOutput(outputs["test_output_map_map_string_from_parent"].Value, "map_root1", "map_root1_sub1", "value"))
+	assert.Equal(t, "fake-abc", util.MustWalkTerraformOutput(outputs["test_output_map_map_string_from_parent"].Value, "not_in_state", "abc", "value"))
+	assert.Equal(t, "a", util.MustWalkTerraformOutput(outputs["test_output_list_string"].Value, "0"))
+	assert.Equal(t, nil, util.MustWalkTerraformOutput(outputs["test_output_list_string"].Value, "1"))
+}
+
 // Test that when you have a mock_output on a dependency, the dependency will use the mock as the output instead
 // of erroring out when running an allowed command.
 func TestDependencyMockOutputRestricted(t *testing.T) {
diff --git a/util/reflect_test.go b/util/reflect_test.go
index b6f7fa3860..3d4f96f6df 100644
--- a/util/reflect_test.go
+++ b/util/reflect_test.go
@@ -32,3 +32,47 @@ func TestKindOf(t *testing.T) {
 		t.Logf("%v passed", testCase.value)
 	}
 }
+
+func TestMustWalkTerraformOutput(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		value    interface{}
+		path     []string
+		expected interface{}
+	}{
+		{
+			value: map[string]map[string]string{
+				"a": {
+					"b": "c",
+				},
+			},
+			path:     []string{"a", "b"},
+			expected: "c",
+		},
+		{
+			value: map[string]map[string]string{
+				"a": {
+					"b": "c",
+				},
+			},
+			path:     []string{"a", "d"},
+			expected: nil,
+		},
+		{
+			value:    []string{"a", "b", "c"},
+			path:     []string{"1"},
+			expected: "b",
+		},
+		{
+			value:    []string{"a", "b", "c"},
+			path:     []string{"10"},
+			expected: nil,
+		},
+	}
+
+	for _, testCase := range testCases {
+		actual := MustWalkTerraformOutput(testCase.value, testCase.path...)
+		assert.Equal(t, testCase.expected, actual)
+	}
+}