How deep can nested Maps go, and how do I properly add data to those nested maps?

Background: Long story short, my company is integrating with a
platform that has a “Role Hierarchy”, so to speak, but that Hierarchy
isn’t stored in Salesforce. To get the “roles” I have to query their
API. Even further than that, each Role can have child Roles, but the
only way to find that out is to individually query each Role via their
API to see if it has any children. Those children can have children,
and those children can have children, etc. In total, with our current
setup, it is possible that the parent-child roles could go as far as 6
levels deep.

SO, I think I need to use a 6 level deep nested Map of Strings, like this:

Map<String,Map<String,Map<String,Map<String,Map<String,Map<String,String>>>>>> allLevels = new Map<String,Map<String,Map<String,Map<String,Map<String,Map<String,String>>>>>>();

First question: Is this even possible? Am I exceeding any limits with a Map like this? I have looked over documentation and can’t find
anything regarding nested map limits.


When I am querying their API, it looks like this:

https://api.website.com/v1/teams?apikey=API-KEY-HERE&source=Salesforce-GETTeams

This returns JSON results like this:

{
    "Id": "pAFwnPpqJTo1",
    "Name": "External",
    "TeamCodeForBulkImport": "446878-External"
},
{
    "Id": "R7tu6KPYk6k1",
    "Name": "English",
    "TeamCodeForBulkImport": "446885-English"
},
{
    "Id": "-7tu-ZPY3Ik1",
    "Name": "Spanish",
    "TeamCodeForBulkImport": "446885-Spanish"
},
{
    "Id": "-TwdJwJzjbY1",
    "Name": "Application",
    "TeamCodeForBulkImport": "446904-Application"
},
{
    "Id": "-Twdzz-jbY1",
    "Name": "Enterprise",
    "TeamCodeForBulkImport": "446904-Enterprise"
},
{
    "Id": "-TWd2w-fXY1",
    "Name": "Enterprise",
    "TeamCodeForBulkImport": "463321-Enterprise"
},

It returns ALL teams, from ALL levels (top level, children, grandchildren, etc) using this query. Note that Enterprise is listed multiple times, that is because Enterprise exists more than once as a child to different parents. In my case, I need to know the true hierarchy which looks something like this:

              v-- External --v
        v-- Spanish        English --v
    v-- Application            Application --v
v-- Enterprise                     Enterprise --v

To get the actual children, I need to do a separate API call using the ID of the Parent, like this:

https://api.website.com/v1/teams/TEAM-ID-HERE/teams?apikey=API-KEY-HERE&source=Salesforce-GETTeams

This will return JSON of ONLY the children of whatever TEAM-ID I specified in my call. So the first thing I want to do is make the “top level” categories (Internal/External) the first String in my map, then loop through those IDs to get the children of each and store them in the “second level” of my String Map, then loop through those SECOND LEVEL IDs and get the children of them and store them in the “third level” of my String map, etc etc, until I’ve traversed all 6 levels and have all of the data stored in my map.

My goal here is to have a Map where the primary keySet is Internal or External, and then the second keySet is the children of Internal or External, and the third keySet is the children of the second keyset, and so on until I’ve reached the end. My result should be a very elaborate map that I can then use in combination with data sourced from Contact records to determine which group to assign them to, and get that groups ID from this nested map.

Second question: Since I will be storing data incrementally in the map
(as detailed above), I need to store data in the map as I get it, and
continue to build the map as I go through the loops and make the
calls. I’ve never dealt with a nested map before, especially not a
6-level-deep nested map. How would I properly insert data into the map
for the second level while maintaining the third, fourth, fifth, and
sixth levels as null, and then move on to storing data in the third
level while keeping the fourth, fifth, and sixth null, and so on,
until the map is complete?

— FINAL WORKING CODE —

After a back-and-forth with sfdcfox, I ended up creating a wrapper class that contained the String id,parentid,name; of the CustomRole. It also contained a Map<String,CustomRole> childRoles. I start by doing an API call to get ALL roles and store them in Map<String,CustomRole> roleMap, then I loop through the values in roleMap to do the individual sub-team API calls, and then I pull the current Role’s childRoles map from the roleMap, and add to it for each child role I get back in my sub-team API call.

This results in a Map<String,CustomRole> where I can loop through the main role and then do an inner loop for its childRoles.

public with sharing class FutureCaller {
    private static final String apikey = 'xxxxx-xx-x-xxxx-x';
    private static final String endpoint = 'https://api.website.com/v1.svc';

    private LitmosFutureCaller() {}

    public class CustomRole{ 
        public String guid, parentId, name; 
        public Map<String, CustomRole> childRoles; 

        public CustomRole(String teamId,String parentId,String name){
            this.guid = teamId;
            this.parentId = parentId;
            this.name = name;
            this.childRoles = new Map<String,CustomRole>();
        }
    }

    @future (callout=true)
    public static void getTeams(Set<Id> contactIds, Boolean isNew){
        List<Contact> newList = [SELECT Id,Primary_Language__c,Activated__c,LoginKey__c,lmsID__c,Title__c FROM Contact WHERE ID in :contactIds];

        // create our roleMap to populate with role Id,CustomRole class
        Map<String, CustomRole> roleMap = new Map<String, CustomRole>();
        // create source string for use in API calls
        String source; 
        // Strings to hold Id and Name for each object in the parser
        String teamId;
        String teamName;

        // set source string for use in API call
        source = 'MSI-Apex-getTeams';

        // build HTTP GET Request with JSON Return
        HttpRequest httpReq = new HttpRequest();
        httpReq.setMethod('GET');
        httpReq.setHeader('Accept','application/json');
        httpReq.setEndpoint(endpoint+'/teams?apikey='+apikey+'&source='+source);

        // create HTTP for Callout
        Http http = new Http();

        // first we need to build the roleMap by making an initial API call
        // to get all teams(roles). Their Id will be the key, and the 
        // CustomRole wrapper will be the value
        try{
            // Get HTTP Callout Response
            HTTPResponse httpResp = http.send(httpReq);

            // Create Parser
            JSONParser parser = JSON.createParser(httpResp.getBody());
            while(parser.nextToken() != null){
                // if Current Token is the Id field, go to Next Token and set teamId
                if((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&
                    (parser.getText() == 'Id')){
                        parser.nextToken();
                        teamId = parser.getText();
                    }
                // if Current Token is the Name field, go to Next Token and set teamName
                if((parser.getCurrentToken() == JSONToken.FIELD_NAME) && 
                   (parser.getText() == 'Name')){
                        parser.nextToken();
                        teamName = parser.getText();       
                   }
                // once we reach the end of the object...
                if((parser.getCurrentToken() == JSONToken.END_OBJECT)){
                    // If the roleMap doesn't contain the key (teamId)
                    if(!roleMap.containsKey(teamId)){
                        // add the related Id to the String,CustomRole(guid,parentId,name) map
                        roleMap.put(teamId,new CustomRole(teamId,null,teamName));
                    }

                    // then reset the team Id and team Name to null before next object in the parser
                    teamId = null;
                    teamName = null;   
                }
            }
            System.debug('Final Top Level KeySet: ' + roleMap.keySet());
            System.debug('Final Top Level Values: ' + roleMap.values());
        }catch(System.CalloutException e){
            System.debug('Callout Exception on GET TOP LEVEL' + e);
        }

        // change source for next call
        source = 'MSI-Apex-buildSubTeams';

        // for each role that was added to the roleMap in the first call
        for(CustomRole currentRole : roleMap.values()){
           // set a dynamic endpoint to get the subteam based on currentRole.guid
           httpReq.setEndpoint(endpoint+'/teams/'+ currentRole.guid +'/teams?apikey='+apikey+'&source='+source);

            try{
                // Get HTTP Callout Response
                HTTPResponse httpResp = http.send(httpReq);

                // Create Parser
                JSONParser parser = JSON.createParser(httpResp.getBody());
                while(parser.nextToken() != null){
                    // if Current Token is the Id field, go to Next Token and set teamId
                    if((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&
                    (parser.getText() == 'Id')){
                            parser.nextToken();
                            teamId = parser.getText();
                    }
                    // if Current Token is the Name field, go to Next Token and set teamName
                    if((parser.getCurrentToken() == JSONToken.FIELD_NAME) && 
                    (parser.getText() == 'Name')){
                        parser.nextToken();
                        teamName = parser.getText();       
                    }
                    // once we reach the end of the object...
                    if((parser.getCurrentToken() == JSONToken.END_OBJECT)){
                        // If the topLevelToIdMap contains the key (Internal or External)
                        if(roleMap.containsKey(currentRole.guid)){
                            roleMap.get(currentRole.guid).childRoles.put(teamId,new CustomRole(teamId,currentRole.guid,teamName));  
                        }

                        // then reset the team Id and team Name to null before next object in the parser
                        teamId = null;
                        teamName = null;   
                    }
                }
            }catch(System.CalloutException e){
                System.debug('Callout Exception on GET SUBTEAM LEVEL' + e);
            }  
        }
        System.debug('Final SubTeam Level KeySet: ' + roleMap.keySet());
        System.debug('Final SubTeam Level Values: ' + roleMap.values());      
    }   
}

Answer

There is a nested limit of 7 collections. If you attempt to go beyond this, you’ll get an error:

Nested type exceeds maximum level: 8


First question: Is this even possible? Am I exceeding any limits with a Map like this? I have looked over documentation and can’t find anything regarding nested map limits.

I know it’s in the documentation[citation needed]†, but it’s easy enough to test by using the following Execute Anonymous script:

Map<Id, Map<Id, Map<Id, Map<Id, Map<Id, Map<Id, Map<Id, Map<Id, Map<Id, Id>>>>>>>>> x;

However, assuming each role Id is unique, I don’t see why you couldn’t just use a simple:

Map<String, CustomRole> roles = new Map<String, CustomRole>();

Where CustomRole is just a class that stores the necessary data. To actually build the trees, perhaps in a Visualforce page, you still only need something like:

CustomRole[] roleTree = new CustomRole[0];

Where CustomRole would contain a list of children you could recursively iterate over. It’d probably be a lot easier to build the required functionality in JavaScript if that’s an option.


Second question: Since I will be storing data incrementally in the map (as detailed above), I need to store data in the map as I get it, and continue to build the map as I go through the loops and make the calls. I’ve never dealt with a nested map before, especially not a 6-level-deep nested map. How would I properly insert data into the map for the second level while maintaining the third, fourth, fifth, and sixth levels as null, and then move on to storing data in the third level while keeping the fourth, fifth, and sixth null, and so on, until the map is complete?

If you really wanted to torture yourself with this logic, you simply need to keep track of which layer you’re in. I’m not going to write this out explicitly, because just off the top of my head, it looks like you’re going to be looking at the 100+ lines of code ballpark. This really isn’t ideal when a simple recursive loop should be just fine. Starting with a simple Map<String, CustomRole> would be a lot easier to read and maintain.

† The documentation states there’s actually a limit of four nested elements, for a total of five deep, but the prior limit is seven nested elements for a total of eight deep. Execute Anonymous currently honors the 8-level construct, but it seems that salesforce is planning on reducing it, either because nobody uses it, or for some other optimization.

Attribution
Source : Link , Question Author : Morgan Marchese , Answer Author : sfdcfox

Leave a Comment