Structs and Mappings
Lab deck 12: grouping the certificate's loose variables into a struct, declaring and using struct variables, two ways to assign fields, storing many certificates in a mapping keyed by certificate ID, the mapping[key].field pattern, default values and the limits of mappings, and the improvement exercises.
The problem: loose variablesSearch on YouTube
After L13 the UniversityDegree contract has seven loose state variables: certificate ID, name, register number, degree, department, graduation year and validity.
- All of them belong to one certificate.
- They are seven disconnected, independent variables.
- How do we issue hundreds of certificates to graduating students?
- More students means more variables: harder to manage and code, and more chances of errors.
Whenever several attributes describe one real-world object, we group them together. In Solidity this tool is called struct.
StructSearch on YouTube
A struct (short for structure) is a composite, user-defined data type that packages several logically related variables under one name. Solidity has built-in types like uint256, string, bool and address. A struct lets you invent a new one, such as Certificate, that bundles several of them.
struct Certificate {
uint256 certificateId;
string studentName;
uint256 registerNumber;
string degree;
string department;
uint256 graduationYear;
bool isValid;
}The items inside (studentName, degree and so on) are called fields or members.
Certificate is a user-defined type, not a built-in one. Eventually we need a list like this:
Certificate 101 → Student A
Certificate 102 → Student B
Certificate 103 → Student C
and so on...Before storing many certificates, we must first define what one certificate looks like.
Declaring a structSearch on YouTube
struct Certificate {
...
}Declaring a struct defines a template. It does not create any certificate and uses no storage. Storage is used only when you declare a variable of that type.
Creating a struct variableSearch on YouTube
Certificate public certificate;| Part | Meaning |
|---|---|
Certificate | The struct data type |
public | Visibility (auto-generates a getter) |
certificate | The variable name |
This certificate variable has every field defined in the struct.
Accessing fields: the dot operatorSearch on YouTube
Read or write one field with variableName.fieldname:
certificate.certificateId = 101;
certificate.studentName = "Rahul";
certificate.degree = "B.Tech in Cyber";
certificate.isValid = true;Assigning values to membersSearch on YouTube
Approach 1: field by field
certificate.certificateId = _certificateId;
certificate.studentName = _studentName;
certificate.registerNumber = _registerNumber;
certificate.degree = _degree;
certificate.department = _department;
certificate.graduationYear = _graduationYear;
certificate.isValid = true;Approach 2: all at once
certificate = Certificate(
_certificateId,
_studentName,
_registerNumber,
_degree,
_department,
_graduationYear,
true
);In approach 2 the arguments must follow the field order of the struct. Solidity also accepts named arguments, Certificate({certificateId: _certificateId, ...}), which do not depend on order.
Version 4: one struct variableSearch on YouTube
Deploying v4 changes only how the variables are handled. Calling the single certificate getter returns all members together:
0: uint256: certificateId 111
1: string: studentName jai
2: uint256: registerNumber 123
3: string: degree B.Tech
4: string: department Cyber
5: uint256: graduationYear 2026
6: bool: isValid trueNow the certificate data is organised. But it is still only one certificate. If issueCertificate() is called again, it wipes the existing data and writes the new data. We need to store many certificates. That is where mapping comes in.
MappingSearch on YouTube
A mapping is a key → value lookup table. You give it a key and it returns the value stored under that key.
mapping(KeyType => ValueType) variableName;
mapping(uint256 => Certificate) public certificates;In this application the key is the certificate ID:
101 → Certificate of Jai
102 → Certificate of Sam
103 → Certificate of LizA mapping is like a Python dictionary or a hash table, but with every possible key already present and set to a default value.
Storing a certificateSearch on YouTube
certificates[_certificateId] selects the slot for that ID. Replace certificate. with certificates[_certificateId].:
| One struct variable | Mapping of structs |
|---|---|
certificate.certificateId = _certificateId; | certificates[_certificateId].certificateId = _certificateId; |
certificate.studentName = _studentName; | certificates[_certificateId].studentName = _studentName; |
certificate.registerNumber = _registerNumber; | certificates[_certificateId].registerNumber = _registerNumber; |
certificate.degree = _degree; | certificates[_certificateId].degree = _degree; |
certificate.department = _department; | certificates[_certificateId].department = _department; |
certificate.graduationYear = _graduationYear; | certificates[_certificateId].graduationYear = _graduationYear; |
certificate.isValid = true; | certificates[_certificateId].isValid = true; |
Issuing two certificatesSearch on YouTube
issueCertificate(101, "Rahul", 1001, "B.Tech", "Cyber", 2026)
=> certificates[101] = { certificateId: 101, studentName: Rahul, registerNumber: 1001,
degree: B.Tech, department: Cyber, graduationYear: 2026, isValid: true }
issueCertificate(102, "James", 1002, "B.Tech", "Cyber", 2026)
=> certificates[102] = { certificateId: 102, studentName: James, registerNumber: 1002,
degree: B.Tech, department: Cyber, graduationYear: 2026, isValid: true }They do not overwrite each other, because each ID is a different key.
Key In, Value Out: "KIVO". A mapping takes a key and gives the value; mapping(Key => Value) is written in the same order.
Retrieving a certificateSearch on YouTube
Now the function must be told which certificate we want. The pattern is mapping[key].field:
function getCertificate(uint256 _certificateId)
public
view
returns (uint256, string memory, uint256, string memory, string memory, uint256, bool)
{
return (
certificates[_certificateId].certificateId,
certificates[_certificateId].studentName,
certificates[_certificateId].registerNumber,
certificates[_certificateId].degree,
certificates[_certificateId].department,
certificates[_certificateId].graduationYear,
certificates[_certificateId].isValid
);
}Version 5: full contractSearch on YouTube
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract UniversityDegree {
address public university;
struct Certificate {
uint256 certificateId;
string studentName;
uint256 registerNumber;
string degree;
string department;
uint256 graduationYear;
bool isValid;
}
mapping(uint256 => Certificate) public certificates;
constructor() {
university = msg.sender;
}
function issueCertificate(
uint256 _certificateId,
string memory _studentName,
uint256 _registerNumber,
string memory _degree,
string memory _department,
uint256 _graduationYear
) public {
require(msg.sender == university, "Only university can issue");
certificates[_certificateId] = Certificate(
_certificateId, _studentName, _registerNumber,
_degree, _department, _graduationYear, true
);
}
function getUniversity() public view returns (address) {
return university;
}
function getCaller() public view returns (address) {
return msg.sender;
}
}In Remix the deck issues three certificates (1111 Jai, 2222 James, 3333 Liza) and reads each back by entering its ID in the certificates getter. The public mapping's getter takes the key as its parameter.
What a mapping cannot doSearch on YouTube
Query a key that was never set, such as 0 or 9999. The getter returns all default values: certificateId 0, empty strings, graduationYear 0, isValid false.
- A mapping does not tell you whether a record exists. Every key "exists" with default values.
- A mapping is for lookup, not listing. You cannot ask it to print all certificates from 1 to 100, and you cannot get its length or loop over its keys.
Use a field such as isValid (false by default) to test existence. To list records, keep a separate array of IDs alongside the mapping.
Improvement exercisesSearch on YouTube
The deck asks you to improve the code:
- Prevent duplicate certificate IDs. The university can issue 101 and then 101 again; the second transaction overwrites the first.
- Add certificate revocation. There is no way to invalidate a certificate.
- Add a dedicated verification function.
getCertificate()returns everything, but an employer only wants to ask "Is certificate 101 currently valid?"
One possible solution (not from the slides):
// 1. inside issueCertificate, before writing
require(certificates[_certificateId].certificateId == 0, "Certificate ID already used");
// 2. revocation
function revokeCertificate(uint256 _certificateId) public {
require(msg.sender == university, "Only university can revoke");
require(certificates[_certificateId].isValid, "Not an active certificate");
certificates[_certificateId].isValid = false;
}
// 3. verification
function verifyCertificate(uint256 _certificateId) external view returns (bool) {
return certificates[_certificateId].isValid;
}Check 1 compares with 0, the default for an unused key. This works as long as ID 0 is never issued.
What to do and check in the lab:
- Replace the seven loose variables with
struct Certificateand oneCertificate public certificate;. Deploy v4 and read all members through one getter. - Issue twice and see the second overwrite the first.
- Switch to
mapping(uint256 => Certificate) public certificates;(v5). Issue 1111, 2222 and 3333, and read each by ID. - Query an unused ID (0, 9999) and explain the default values.
- Add duplicate prevention, revocation and a
verifyCertificateview function.
Quick self-testSearch on YouTube
What is a struct and why did the certificate contract need one?
A user-defined composite type that groups related variables under one name. The contract had seven disconnected variables for one certificate; a struct bundles them into one Certificate type.
Viva: does declaring struct Certificate { ... } create a certificate?
No. It only defines a template. A certificate exists only when you declare a variable of that type, such as Certificate public certificate;, or store one in a mapping.
How do you read or write one field of a struct variable?
With the dot operator, variableName.fieldname, for example certificate.studentName = "Rahul";.
Show the two ways to assign values to a struct.
Field by field (certificate.certificateId = _certificateId; and so on for each field), or all at once (certificate = Certificate(_certificateId, _studentName, ..., true); in field order).
Why was a single struct variable still not enough?
It holds only one certificate. Calling issueCertificate() again overwrites the existing data with the new data.
Write the syntax of a mapping and the one used in the lab.
mapping(KeyType => ValueType) variableName;. In the lab: mapping(uint256 => Certificate) public certificates;, keyed by certificate ID.
What pattern reads a field of a struct stored in a mapping?
mapping[key].field, for example certificates[_certificateId].studentName.
Viva: you query certificates(9999) and get zeros, empty strings and false. Why?
Mappings return default values for any key never written. They cannot tell whether a record exists; every key appears to exist with defaults.
Can you list all certificates stored in a mapping? How would you work around it?
No. Mappings are only for lookup: no length, no key list, no iteration. Keep a separate array of issued IDs and push each new ID into it.
How would you stop the same certificate ID from being issued twice?
Add a check before writing, for example require(certificates[_certificateId].certificateId == 0, "Certificate ID already used");.
- Seven loose variables for one certificate are hard to manage; a struct groups them into one user-defined type.
struct Certificate { ... }is a template; its items are fields or members.- Declare
Certificate public certificate;, access withcertificate.field, assign field by field or all at once withCertificate(...). - One struct variable holds one certificate; a new issue overwrites it.
mapping(uint256 => Certificate) public certificates;stores many certificates keyed by ID; they do not overwrite each other.- Read with
certificates[id].field; the public getter takes the key as input. - Unset keys return defaults; mappings are for lookup, not listing.
- Exercises: prevent duplicate IDs, add revocation, add a verify function.