nlohmann::json
library.In previous lessons, we covered how our programs can interact with data on our file system. We streamed data from our program to create new files, and read input from files to modify our program's state in some way.
In those examples, we read and write our data in unformatted strings. In this lesson, we’ll introduce the JSON format, and how to use it in C++
We’ll be using a popular third-party library, nlohmann::json
, which lets us work much more quickly than trying to do everything ourselves.
JSON (JavaScript Object Notation) is a text-based data format that is designed to be readable and understood by humans, whilst also being sufficiently structured to be easily parsed by machines.
Below is an example of a JSONÂ document:
{
"name": "Roderick",
"class": "Barbarian",
"level": 5,
"health": 100,
"isOnline": true,
"pet": null,
"guild": {
"name": "The Fellowship",
"members": 36
},
"equipment": [
{
"name": "Fiery Sword",
"damage": 5,
"critChance": 0.2
},
{
"name": "Iron Helmet",
"armor": 30,
"durability": 4
}
]
}
JSON documents are comprised of four primitive types and two structured types.
The primitive types are booleans, numbers, strings, and null
. The structured types are objects and arrays.
From composing just these 6 basic types, we can describe almost anything.
null
Booleans, numbers, and strings in JSON broadly follow the same syntax as C++.
true
and false
2
, 3.14
, and -6
"Hello"
null
represents an empty value, conceptually similar to keywords like nullptr
and void
in C++A string, boolean, number, or null
are, in isolation, valid examples of JSON. However, it is somewhat pointless to use JSON to store or transmit something so simple.
More typically, our JSON will be an object or an array.
Our previous example of an adventurer was a JSON object. Objects start with {
, and end with }
. Objects contain collections of key-value pairs, separated by commas: ,
{
"movie": "Star Wars",
"released": true,
"year": 1977
}
Each key is a string. The keys can contain spaces, but it’s typically avoided because it can make the JSON difficult to work with when it is imported into an environment where variables typically cannot contain spaces.
As such, we tend to use camelCase or snake_case for our key names.
The object values can be any type, including an array or another object:
{
"movie": "Star Wars",
"released": true,
"year": 1977,
"director": {
"firstName": "George",
"surname": "Lucas",
"born": 1944
},
"cast": [
"Mark Hamill",
"Harrison Ford",
"Carrie Fisher"
]
}
Within our previous example, the value under the "equipment"
key was an example of an array. Arrays start with [
, end with ]
and contain a collection of values, separated by commas: ,
Each element in the array can be a value of any other type, including an object or a nested array.
[4, null, { "ready": true }, [1, 2, 3], "hello"]
Similar to C++, JSON is generally not sensitive to spacing. The following are equivalent:
{
"movie": "Star Wars",
"year": 1977,
"cast": [
"Mark Hamill",
"Harrison Ford",
"Carrie Fisher"
]
}
{ "movie": "Star Wars", "year": 1977, "cast":
[ "Mark Hamill", "Harrison Ford", "Carrie Fisher" ]
}
When our JSON documents are formatted to be readable by humans, we tend to indent them in a similar way we indent code. However, this is not required - we are free to lay out our documents how we want.
Machine-generated JSON tends to eliminate unnecessary white space. This helps with performance but tends to make the documents less readable by humans.
When we want to view JSON documents in a more readable way, we have a few options.
Code editors and web browsers can generally format, or "prettify" JSON documents. Some support this natively, but almost all can do it with a plugin.
Alternatively, many free online tools let us copy and paste JSON, and hit a button to quickly lay it out in a human-friendly way. Doing a web search for "JSON formatter" will yield many options.
How we order keys within JSON objects is not significant. The following two objects are equivalent:
{
"movie": "Star Wars",
"year": 1977,
}
{
"year": 1977,
"movie": "Star Wars",
}
The library we’ll be using in this lesson tends to order keys in alphabetical order, but any ordering is valid.
However, the ordering of items within arrays is important. The following two arrays are not equivalent:
["Mark Hamill", "Harrison Ford"]
["Harrison Ford", "Mark Hamill"]
nlohmann::json
)C++ does not natively support the JSON data format, but there are many options from third-party libraries we can choose from. In this lesson, we’ll use the "JSON for Modern C++" library, which is commonly referred to nlohmann::json
among developers who are familiar with it.
The library’s homepage is available here: https://json.nlohmann.me/
We can install this library through our package manager. For example, vcpkg users can install it using the terminal command:
.\vcpkg install nlohmann-json
Alternatively, given the library is "header-only", we can just grab the header and copy it into our project. The latest header is available from the GitHub releases page: https://github.com/nlohmann/json/releases/
From the "assets" section at the bottom of the release, we should download json.hpp
and copy it into our project.
Once we’ve acquired the library, we then #include
it in our files in the normal way. The exact path will depend on how our include directories are set up, and where the header file is located. But, we likely need either:
#include <nlohmann/json.hpp>
Or:
#include <json.hpp>
Once included, the most important class within this library is nlohman::json
, which we typically alias to json
:
#include <json.hpp>
using json = nlohmann::json;
int main() {
json Document;
}
The nlohmann::json
class includes the static method parse
, which is one of the ways we can convert a raw JSON string to a nlohmann::json
 object:
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{
R"({ "name": "Roderick", "role": "Barbarian" })"};
json Doc{json::parse(Data)};
}
Once we’ve created a nlohmann::json
object from our string, we can begin working with it using regular C++Â techniques.
Similar to standard library containers such as arrays and maps, we can read values from the JSON objects using the []
 operator.
When reading from JSON objects, we’d pass the name of the key we want to access:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{
R"({ "name": "Roderick", "role": "Barbarian" })"};
json Doc{json::parse(Data)};
std::string Name{Doc["name"]};
std::string Role{Doc["role"]};
std::cout << "Name: " << Name;
std::cout << "\nRole: " << Role;
}
Name: Roderick
Role: Barbarian
When reading from JSON arrays, we’d instead pass the numeric index we want to read:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(["Roderick", "Anna"])"};
json Doc{json::parse(Data)};
std::string Player1{Doc[0]};
std::string Player2{Doc[1]};
std::cout << "Player 1: " << Player1;
std::cout << "\nPlayer 2: " << Player2;
}
Player 1: Roderick
Player 2: Anna
We can chain the []
operator to read deeply nested values:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian",
"guild": {
"name": "The Bandits"
},
"equipment": [{
"name": "Iron Sword",
"damage": 5
}]
}
)"};
json Doc{json::parse(Data)};
std::string GuildName{Doc["guild"]["name"]};
int WeaponDamage{
Doc["equipment"][0]["damage"]};
std::cout << "Guild Name: " << GuildName;
std::cout << "\nWeapon Damage: "
<< WeaponDamage;
}
Guild Name: The Bandits
Weapon Damage: 5
As an alternative to the []
operator, we can instead use the at
 method.
The main difference is that the at
method throws an exception if the key or index we’re trying to access does not exist:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian"
}
)"};
json Doc{json::parse(Data)};
try {
Doc.at("does-not-exist");
} catch (...) {
std::cout << "That didn't work!";
}
}
That didn't work!
Exceptions from the library all inherit from nlohmann::json::exception
, which inherits from std::exception
:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian"
}
)"};
json Doc{json::parse(Data)};
try {
Doc.at("does-not-exist");
} catch (json::exception e) {
std::cout << e.what();
}
}
[json.exception.out_of_range.403]
key 'does-not-exist' not found
The most likely problem we’ll encounter when first using JSON is that our JSON strings are invalid. Attempting to convert such a string to a JSON object will result in a nlohmann::json::parse
 exception.
In the following example, we removed the closing }
from our JSON string, making it invalid:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian"
}
)"};
try {
json Doc{json::parse(Data)};
} catch (json::parse_error e) {
std::cout << e.what();
}
}
[json.exception.parse_error.101]
parse error at line 6, column 3
syntax error while parsing object
unexpected end of input; expected '}'
The json::accept
method can tell us if a string is valid JSON before we try to parse it:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian"
}
)"};
if (json::accept(Data)) {
std::cout << "It looks valid!";
}
}
It looks valid!
A thorough list of all exceptions that can occur is available on the official site: https://json.nlohmann.me/home/exceptions/
The previous examples implicitly map types within the JSON document to C++ types like std::string
and int
. This is generally okay for simple types like these, but the approach can run into issues when dealing with more complex types.
As such, there are alternative, more recommended ways of converting JSON content to C++Â types.
The get
method on the nlohmann::json
class has a template parameter that allows us to specify the exact conversion we want.
For example, to get the name
value from a JSON document as a std::string
, we’d use this function call:
Doc["name"].get<std::string>()
Below, we explicitly create a std::string
from a JSON string, and a std::vector
from a JSONÂ array:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"({
"greeting": "Hello!",
"numbers": [1, 2, 3]
})"};
json Doc{json::parse(Data)};
auto Greeting{
Doc["greeting"].get<std::string>()};
auto Numbers{
Doc["numbers"].get<std::vector<int>>()};
std::cout << Greeting << '\n';
for (int i : Numbers) {
std::cout << i;
}
}
Hello!
123
We’re not restricted to using get()
on part of the JSON document. Sometimes we’ll want to convert the entire document into a specific C++ type. Below, our entire document is a JSON array, which we insert into a std::vector
:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
json Doc{json::parse(R"([1, 2, 3])")};
auto Numbers{Doc.get<std::vector<int>>()};
for (int i : Numbers) {
std::cout << i;
}
}
123
When the variable we want to store the data in already exists, we can use the get_to
method instead. This can automatically infer the required type, using the type of argument we pass to the function:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
json Doc{json::parse(R"([1, 2, 3])")};
std::vector<int> Numbers;
Doc.get_to(Numbers);
for (int i : Numbers) {
std::cout << i;
}
}
123
We can convert JSON objects to associative containers, such as std::map
, using the same get
and get_to
methods we covered previously:
#include <iostream>
#include <json.hpp>
#include <map>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian"
}
)"};
json Doc{json::parse(Data)};
std::map<std::string, std::string> Map;
Doc.get_to(Map);
std::cout << "Name: " << Map["name"];
std::cout << "\nRole: " << Map["role"];
}
Generally, this will only be useful if every value in our JSON object has the same type, such as strings in the previous examples.
More commonly, our object values will span a variety of types, in which case we’ll generally want to store them in a user-defined class or struct. We’ll show you how to set that up later in this lesson.
The nlohmann::json
type has been set up to interact with stream operators, so we can easily send its contents to any stream. For example, we can view its contents by sending it to std::cout
:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian"
}
)"};
json Doc{json::parse(Data)};
std::cout << Doc;
}
{"name":"Roderick","role":"Barbarian"}
We’re not restricted to streaming the entire document - we can stream any sub-part of the JSON tree:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian",
"guild": {
"name": "The Bandits"
}
}
)"};
json Doc{json::parse(Data)};
std::cout << Doc["guild"];
}
{"name":"The Bandits"}
Additionally, the dump()
method returns the current state of the JSON tree, or any sub-part, as a std::string
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian",
"guild": {
"name": "The Bandits"
}
}
)"};
json Doc{json::parse(Data)};
std::string JSON{Doc.dump()};
std::cout << JSON;
}
{"guild":{"name":"The Bandits"},"name":"Roderick","role":"Barbarian"}
The dump()
method accepts an optional numeric argument. If provided, additional spacing for new lines and indentation will be added to our string. This will make it easier to read for humans.
The value of the numeric argument specifies how many spaces we want to use for indentation. The most common selections are 2
or 4
:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian",
"guild": {
"name": "The Bandits"
}
}
)"};
json Doc{json::parse(Data)};
std::cout << Doc.dump(2);
}
{
"guild": {
"name": "The Bandits"
},
"name": "Roderick",
"role": "Barbarian"
}
This can help with debugging but, generally, we don’t want to use this for real systems. The additional space characters make our JSON documents bigger, and slightly slower to parse for computers.
Meanwhile, humans who are regularly working with JSON documents will already have the tools to format them into a human-friendly format when needed. JSON formatting is a standard feature of most code editors, either natively or as a plugin.
To make our custom types compatible with nlohmann::json
, we need to provide two functions:
to_json
, which provides a reference to a JSON object, and a const
reference to our custom object. We update the JSON object based on the state of our custom object.from_json
, which provides a const
reference to a JSON object, and a reference to our custom object. We update our custom object, based on the contents of the JSON object.The following shows how we can set this up with a simple Character
 type:
#pragma once
#include <json.hpp>
#include <string>
using json = nlohmann::json;
class Character {
public:
std::string Name;
int Level;
};
void to_json(json& j, const Character& C) {
j = json{{"Name", C.Name},
{"Level", C.Level}};
}
void from_json(const json& j, Character& C) {
j.at("Name").get_to(C.Name);
j.at("Level").get_to(C.Level);
}
With those changes in place, we can use functions like get
and get_to
with our custom type:
#include <iostream>
#include <json.hpp>
#include "Character.h"
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"Name": "Roderick",
"Level": 10
}
)"};
json Doc{json::parse(Data)};
Character Player;
Doc.get_to(Player);
std::cout << "Name: " << Player.Name;
std::cout << "\nLevel: " << Player.Level;
}
Name: Roderick
Level: 10
Similarly, we can easily generate a JSON representation of our objects:
#include <iostream>
#include <json.hpp>
#include "Character.h"
using json = nlohmann::json;
int main() {
Character Player{"Roderick", 10};
json Doc(Player);
std::cout << Doc.dump(2);
}
{
"Level": 10,
"Name": "Roderick"
}
Note, our initialization of Doc
above is using parenthesis (
and )
when we’d typically use braces {
and }
This is because the nlohmann::json
type can be created from an initializer list, and that is how our code would have been interpreted had we used braces. Even though we pass only one argument, it would be treated as a list of length 1
.
This results in the creation of a JSON array, when we may instead have intended to create a JSONÂ object.
#include <iostream>
#include <json.hpp>
#include "Character.h"
using json = nlohmann::json;
int main() {
Character Player1{"Roderick", 10};
Character Player2{"Anna", 15};
json DocA(Player1);
json DocB{Player1};
json DocC{Player1, Player2};
std::cout << "Doc A:\n" << DocA.dump(2);
std::cout << "\n\nDoc B:\n" << DocB.dump(2);
std::cout << "\n\nDoc C:\n" << DocC.dump(2);
}
Doc A:
{
"Level": 10,
"Name": "Roderick"
}
Doc B:
[
{
"Level": 10,
"Name": "Roderick"
}
]
Doc C:
[
{
"Level": 10,
"Name": "Roderick"
},
{
"Level": 15,
"Name": "Anna"
}
]
In our previous example where we added JSON serialization to our custom type, we defined full-fledged to_json
and from_json
 functions.
void to_json(json& j, const Character& C) {
j = json{{"Name", C.Name},
{"Level", C.Level}};
}
void from_json(const json& j, Character& C) {
j.at("Name").get_to(C.Name);
j.at("Level").get_to(C.Level);
}
This gave us a lot of flexibility but is not always necessary. The nlohmann::json
library includes some macros that cover most use cases.
The above code can be replaced with this macro call, which will generate the two required functions:
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Character,
Name,
Level)
The first argument to the macro is the type we want to add serialization capabilities to. The remaining arguments are the names of the fields we want to serialize. This assumes that the fields we want to serialize are public
within our class or struct.
If they’re not public, we can instead use the "intrusive" form of our macro, within the public area of our class:
#pragma once
#include <json.hpp>
#include <string>
using json = nlohmann::json;
class Character {
public:
std::string Name;
int GetLevel() { return Level; }
NLOHMANN_DEFINE_TYPE_INTRUSIVE(Character,
Name,
Level)
private:
int Level;
};
Both of these macros also assume the name of the fields we want to serialize within our class in our class identically match the name of the corresponding keys within the JSON. If that’s not the case, we need to revert to writing the to_json
and from_json
functions manually.
JSON documents from the nlohmann::json
library correctly implement the =
operator, so we can create and update their values as we might expect:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
json Doc;
Doc["name"] = "Roderick";
Doc["role"] = "Barbarian";
Doc["guild"]["name"] = "The Bandits";
std::cout << Doc.dump(2);
}
{
"guild": {
"name": "The Bandits"
},
"name": "Roderick",
"role": "Barbarian"
}
We can create arrays as lists of values, and objects as lists of pairs, where the first element is the key, and the second element is the value:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
json Doc;
Doc["movie"] = "Star Wars";
Doc["released"] = true;
Doc["year"] = 1977;
// Array
Doc["cast"] = {"Mark Hamill", "Harrison Ford",
"Carrie Fisher"};
// Object
Doc["director"] = {{"name", "George Lucas"},
{"born", 1944}};
// Array of Objects
Doc["similar_movies"] = {
{{"name", "Dune"}, {"year", 2021}},
{{"name", "Rogue One"}, {"year", 2016}}};
std::cout << Doc.dump(2);
}
{
"cast": [
"Mark Hamill",
"Harrison Ford",
"Carrie Fisher"
],
"director": {
"born": 1944,
"name": "George Lucas"
},
"movie": "Star Wars",
"released": true,
"similar_movies": [
{
"name": "Dune",
"year": 2021
},
{
"name": "Rogue One",
"year": 2016
}
],
"year": 1977
}
The library implements appropriate converters for standard library containers, so we can automatically generate JSON arrays from containers like std::array
and std::vector
. We can also generate JSON objects from associative containers, like a std::map
.
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::vector Cast{"Mark Hamill",
"Harrison Ford",
"Carrie Fisher"};
json Doc;
Doc["cast"] = Cast;
std::cout << Doc.dump(2);
}
{
"cast": [
"Mark Hamill",
"Harrison Ford",
"Carrie Fisher"
]
}
When we want to create a JSON document from a string, we can use the parse
method, as described right at the start of the lesson.
However, there is a more succinct way of doing it. By including a using
statement for the nlohmann::literals
namespace, we can create a nlohmann::json
by appending _json
to the end of a raw string. It looks like this:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
using namespace nlohmann::literals;
json Doc{R"({
"movie": "Star Wars",
"released": true,
"year": 1977,
"cast": [
"Mark Hamill",
"Harrison Ford",
"Carrie Fisher"
],
"director": {
"born": 1944,
"name": "George Lucas"
}
})"_json};
std::cout << Doc.dump(2);
}
{
"cast": [
"Mark Hamill",
"Harrison Ford",
"Carrie Fisher"
],
"director": {
"born": 1944,
"name": "George Lucas"
},
"movie": "Star Wars",
"released": true,
"year": 1977
}
Alternatively, we can define our JSON document using C++ syntax, by combining a composition of lists and pairs:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
json Doc{
{"movie", "Star Wars"},
{"released", true},
{"year", 1977},
{"cast", {1, 0, 2}},
{"director",
{{"name", "George Lucas"},
{"born", 1944}}},
{"related_movies",
{{{"name", "Rogue One"}, {"year", 2016}},
{{"name", "Dune"}, {"year", 2021}}}}};
std::cout << Doc.dump(2);
}
This can get quite difficult to follow for more complex documents. The raw string approach is generally going to be more readable.
However, unlike raw strings, the C++ style makes it easier to weave in C++ objects and containers. This is a more common requirement than creating static JSON documents from strings. It looks like this:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
struct Person {
std::string name;
int born;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Person,
name,
born)
struct RelatedMovie {
std::string name;
int year;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(RelatedMovie,
name,
year)
int main() {
Person Director{"George Lucas", 1944};
std::vector<std::string> Cast{
"Mark Hamill", "Harrison Ford",
"Carrie Fisher"};
std::vector<RelatedMovie> RelatedMovies{
{"Rogue One", 2016}, {"Dune", 2021}};
json Doc{{"movie", "Star Wars"},
{"released", true},
{"year", 1977},
{"cast", Cast},
{"director", Director},
{"related_movies", RelatedMovies}};
std::cout << Doc.dump(2);
}
{
"cast": [
"Mark Hamill",
"Harrison Ford",
"Carrie Fisher"
],
"director": {
"born": 1944,
"name": "George Lucas"
},
"movie": "Star Wars",
"related_movies": [
{
"name": "Rogue One",
"year": 2016
},
{
"name": "Dune",
"year": 2021
}
],
"released": true,
"year": 1977
}
The nlohmann::json
type has implemented the stream operators >>
and <<
to allow us to read and write our JSON documents to any stream, including file streams.
We covered accessing the file system, and using file streams in dedicated lessons earlier in the course:
Below, we save our JSON to a file on our hard drive:
#include <fstream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
using namespace nlohmann::literals;
json Doc{R"({
"movie": "Star Wars",
"released": true,
"year": 1977
})"_json};
std::fstream File;
File.open(R"(c:\test\file.json)",
std::ios::out);
File << Doc;
}
The json::parse
method accepts an input stream as an argument, which allows us to generate a JSON document from a file stream. Below, we read the file from our hard drive that the previous example created:
#include <fstream>
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::fstream File;
File.open(R"(c:\test\file.json)",
std::ios::in);
json Doc{json::parse(File)};
std::cout << Doc.dump(2);
}
{
"movie": "Star Wars",
"released": true,
"year": 1977
}
Our previous lesson on file streams included a lesson showing how we can save and load the state of objects in our program using files on the user’s hard drive. That example used unstructured strings. Below, we upgrade our approach to use JSON instead:
#pragma once
#include <json.hpp>
#include <string>
using json = nlohmann::json;
class Character {
public:
std::string Name;
int Level;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Character,
Name,
Level)
#include <filesystem>
#include <fstream>
#include <iostream>
#include "Character.h"
namespace fs = std::filesystem;
int main() {
Character Player;
std::fstream File;
fs::directory_entry SaveFile{
R"(c:\test\savefile.json)"};
if (SaveFile.is_regular_file()) {
std::cout << "Loading a saved game\n";
File.open(SaveFile, std::ios::in);
json Data = json::parse(File);
Data.get_to(Player);
} else {
std::cout << "Starting a new game\n";
File.open(SaveFile, std::ios::out);
Player.Name = "Conan";
Player.Level = 1;
json Data = Player;
File << Data;
}
File.close();
std::cout << "Name: " << Player.Name;
std::cout << "\nLevel: " << Player.Level;
}
The first run of our program saves a file on our hard drive, and generates this output:
Starting a new game
Name: Conan
Level: 1
Subsequent runs read the file, and generate this output:
Loading a saved game
Name: Conan
Level: 1
JSON documents include support for iterators, allowing us to traverse over values in our JSON objects, or elements in our JSONÂ arrays.
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian",
"level": 10,
"guild": {"name": "The Bandits" }
}
)"};
json Doc{json::parse(Data)};
for (auto& Item : Doc) {
std::cout << Item << '\n';
}
}
{"name":"The Bandits"}
10
"Roderick"
"Barbarian"
If we want access to both keys and values, we have two options. We can directly use the iterators, which include key
and value
 methods:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian",
"level": 10,
"guild": {"name": "The Bandits" }
}
)"};
json Doc{json::parse(Data)};
for (auto it = Doc.begin(); it != Doc.end();
++it) {
std::cout << "Key: " << it.key()
<< ", Value: " << it.value()
<< '\n';
}
}
Key: guild, Value: {"name":"The Bandits"}
Key: level, Value: 10
Key: name, Value: "Roderick"
Key: role, Value: "Barbarian"
Alternatively, we can use the items()
method, which returns a collection of key-value pairs. This is compatible with a range-based for loop:
for (auto& Item : Doc.items()) {
std::cout << "Key: " << Item.key()
<< ", Value: " << Item.value()
<< '\n';
}
Each pair also supports structured binding:
for (auto& [key, value] : Doc.items()) {
std::cout << "Key: " << key
<< ", Value: " << value << '\n';
}
Note, that neither of these methods is recursive - they only iterate over the first-level keys or values that they’re called upon. If needed, we could build our recursive code to traverse documents deeply. The is_structured()
method, covered later in this lesson, would be helpful for this.
The nlohmann::json
type overloads the ==
and !=
operators, allowing us to compare JSONÂ documents:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"role": "Barbarian"
}
)"};
std::string Data2{R"(
{
"role": "Barbarian",
"name": "Roderick"
}
)"};
json Doc1{json::parse(Data)};
json Doc2{json::parse(Data)};
if (Doc1 == Doc2) {
std::cout << "Documents are equivalent\n";
}
Doc1["name"] = "Anna";
if (Doc1 != Doc2) {
std::cout << "Not any more!";
}
}
Documents are equivalent
Not any more!
The nlohmann::json
library includes a range of additional methods we may find situationally helpful:
We can check the type of our JSON document, or any element within it, using a range of helper methods:
is_number()
returns true
if the element is any numberis_number_integer()
returns true
if the element is an integeris_number_float()
returns true
if the element is a floating point numberis_boolean()
returns true
if the element is a booleanis_null()
returns true
if the element is a nullis_string()
returns true
if the element is a stringis_array()
returns true
if the element is an arrayis_object()
returns true
if the element is an objectis_primitive()
returns true
if the element is a number, boolean, null, or stringis_structured()
returns true
if the element is an array or objectSome examples of using these methods are below:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"level": 10
}
)"};
json Doc{json::parse(Data)};
if (Doc.is_object()) {
std::cout << "Document is a JSON object\n";
}
if (Doc.at("name").is_string()) {
std::cout << "Name is a JSON string\n";
}
if (Doc.at("level").is_number()) {
std::cout << "Level is a JSON number\n";
}
}
Document is a JSON object
Name is a JSON string
Level is a JSON number
size()
and empty()
MethodsWe can use the size()
method to find out how big our document, or part of a document, is. The return value varies depending on the JSON type we’re calling it on:
size()
returns the number of keyssize()
returns the number of elementssize()
returns 1null
values or missing keys, size()
returns 0#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"level": 10,
"equipment": [{}, {}]
}
)"};
json Doc{json::parse(Data)};
std::cout << "Document Size: " << Doc.size();
std::cout << "\nEquipment Size: "
<< Doc.at("equipment").size();
std::cout << "\nEquipment[0] Size: "
<< Doc.at("equipment")[0].size();
}
Document Size: 3
Equipment Size: 2
Equipment[0] Size: 0
If we want to check if size() == 0
, we can use the empty()
method instead:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
std::string Data{R"(
{
"name": "Roderick",
"level": 10,
"equipment": []
}
)"};
json Doc{json::parse(Data)};
if (Doc.at("equipment").empty()) {
std::cout << "No equipment!";
}
}
No equipment!
contains()
MethodThe contains
method allows us to check if our JSON contains a specific key. It will return true
if it does, even if the corresponding value for that key is null
.
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
using namespace nlohmann::literals;
json Doc{
R"({
"name": "Anna",
"level": 10,
"guild": { "name": null }
})"_json};
if (Doc.contains("name")) {
std::cout << "Doc contains name";
}
if (!Doc.contains("age")) {
std::cout << "\nDoc does not contain age";
}
if (Doc.at("guild").contains("name")) {
std::cout << "\nGuild contains name";
}
}
Doc contains name
Doc does not contain age
Guild contains name
erase()
MethodThe erase method is useful for either removing a key from a JSON object, or removing an element from a JSONÂ array.
Below, we erase the element at index 0
of the equipment array, then erase the level
from the top-level document:
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
using namespace nlohmann::literals;
json Doc{
R"({
"name": "Anna",
"level": 10,
"equipment": ["Weapon", "Armor"]
})"_json};
Doc.at("equipment").erase(0);
std::cout << "Doc:\n" << Doc;
Doc.erase("level");
std::cout << "\n\nDoc:\n" << Doc;
}
Doc:
{"equipment":["Armor"],"level":10,"name":"Anna"}
Doc:
{"equipment":["Armor"],"name":"Anna"}
clear()
MethodThe clear
method works similarly to erase
, except it will instead set the element back to its default value. The default value depends on the object type:
""
0
false
null
{}
[]
#include <iostream>
#include <json.hpp>
using json = nlohmann::json;
int main() {
using namespace nlohmann::literals;
json Doc{
R"({
"name": "Anna",
"level": 10,
"equipment": ["Weapon", "Armor"]
})"_json};
Doc.at("equipment").at(0).clear();
std::cout << "Doc:\n" << Doc;
Doc.at("equipment").clear();
std::cout << "\n\nDoc:\n" << Doc;
Doc.clear();
std::cout << "\n\nDoc:\n" << Doc;
}
Doc:
{"equipment":["","Armor"],"level":10,"name":"Anna"}
Doc:
{"equipment":[],"level":10,"name":"Anna"}
Doc:
{}
In this lesson, we've delved into the fundamentals of using JSON (JavaScript Object Notation) with C++ and explored how to effectively utilize the nlohmann::json
library for JSON parsing and serialization. Here are the key takeaways:
nlohmann::json
Library: This popular library simplifies JSON operations in C++, offering intuitive ways to parse, create, and manipulate JSON data.A practical guide to working with the JSON data format in C++ using the popular nlohmann::json
library.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.