Implementing Nested Arrays in Solidity Smart Contracts

Using Structs, Arrays and Mappings to their Fullest Potential

Peter M. Ogwara
Coinmonks
Published in
4 min readOct 16, 2022

--

Introduction

Arrays are one of the most popular data types in object-oriented programming. By definition, an array is a data type that stores multiple variables of other data types within itself. In typical object-oriented programming, arrays are really powerful because they can also store arrays within themselves to almost infinite levels. Arrays within arrays, called nested arrays, are great representations of real-life data.

Solidity, however, is not a true object-oriented language (it’s contract-oriented) and its arrays are defined differently. For one, arrays in solidity can only accept variables of a single, predefined data type. Thus, a uint array can only accept uint values, a string array can only accept string variables, etc. And solidity arrays certainly cannot hold other arrays within itself. Nested arrays are impossible. As a programmer transitioning from Java, Python, or JavaScript to Solidity, one of the immediate questions therefore becomes how on earth do we implement arrays containing arrays in order to build well-structured on-chain metadata? Well, the answer is simple. We combine two unique-to-Solidity data types called mappings and structs with arrays.

What is a Struct?

A struct is a solidity data type used to define a record. In other words, structs hold a specific number of variables all of serarately pre-declared data types. A struct is declared as shown below:

struct People {
string name;
uint height_in_cm;
bytes gender;
}
People memory yours_truly = People("Peter Ogwara", 155, "m");

Thus a struct declaration merely gives a mould from which instances of it may be created, such as yours_tuly above.

What is a Mapping?

A mapping is a datatype that enables instances of one datatype to be paied with another. For example, a uint representing unique user id could be paired with an address, or a string representing a name to a uint representing an age. ERC tokens use this datatype to keep record of the each address’ token balance. Mappings are declared and set thus:

mapping(uint=>string) user_id_to_username;
uint id = 1;
user_id_to_username[id] = "peterogwara";

To call the inforation in a mapping, all that is needed is the value of the first variable. For example, to use the mapping above to get the username of the user with id 1, we would do the following.

uint id = 1;
username = user_id_to_username[id] //This will return "peterogwara"

Combining Arrays and Mappings with Structs

Here comes the fun part. Structs and mappings can be combined! To create a nested array one level deep, we can create a mapping of any primitive data type to a struct. Continuing with the People struct defined above and the idea of a unique id, here’s an example:

mapping(uint=>People) user_id_to_details;

The unique id could then be used to call up a lot more details than a single username. Any number of variables could be added to the struct of course, to make it more comprehensive in defining a person. Note that the mapping includes the specific name of the struct (People), and not just ‘struct’.

Going More Levels Deep

However, let’s say we want to include a list of each person’s tech stack and details of their pet cats in their details. We have to go an extra level deep. This can be achieved by declaring a string array for the tech stack within the people struct. Unfortunately, we cannot include a struct within a struct, or a nested mapping within a struct. However, we can construct separate structs for the same purpose and use arrays within the original struct to keep track of them, thus creating a CatDetails struct and a CatDetails array within the People struct. Our code would look something like this:

uint userCount;
string[] _techstack;
CatDetails[] _cats;
struct People {
string name;
uint height;
bytes gender;
string[] tech_stack;
CatDetails[] cats;
}
struct CatDetails {
string name;
uint age;
string color;
}
mapping (uint=>People) user_id_to_details;

Setting the variable values could then be achieved by first declaring the innermost array and setting its values, and then doing same for the struct in the outermost level. The global arrays declared at the beginning of the contract provide a storage location for in-function declarations to point to.

function setUser() public {
uint userId = userCount++;
string[] storage my_techstack = _techstack;
my_techstack.push("Python");
my_techstack.push("JavaScript");
my_techstack.push("Solidity");
CatDetails[] storage cats = _cats;
CatDetails memory firstcat = CatDetails("Snuffles", 3, 'white');
cats.push(firstcat);
CatDetails memory secondcat = CatDetails("Garfield", 5, 'orange');
cats.push(secondcat);
People memory yours_truly = People("Peter M. Ogwara", 155, "m", my_techstack, cats);
user_id_to_details[userId] = yours_truly;
}

And just like that, we have nested arrays/structs! Calling the data is quite simple and done in the conventional mapping/struct/array calling format. For example, the line below will return the name of the second cat of user #1.

return user_id_to_details[1].my_cats[1].name;

Finally, be sure to include checks to make sure the index exists before calling for values.

🌟 Feel free to ask questions! 🌟

Follow me for more Blockchain, Javascript, PHP, Python, or pure programming stories.

Medium| Twitter | Github | Amazon

New to trading? Try crypto trading bots or copy trading

--

--

Peter M. Ogwara
Coinmonks

Academic Writer, Full-Stack Web Developer, Web3 Enthusiast