Using JSON in Modern C++

A practical guide to working with the JSON data format in C++ using the popular nlohmann::json library.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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.

The JSON Data Format

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.

Primitive Types: Booleans, Numbers, Strings, and null

Booleans, numbers, and strings in JSON broadly follow the same syntax as C++.

  • Booleans are true or false values, which we represent by literals: true and false
  • Numbers can be either integers or floating point, and we represent them by literals like 2, 3.14, and -6
  • Strings are collections of characters, enclosed within double quotes, eg "Hello"
  • The literal 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.

Structured Type: Objects

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"
  ]
}

Structured Type: Arrays

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"]

White Space

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.

Formatting JSON

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.

Ordering

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"]

JSON for Modern C++ (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;
}

Parsing JSON Documents

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!

Handling Invalid JSON and Exceptions

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/

Mapping JSON Types to C++ Types

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

Creating C++ Maps from JSON Objects

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.

Outputting JSON to Streams

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.

Supporting JSON in User-Defined Types

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"
  }
]

Using Serialization Macros in User-Defined Types

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.

Creating JSON Documents from C++ Data

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
}

Reading and Writing JSON Files

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
}

Saving and Restoring Program State using JSON

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

Iterating Over JSON Documents

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.

Comparing JSON Documents

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!

Additional Utility Methods

The nlohmann::json library includes a range of additional methods we may find situationally helpful:

JSON Type Checkers

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 number
  • is_number_integer() returns true if the element is an integer
  • is_number_float() returns true if the element is a floating point number
  • is_boolean() returns true if the element is a boolean
  • is_null() returns true if the element is a null
  • is_string() returns true if the element is a string
  • is_array() returns true if the element is an array
  • is_object() returns true if the element is an object
  • is_primitive() returns true if the element is a number, boolean, null, or string
  • is_structured() returns true if the element is an array or object

Some 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

The size() and empty() Methods

We 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:

  • For objects, size() returns the number of keys
  • For arrays, size() returns the number of elements
  • For strings, booleans, and numbers, size() returns 1
  • For null 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!

The contains() Method

The 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

The erase() Method

The 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"}

The clear() Method

The 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:

  • Strings have all their characters removed, and are left empty: ""
  • Numbers are reset to 0
  • Booleans are reset to false
  • Nulls remain null
  • Objects have all their keys removed, and are left as an empty object: {}
  • Arrays have all their elements removed, and are left as an empty array: []
#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:
{}

Summary

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:

  1. Understanding JSON: We covered the basics of the JSON data format, highlighting its structure and types - primitive (booleans, numbers, strings, null) and structured (objects and arrays).
  2. Utilizing nlohmann::json Library: This popular library simplifies JSON operations in C++, offering intuitive ways to parse, create, and manipulate JSON data.
  3. Reading and Writing JSON: Techniques for parsing JSON strings into C++ objects and vice versa were demonstrated. We also covered the handling of nested structures and arrays within JSON documents.
  4. Advanced Features: The lesson included insights into more advanced features like custom-type serialization and iterators
  5. File I/O with JSON: We explored how to read from and write JSON data to files, which is crucial for tasks like configuration management and data storage.
  6. Error Handling: The lesson touched on common issues and exceptions that can arise when working with JSON in C++, guiding you on how to handle them effectively.

Was this lesson useful?

Next Lesson

Using HTTP in Modern C++

A detailed and practical tutorial for working with HTTP in modern C++ using the cpr library.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Libraries and Dependencies
Next Lesson

Using HTTP in Modern C++

A detailed and practical tutorial for working with HTTP in modern C++ using the cpr library.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved