[SalesForce] How to create a recursive method in Apex which takes a dot notation string and convert it into Hierarchical Map/Json

I'm trying to create a utility class/method where a certain method will accept a list of Strings – each string will be constructed as follows:

"Root.Parent.Child... N"

This string can be N long (or basically holds infinite number of parts).

Each Part of the String (parts are the split string by dot) is intended to be a Json Object.

For Example:

  //sample data
    String rec1 = 'Root.Parent.FirstChild';
    String rec2 = 'Root.Parent.FirstChild2';
    String rec3 = 'Root.Parent2.Child';
    String rec4 = 'Root.Parent2.Child2';
    String[] fullPathsList = new List<String>{ rec1,rec2,rec3,rec4  };

 //should create as many maps as needed dynamically based on total size of string paths
    Map<String,Object> output = new Map<String,Object>();
    Map<String,Object> root = new Map<String,Object>();
    Map<String,Object> level1 = new Map<String,Object>();
    Map<String,Object> level2 = new Map<String,Object>();

//iterate over the full list and grab each path
for(String fullJsonPath:fullPathsList ) {
  //check if has a dot
  if( fullJsonPath.indexOf('.') != -1 ){
    String[] pathPartsList =  fullJsonPath.split('\\.');
      Integer totalSize = pathPartsList.size();
      for(Integer i=0;i<totalSize;i++) {
          level2.put(pathPartsList[totalSize-1] , 'VALUE');
          if(totalSize-2 > 0){
           level1.put(pathPartsList[totalSize-2], level2);
          }
         root.put(pathPartsList[0], level1); 
      } 
   }
   output.put('results', root);
}

System.debug('@@@ output ' + JSON.serialize(output));

The output will show :

{ "results": {
    "Root": {
      "Parent2": {
        "Child2": "VALUE",
        "Child": "VALUE",
        "FirstChild2": "VALUE",
        "FirstChild": "VALUE"
      },
      "Parent": {
        "Child2": "VALUE",
        "Child": "VALUE",
        "FirstChild2": "VALUE",
        "FirstChild": "VALUE"
      }
    }
  }
}

My problems here is :

  1. The level 2 map is holding all childs – each parent holds all childs
    and not only it's own.
  2. I need to have an ability to generate those Maps dynamically based on the number of childs the strings have – was thinking some
    kind of a recursion method will do the trick.

Anybody see a clever way of achieving this in Apex?

In Javascript it's pretty simple with this one liner which I'm trying to replicate somehow in Apex (any equivalent reduce method?) :

'Root.Parent.FirstChild'.split('.').reduce((o,i)=>o[i], obj);

Best Answer

I've done exactly this before. Here's working code for it:

public class JsonBoxer {

    public Map<String, Object> root {get; private set;}

    public JsonBoxer() {
        this.root = new Map<String, Object>();
    }

    public void put(String key, Object value) {
        doPut(root, key.split('\\.'), value);
    }

    private void doPut(Map<String, Object> currentRoot, List<String> keyChain, Object value) {
        if(keyChain.size() == 1) {
            currentRoot.put(keyChain[0], value);
        } else {
            String thisKey = keyChain.remove(0);
            Map<String, Object> child = (Map<String, Object>)currentRoot.get(thisKey);
            if(child == null) {
                child = new Map<String, Object>();
                currentRoot.put(thisKey, child);
            }

            doPut(child, keyChain, value);
        }
    }
}

Even with a test:

@IsTest
private class JsonBoxerTest {

    @IsTest static void noBoxing() {
        JsonBoxer boxer = new JsonBoxer();

        boxer.put('a', 'b');

        System.assertEquals('b', boxer.root.get('a'));
    }

    @IsTest static void boxing() {
        JsonBoxer boxer = new JsonBoxer();

        boxer.put('a.1', 'b');

        System.assertEquals('b', ((Map<String, Object>)boxer.root.get('a')).get('1'));
    }

    @IsTest static void twoSubKeyValues() {
        JsonBoxer boxer = new JsonBoxer();

        boxer.put('a.1', 'b');
        boxer.put('a.2', 'c');

        System.assertEquals('b', ((Map<String, Object>)boxer.root.get('a')).get('1'));
        System.assertEquals('c', ((Map<String, Object>)boxer.root.get('a')).get('2'));
    }

    @IsTest static void overwriteSubKey() {
        JsonBoxer boxer = new JsonBoxer();

        boxer.put('a.1', 'b');
        boxer.put('a.1', 'c');

        System.assertEquals('c', ((Map<String, Object>)boxer.root.get('a')).get('1'));
    }
}