Skip to content

bug: native deep merge replaces nested map with integer YAML key instead of recursively merging (regression in v1.212.0+) #2376

Description

Describe the Bug

Since v1.212.0 (PR #2201 — native deep merge replacing mergo), a nested map whose parent key is an unquoted integer in YAML (e.g. eni.1) is replaced instead of recursively deep-merged when a stack overrides a subset of its keys.

With mergo (≤ v1.211.0) the two maps were deep-merged — keys present only in the catalog were preserved alongside keys present only in the stack override. With the native implementation the catalog keys are silently dropped.

The partial regression fix in PR #2248 addressed list → map type mismatches, but did not address the case where both sides are maps but the parent key is parsed as a YAML integer.

Expected Behavior

Given a catalog that defines:

# catalog/nss.yaml
components:
  terraform:
    ec2-custom-nss-web:
      vars:
        eni:
          1:                             # integer key
            description: NSS system interface
            sg_rules:
              ip4_cidr_blocks:
                egress,tcp,443: [0.0.0.0/0]

And a stack that adds keys to the same eni.1 entry:

# stack/ue1-pci-it.yaml
components:
  terraform:
    ec2-custom-nss-web:
      vars:
        eni:
          1:
            security_groups:
              - sg-07dd44f6e72142c44
            sg_rules:
              ip4_cidr_blocks:
                egress,tcp,8601: [10.107.0.0/17]

atmos describe component ec2-custom-nss-web -s ue1-pci-it should produce a merged eni.1:

eni:
  1:
    description: NSS system interface      # ← preserved from catalog
    security_groups:
      - sg-07dd44f6e72142c44
    sg_rules:
      ip4_cidr_blocks:
        egress,tcp,443: [0.0.0.0/0]       # ← preserved from catalog
        egress,tcp,8601: [10.107.0.0/17]

Actual result (v1.212.0): description and egress,tcp,443 are dropped — the stack's eni.1 entirely replaces the catalog's eni.1:

eni:
  1:
    security_groups:
      - sg-07dd44f6e72142c44
    sg_rules:
      ip4_cidr_blocks:
        egress,tcp,8601: [10.107.0.0/17]

Steps to Reproduce

  1. Create a catalog file with a nested map under an integer YAML key:
# stacks/catalog/base.yaml
components:
  terraform:
    my-component:
      vars:
        config:
          1:
            from_catalog: true
            shared_key: catalog_value
  1. Create a stack file that adds/overrides a subset of keys under the same integer key:
# stacks/env/my-stack.yaml
import:
  - catalog/base

components:
  terraform:
    my-component:
      vars:
        config:
          1:
            from_stack: true
            shared_key: stack_value
  1. Run:
atmos describe component my-component -s my-stack
  1. Observe config.1 in the output.

v1.211.0: config.1 contains from_catalog: true, from_stack: true, shared_key: stack_value (deep merge).
v1.212.0: config.1 contains only from_stack: true, shared_key: stack_value — from_catalog is absent (replace).

Workaround: Quoting the key ("1":) in both files forces a string key and the native merge handles it correctly, as deepMergeNative only operates on map[string]any.

Screenshots

N/A — see Steps to Reproduce output above.

Environment

  • OS: Linux (Geodesic-based infra container)
  • Atmos version: v1.212.0 (regression), v1.211.0 (works correctly)
  • Terraform version: n/a (issue is in atmos describe component, no Terraform invocation needed)

Additional Context

Root cause hypothesis:

Go's yaml.v3 parses an unquoted integer key (1:) as int(1), not string("1"). Before merging, Atmos normalizes maps to map[string]any. If this normalization runs correctly, the key becomes "1" and the native merge should recurse into it. However, if any step in the pipeline between YAML unmarshaling and deepMergeNative skips normalization, the map may arrive as map[interface{}]any or map[int]any. The native merge's type switch only matches map[string]any; an unrecognized map type falls through as a scalar leaf and is replaced rather than recursed into.

Mergo used reflection and could merge any concrete map type, masking this normalization gap.

Related PRs:

Workaround available: Quote all integer YAML keys ("1": instead of 1:). This is safe but requires touching every catalog and stack file that uses numeric keys.


Assisted-by: Sisyphus:claude-sonnet-4-6 opencode

Activity

  1. changed the title [-]bug: native deep merge replaces nested map with integer YAML key instead of recursively merging (regression from #2201)[/-] [+]bug: native deep merge replaces nested map with integer YAML key instead of recursively merging (regression in v1.212.0+)[/+] on Apr 29, 2026
  2. MaxymVlasov commented on Apr 29, 2026

    @MaxymVlasov
    Author

    In case of string replacement, we, for some reason see next error in Spacelift:

    ╷
    │ Error: Plugin did not respond
    │ 
    │   with module.iam_roles.module.account_map.data.utils_component_config.config[0],
    │   on .terraform/modules/iam_roles.account_map/modules/remote-state/main.tf line 1, in data "utils_component_config" "config":
    │    1: data "utils_component_config" "config" {
    │ 
    │ The plugin encountered an error, and failed to respond to the
    │ plugin.(*GRPCProvider).ReadDataSource call. The plugin logs may contain
    │ more details.
    ╵
    
    Stack trace from the terraform-provider-utils plugin:
    

    so for now we stick to 1.211.0

  3. MaxymVlasov commented on Jul 7, 2026

    @MaxymVlasov
    Author

    Still don't work in 1.222.0

  4. added a commit that references this issue on Jul 9, 2026
    b248714
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions