How to organically merge nested associations?

Initial data:

peopleFacts = <|
    alice -> <|age -> 29, shoeSize -> 7|>, 
    bob -> <|age -> 27, sex -> male,  hair -> <|Color -> RGBColor[1, 0, 0]|>
    |>
|>;

Here is a version of RecurAssocMerge reduced to a single definition.

MergeNested = If[MatchQ[#, {__Association}], Merge[#, #0], Last[#]] &

MergeNested @ {peopleFacts, <|bob -> <|hair -> <|length -> 120|>|>|>}
 <|
   alice -> <|
     age -> 29, 
     shoeSize -> 7|>, 
   bob -> <|
     age -> 27, 
     sex -> male,  
     hair -> <|Color -> RGBColor[1, 0, 0], length -> 120|>
   |>
 |>

Special case of 2-level deep association

Merge[{
   peopleFacts,
   <|bob -> <|hairColor -> 1|>|>
 },
 Association
]

"Tidy" approach to write NestedMerge:

RecurAssocMerge[a : {__Association}] := Merge[a, RecurAssocMerge];
RecurAssocMerge[a_] := Last[a];
  • adding key to deep level association:

    RecurAssocMerge[
      {peopleFacts, <|bob -> <|hair -> <|length -> 120|>|>|>}
     ]
    
     <|alice -> <|age -> 29, shoeSize -> 7|>, 
       bob -> <|age -> 27, sex -> male, hair -> <|
            Color -> RGBColor[1, 0, 0], length -> 120 |>
       |>
     |>
    
  • entirely new tree

    RecurAssocMerge[
       {peopleFacts, <|kuba -> <|hair -> <|length -> 120|>|>|>}
     ]
    
     <|
        alice -> <|age -> 29, shoeSize -> 7|>, 
        bob -> <|age -> 27, sex -> male, hair -> <|Color -> RGBColor[1, 0, 0]|>
        |>, 
        kuba -> <|hair -> <|length -> 120|>|>
    |>
    

Section added by Jess Riedel:

Specialize to single new entry

RecurAssocMerge defined above is a general method for merging nested Associations. We can define an abbreviation for the special case when we are adding only a single new entry.

RecurAssocMerge[ini_Association, path_List, value_] := RecurAssocMerge[{
   ini, Fold[<|#2 -> #|> &, value, Reverse@path]
}]

Then we can just do

RecurAssocMerge[peopleFacts, {bob, hair, length}, 120]
 <|alice -> <|age -> 29, shoeSize -> 7|>, 
   bob -> <|age -> 27, sex -> male, hair -> <|
            Color -> RGBColor[1, 0, 0], length -> 120 |>
     |>
   |>

Notes

If you want to modify peopleFacts the peopleFacts = Merge... is needed of course.


Update

Created an upsert function to update/insert new keys and values into a nested association structure. It automatically inserts nested associations where they do not exists and does not need to be assigned back to the original association. It updates existing keys when they are found.

ClearAll[upsert]
Attributes[upsert] = {HoldFirst};
upsert[dat_?AssociationQ, key_, value__] :=
 If[First@Dimensions@{value} == 1,
  dat[key] = value,
  (
   If[KeyExistsQ[dat, key] == False, dat[key] = <||>];
   upsert[dat[key], First@{value}, Sequence @@ Rest@{value}]
  )
  ]

Can use upsert with as many nested levels as needed.

peopleFacts = <|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>, 
   "bob" -> <|"age" -> 27, "sex" -> "male"|>|>;

Insert "steve" and association "haircolor" key/value.

upsert[peopleFacts, "steve", "haircolor", "Red"];
peopleFacts

(* <|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>, 
 "bob" -> <|"age" -> 27, "sex" -> "male"|>, 
 "steve" -> <|"haircolor" -> "Red"|>|> *)

Insert "tim", association "music" key/value, and nested association "rock" key/value.

upsert[peopleFacts, "tim", "music", "rock", "jimmy"];
peopleFacts

(* <|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>, 
 "bob" -> <|"age" -> 27, "sex" -> "male"|>, 
 "steve" -> <|"haircolor" -> "Red"|>, 
 "tim" -> <|"music" -> <|"rock" -> "jimmy"|>|>|> *)

Update "alice" "age".

upsert[peopleFacts, "alice", "age", 25];
peopleFacts

(* <|"alice" -> <|"age" -> 25, "shoeSize" -> 7|>, 
 "bob" -> <|"age" -> 27, "sex" -> "male"|>, 
 "steve" -> <|"haircolor" -> "Red"|>, 
 "tim" -> <|"music" -> <|"rock" -> "lenny"|>|>|> *)

Original Post

Each time there is a new key that has an association as its value you must initialise it as an association. Then you can use the feature of Association that creates a key when a value is assigned to a non-existing key.

peopleFacts = <|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>, "bob" -> <|"age" -> 27, "sex" -> "male"|>|>;

peopleFacts["steve"] = <||>;
peopleFacts
(* <|alice -> <|age -> 29, shoeSize -> 7|>, 
 bob -> <|age -> 27, sex -> male|>, steve -> <||>|> *)

peopleFacts["steve"]["hairColor"] = "Red";
peopleFacts
(* <|alice -> <|age -> 29, shoeSize -> 7|>, 
 bob -> <|age -> 27, sex -> male|>, steve -> <|hairColor -> Red|>|> *)

peopleFacts["bob"]["age"] = 22;
peopleFacts
(* <|alice -> <|age -> 29, shoeSize -> 7|>, 
 bob -> <|age -> 22, sex -> male|>, steve -> <|hairColor -> Red|>|> *)

peopleFacts["steve"]["major"] = "Physics";
peopleFacts
(* <|alice -> <|age -> 29, shoeSize -> 7|>, 
 bob -> <|age -> 22, sex -> male|>, 
 steve -> <|hairColor -> "Red", major -> "Physics"|>|> *)

Hope this helps.


Try this, we're using it on a daily basis

Nest[Merge,f,n]

To your starting data, slightly modified (strings vs symbols):

peopleFacts = <|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>, 
   "bob" -> <|"age" -> 27, "sex" -> "male"|>|>;

And new facts:

newFacts = <|
  "steve" -> <|"hairColor" -> "red", "major" -> "physics"|>,
  "bob" -> <|"age" -> 22|>|>

Update semantics for replacing existing values: (1) add newFacts last, (2) apply Last eg:

{peopleFacts, newFacts} // Nest[Merge, Last, 2]

<|"alice" -> <|"age" -> 29, "shoeSize" -> 7|>, "bob" -> <|"age" -> 22, "sex" -> "male"|>, "steve" -> <|"hairColor" -> "red", "major" -> "physics"|>|>

Keep in mind Merge does not yet have full complement of options like JoinAcross eg inner/outer/left/right and will not impute missing keys, so often KeyIntersection is required.

Also will admit for some types of ragged hierarchies, there's no current easy way to merge all relevant branches automatically (an All level spec). In other words, knowledge of the schema is required.