C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
176: Building a Simple JSON Parser
I've always found that the best way to actually understand how a parser works is to try and build one the "wrong" way first. We're going to build a simple JSON parser. Now, you could spend weeks writing a perfect, spec-compliant beast, but for today, let's just try to get a small configuration string into a C++ object.
Let's start with this string: {"name": "Kaelen", "level": 42, "stats": {"str": 10, "dex": 15}}. It looks simple enough, right? Just some keys and values.
The "Split" Trap
My first instinct, if I'm being honest and lazy, is to just split the string by commas. I can find the colon, take the left side as the key and the right side as the value. Let's see what happens when I try that with our example.
// Imagine a loop splitting by ','
// First chunk: {"name": "Kaelen"
// Second chunk: "level": 42
// Third chunk: "stats": {"str": 10
// Fourth chunk: "dex": 15}}
Yeah, that's a disaster. The comma inside the stats object broke my logic. This is the classic "regular expression/splitting" trap. JSON isn't a flat list; it's a tree. If we have nested structures, we can't treat the input as a linear sequence of tokens separated by a single character. We need to track state.
Defining the Value Container
Before I can parse, I need somewhere to put the data. Since a JSON value can be a string, a number, or another object, I need a type that can be "anything." In modern C++, std::variant is our best friend here. I'll define a Value type that can hold a string, an integer, or a map of other Value objects.
#include <iostream>
#include <string>
#include <map>
#include <variant>
#include <vector>
struct Value;
using Object = std::map<std::string, Value>;
struct Value {
std::variant<std::string, int, Object> data;
};
I'm using a std::map for the Object because JSON keys are unique and we usually want to look them up by name. Now I have a place to store the result; now I just have to get the data in there without losing my mind.
Walking the String
Instead of splitting, I'll use a pointer (or an index) to walk through the string one character at a time. I'll create a Parser class that keeps track of the current position. Let's try to handle a simple string first. When I see a quote ", I know everything until the next quote is the actual text.
std::string parseString() {
consumeWhitespace();
if (current == '"') {
current++; // skip opening quote
std::string result;
while (current != '"') {
result += input[current++];
}
current++; // skip closing quote
return result;
}
return "";
}
That works for the keys and the names. But what happens when I hit the curly brace {? That's the magic moment. That's where the parser needs to say, "Oh, I'm starting a new object, and I'll keep parsing until I find the matching closing brace."
The Recursive Leap
This is where recursive descent comes in. If the parseValue function sees a {, it doesn't just return a string; it calls parseObject. And parseObject, while looking for values, calls parseValue again.
I tried writing this linearly at first, but it became a nightmare of if/else blocks. The "aha!" moment was realizing that the structure of the code should mirror the structure of the JSON. If JSON can be nested, the code must be recursive.
Value parseValue() {
consumeWhitespace();
if (input[current] == '{') {
return parseObject();
} else if (input[current] == '"') {
return Value{ parseString() };
} else {
return Value{ parseNumber() };
}
}
Object parseObject() {
Object obj;
current++; // skip '{'
while (input[current] != '}') {
std::string key = parseString();
consumeWhitespace();
current++; // skip ':'
obj[key] = parseValue(); // Here is the recursion!
consumeWhitespace();
if (input[current] == ',') current++;
}
current++; // skip '}'
return obj;
}
Now, if I run this against {"stats": {"str": 10}}, the flow is: parseValue $\rightarrow$ parseObject $\rightarrow$ parseString("stats") $\rightarrow$ parseValue $\rightarrow$ parseObject... and so on. It naturally handles any depth of nesting because the call stack keeps track of where we are in the hierarchy.
Dealing with the Messy Bits
I noticed immediately that my first version crashed on {"name" : "Kaelen"} because of the space before the colon. Real-world data is messy. I had to add a consumeWhitespace() helper that just eats up spaces, tabs, and newlines before every significant character check. It's a tedious bit of code, but without it, the parser is too fragile to be useful.
We've built a skeletal parser. It doesn't handle arrays or floating-point numbers yet, but the core logic—the recursive descent—is exactly how the heavy-hitters like RapidJSON or nlohmann/json operate under the hood.
📋 Practical Task
Extend the JSON Value System to Support Booleans
Currently, our Value struct only supports strings, integers, and objects. Your task is to extend the parser to handle boolean values (true and false).
- Modify the
Valuestruct'sstd::variantto includebool. - Implement a
parseBoolean()method that checks if the current characters are "true" or "false". - Update the
parseValue()logic to detect when a boolean is starting (usually by checking if the current character is 't' or 'f') and callparseBoolean()accordingly. - Test your parser with the following string:
{"name": "Kaelen", "is_active": true, "stats": {"is_boss": false}}.
There are no comments for now.