Niels Lohmann is the author of JSON for Modern C++, better known as nlohmann json. It's one of the most popular C++ libraries, with over 50K GitHub stars and over 7.5K forks. Chances are you've used it, or at least depended on something that does, but you may not have met the man behind it. I'm delighted share this interview with Niels Lohmann.
I'm Niels, a developer and engineering manager based in Berlin, and I am currently working for MongoDB.
My first contact with C++ was studying computer science. Later I worked at university as a researcher, at a research group that built model checkers (verification software). The idea is that you prove a system correct by enumerating its state space. We used C++ heavily, partly because we could compile it on large Unix machines, and partly because it gave us tight control over memory and let us write efficient code. A state space can run to billions of states, so both mattered.
That work shaped how I think about software more than anything else I've done. Correctness was always very important to me. But it also taught me the limits on both sides: testing can only do so much, and formal proofs are not always feasible.
From there I moved into the automotive industry, where embedded systems mattered, and now I'm at MongoDB, where the main database server is written in C++.
Besides the library, I've been looking into Swift and building small iOS apps. It's a nice change to work for a different audience, and for different software and hardware platforms.
It started as telemetry, not as a parser.
The model checkers we built at university often ran on servers where we had no simple way to observe their progress. So we implemented a logging server, and each running node would send a small JSON payload every once in a while so we could get an idea of the state of things.
At that time there wasn't a C++ JSON library — or at least I wasn't aware of one. And we didn't need to support all of JSON. We just needed to send a small dictionary. So I implemented something very naive that converted a few numbers into a JSON string and sent it over the wire. Eventually we supported more and more data types, because we didn't only want to send numbers but also strings. And one day a colleague added a very naive parser. We ended up with an internal library used in several of our tools for exactly this purpose.
I took that code and rewrote it so it could be used independently of our tools, and put it on GitHub. That was the first time I was aware of GitHub and used it.
The library didn't have a name at first. That came later, around the time Scott Meyers was wrapping up Effective Modern C++ — he was blogging about it, there were a lot of talks — and I took the "modern C++" part from him. It fit, because the library leaned heavily on features that were new then: auto instead of writing out the exact return type, and brace initialization, which let me get a syntax close to JSON itself, so you could write arrays and objects almost the way you would in JavaScript or Python.
There wasn't a moment where I noticed it happening. For a long time it was a toy for myself — somewhere to build up a CI, and to get the test coverage where I wanted it. Adding tests and reaching 100% line coverage was very important to me from early on, long before anyone else was involved.
The first two issues in the tracker were both mine, filed in December 2013, and the second one was about test coverage. That was the whole world of the project for thirteen months.
Then it all arrived on a single day. On 5 January 2015, in the morning, a pull request came in from someone I'd never heard of, adding CMake and lcov — my first outside contribution was somebody improving my coverage tooling. That afternoon came the first issue from somebody else, about using unique_ptr for ownership, and a few hours later another one about UTF-8 encoding and decoding. Build system, ownership, Unicode: more or less the whole subsequent history of the library, in one day.
That was the aha moment. Until then I simply wasn't aware of users. Suddenly people were not only using it, they were willing to contribute to it.
For a long time what motivated me most was the extent to which people were willing to contribute. Some contributors have been with me for years — they watch the project, they dive into discussions, they give me proper code review. Others come and go. Either way it's motivating to see people spend their own time on a specific problem.
A good example: the template machinery that lets you use arbitrary types. You implement to_json and from_json for your type, the library picks them up from your namespace, and then you can just write json j = your_type; and it knows what to do. That was built by one volunteer, Théo Delrieu, over quite some time — three attempts at it across three months before it landed, and then a long tail of follow-up work, including the refactoring that split the library into the separate headers it still uses today.
That's also why I try to stay responsive. You don't want to be the one blocking other people.
The person I owe the most to right now is Greg Marr. He's the most active contributor the project has, and more than that — he reviews, he takes part in the discussions, he answers people's questions before I get to them, and he does a fair amount of C++ evangelism along the way. He has been involved in more than 800 issue and pull request threads. A project like this doesn't survive on code alone; it survives on somebody being reliably present, and for years now that's been Greg.
I'm naming two people here and there are many more who deserve it. The README contains a long contributor list.
Most of my own time goes into reviewing and maintaining rather than writing code. And for a while, honestly, I could only triage. If an issue came in and I didn't find the time or the motivation to work on it, it might be auto-closed after a month or two — because I wanted to be honest and say this is not a backlog, this is something I'm not going to do. Same with pull requests: if the CI was red, I'd comment, and if the contributor didn't come back, at some point I'd close it. I always felt a bit bad about that, but I had to be conscious about my time.
That part has changed recently. I now use AI for the tedious, repetitive work — going through old discussions, finding connections between issues, revisiting things that were closed due to inactivity to see whether they're still relevant, checking that every API description is still valid and every example still compiles. It's less personal than I'd like, but given that I'm alone on this project, it's as good as it can be. And it's put me back in the driver's seat for actual features — I can try ideas now that I'd never have had time to start before.
The standard for merging hasn't moved. It was always a green CI and tests for whatever you added, and that's still the bar. The contribution guidelines are more elaborate now, but they come down to the same thing — and in one respect it's gone up: since last year, every public API function has to be properly documented, with templates and checkers that keep the documentation in lockstep with the code. That applies to everything that lands, regardless of how it was produced.
Around ten hours a week, usually less, and it depends a lot on my own motivation. It's a free-time project. I have a day job and a family — it's not that I have 24 hours a day at my disposal for a JSON library.
C++11 was ambitious at the time, because a lot of compilers didn't support it. I lived through how long it took before I could rely on every feature, so I've never taken it for granted.
But really it's not so much about the standard. It's about a set of compilers we want to keep supporting — GCC back to 4.8, Clang back to 3.4. That's the actual constraint. Newer standards and newer standard libraries would make a lot of what we do much easier, and we carry a certain amount of #ifdef hell to use the best available approach on each compiler and language version.
Every once in a while I'm tempted to drop C++11 for something newer. But from my time in industry I know that a lot of software development doesn't happen on recent compilers, for all sorts of reasons. I could make my own life as a maintainer much easier and at the same time make the library unusable for a large number of people.
So C++11 may sound old and boring. But old and boring is exactly why the library is used as widely as it is. And we're not frozen: where a newer standard offers better code, we use it behind an #ifdef.
I'd never say never — we dropped C++98 a long time ago.
Right now it feels fine as it is. C++ isn't evolving in a direction that would make a rewrite worthwhile. A lot of the evolution I see arrives either as new standard library functions or as optimizations you get for free by recompiling your existing C++11 code with a newer compiler.
The moment that changes — if language constructs arrive that greatly simplify the code while making it more performant, and that can't be reached with macros or preprocessor magic — that's when it's worth reconsidering. But not today.
Reflection is the obvious candidate, and it only takes you so far. The library is much more than type conversions: there's a parser, there's extensive support for binary formats. But if there were a clean way to annotate a class as JSON-serializable, then by all means the library should support it. I'd hope that means an additional header rather than a rewrite — something that handles the reflection part while the rest of the library stays as it is.
I don't have a dramatic answer. I follow the evolution, and I'm usually fairly sure I won't be using a new feature in the library or at work for quite some time, because not everyone is on the bleeding edge. I don't see an inflection point coming that would force me to reconsider anything.
I'm not too concerned about the code I write. I'm more concerned about the code users write. If something arrives that makes the library easier to use, good. And if the language evolves in a direction where my library no longer feels natural — no longer feels canonical — that's the point to reconsider. The API has been stable for years, and 90% of user code comes down to a handful of operators. As long as those still feel like native C++, all is fine.
On memory safety specifically: the library had some issues in the past, but I have a good feeling about where it is now. There's not much going on with pointers. We run Valgrind, we have the sanitizers, we have the test coverage, and OSS-Fuzz has been fuzzing the library around the clock for years. Other languages have more to say about memory safety than C++ does. For this library, it isn't really an issue.
If the standard library had JSON support, we wouldn't be having this conversation. :-)
More seriously: the internal representation of a JSON value is a tagged union. Proper variant support would have made a good part of the library much easier. Apart from that, I'm fine.
No, I don't think so.
It would be the first time a data format was covered by the standard library, and it would take an enormous amount of specification to get all the rough edges right. If you pull in JSON you pull in Unicode, and a lot of other standards with it, and the result is a burden on compiler vendors. Even if it were done today, it would take years before it reached production code in most industries.
I don't think the problem is the absence of JSON from the standard. I think the problem is the absence of a default package manager, where adding a JSON library is as trivial as writing an import statement. The C++ ecosystem is good enough that if you already use one external library, adding a second is easy. The rough edge is the first one — that's where you have to choose a package manager that fits your build system, and so on. I don't see the added value of putting JSON in the standard itself.
It's everything that comes with it.
The first part is the design goal. My motivation was always that it should work the way it does in Python: the user shouldn't have to tell you what kind of data they want to store. You write j = 1, or j = "foo", or assign an array, and it works. You shouldn't have to announce that you're about to store an array.
The second part is that you have to read and write the format. A parser immediately gives you number parsing, UTF-8 parsing, recursion depth, and more. That's a complexity in itself. Serializing gives you the mirror image: how to represent numbers, how to handle escape sequences.
None of these problems is difficult on its own. The devil is in the detail. Writing a JSON library in a weekend isn't the issue. The issue is getting through all the edge cases you didn't think of at the start.
Finding the right architecture for the binary formats.
The library supports CBOR, MessagePack, BSON, UBJSON and BJData. These formats are all described similarly, especially for strings and numbers, but each goes its own way on containers and recursive types. Originally I implemented each one as a standalone parser. After the second or third, I could see how much they had in common and tried to express that by sharing functions.
That took far longer than I expected. And we're still at a point where a refactoring touches a lot of functions, and some of the bugs we find show us that the abstraction we picked wasn't the right one.
It may not sound as complex as it was. But the goal of the library isn't to support features — it's to support a feature in a way that we can properly test, so we're confident it's right. That means a lot of cycles of fixing small issues, which means you need an architecture where picking up old code years later is easy, and where testing each individual function is easy
You may ask how it ended up being five binary formats. Partly it was my own education and entertainment. Like many computer scientists I'd read about JSON early on but never sat down with the standard at all — and implementing a standard is what makes you understand it. The same with UTF-8, and the same with the binary formats. Those were among the first RFCs I picked up and implemented just to see all the examples in the specification work.
It started as curiosity: there's a big overlap between these formats and JSON, so you can convert JSON into something considerably smaller on the wire. Then you write the parser for symmetry. These formats are used — though I suspect a very small percentage of users touch them, and they generate a disproportionate share of the bug reports, because that's where the remaining edge cases are. Ignoring the compile time you pay for it, the feature comes for free, and if wire size matters to you it's genuinely useful.
Trust is hard to earn in this.
What the library tries to do is turn every bug into a regression test. When we implement a new function, we want 100% line coverage on it. When we see an edge case, we try not just to fix that case but to work out whether it can occur anywhere else.
Can I say all the bugs are out? No. But looking at the kind of bugs we fix now, it's getting harder and harder to explain them. Years ago, you could hand me a valid JSON document and our parser might reject it because we'd misunderstood the standard. Now the bugs are edge cases in the binary formats, or combinations of features nobody foresaw and nobody wrote a test for.
Furthermore, we try to keep the library warning-free on the most popular compilers. We compile with -Weverything on Clang with 8 exceptions and over 300 warning flags enabled in GCC.
First, to be clear: it's line coverage. I don't talk about branch coverage much because the tooling for C++ isn't good enough. If there were a good way to measure it, I'd invest in that too.
And the reason isn't that coverage is automatically linked to correctness or code quality. It's that it lets me cover parts of the code automatically, without having to look at them myself.
I spend most of my time looking at other people's code. One round of the unit tests under C++20 runs 151,824,922 assertions — and that's a single configuration. CI runs the suite across 50+ compilers, several language standards and all kinds of build configurations, so the real number is far higher. If all of that completes and I see a green check mark, then it makes sense to invest in a code review. Until then, it's easy for me to say: have a look at the CI logs, they'll point you at the parts of your change that need more work.
So it's an investment in making future work cheaper — for me and for contributors.
Honestly, it's rarely a case of the matrix finding a bug in the library. More often it shows where compilers disagree about how to interpret the standard, with our library caught in between and looking like it has a bug.
For instance, there's a subtle difference in how GCC and Clang interpret brace initialization, where the same code produces an array on one and a plain value on the other. It's in our FAQ.
Or Visual Studio, where we put parentheses around std::min and std::max. I never thought that would make sense, and we do it to satisfy that compiler.
So the matrix doesn't find our bugs. It helps us find a way to express what we mean that every compiler agrees on — sometimes in a form we'd rather not write. But it means that wherever our users are, the library runs. A lot of them have no choice about updating. I'd rather add some preprocessor magic for a strange compiler than tell them we're not going to help.
The strangest one is an inversion of the usual relationship. The library was mentioned several times in slides Microsoft presented for new Visual Studio versions, and at one point there was a slide noting explicitly that the current version of the library compiled without a single warning.
So instead of the library defending itself against compilers, the compiler was using the library as a benchmark.
Writing open source software is a great thing to do, and the people doing it deserve a lot of thank-yous. They don't owe anyone anything. I like the way the MIT license puts it — the software is provided "as is", without warranty of any kind.
An open source library might just be something someone built for themselves and put on GitHub. That doesn't entitle anyone to use it in a completely different scenario and be angry when that scenario isn't supported.
There are libraries that are a real source of inspiration — you look at them and think, that's craftsmanship, that's a piece of art. And there's a lot of code out there that somebody put on the internet years ago and never touched again, which solved my small problem perfectly well. It depends far more on the person using it than on the library. We should be grateful that people give software away for free at all.
It's finished in the sense that the issues coming in now all point in a similar direction, and I simply haven't found the time to fix them. Plenty of the rest we know exactly how to solve — but it would break the API, and we're not going to do that.
So the question isn't whether the library will ever be finished. The 3.x line will be finished at some point. The hard part is starting the next breaking change and opening the 4.x family. My idea is a smooth transition using feature macros: implement the breaking changes behind preprocessor flags, and eventually, once we're sure of what we're doing, the next major release is a combination of feature flags people can switch on in their existing code. Then moving up doesn't hurt so much.
But this is a one-person show, and I won't be able to support more than one version. Once there's a 4.x, a bug fix lands on main and does not get backported. Which means one real possibility is that there's never a major release at all — we continue with 3.x forever, and eventually work through the open issues, with occasional work when new compilers arrive and things that are fine today become deprecated.
A few years ago I got a private email from someone reporting an ABI compatibility warning from the compiler, which he thought came from our library. I looked into it. It was a very old GCC, and the warning had nothing to do with us — it came from an STL function it was using.
So I asked the obvious question: can't you just update your compiler? He was very cryptic. He said it was an embedded system and he didn't have full control over the toolchain. Apparently my answer was enough to convince his project manager that they could keep using the library.
I didn't think much of it. No code change was needed.
A while later I got another email with some context. The embedded system was a configuration library, and the project was a spacecraft intended to land on the Moon — Astrobotic's Peregrine Mission One. So the library was going to the Moon.
It didn't land. Shortly after launch there was a propellant leak, and instead of a landing the spacecraft went out as far as the Moon's distance and came back, and had to be destroyed in Earth's atmosphere.
So the library went to space, got as far as the Moon, and didn't make it home. I have a long list of the industries the library is used in. That one is the coolest by far.
Why not. But it's worth saying why the JSON library happened at all, because it wasn't a plan.
C++11 was new and I wanted to play with it, and at the same time I had a concrete problem that gave me somewhere to try it out. That combination is the precondition: a real problem to solve, and something new to learn along the way. If I find myself in that situation again, that's good ground for a new library.
Right now that combination is iOS. I am building a weather app and am learning a new language while doing it.
There's also a difference in audience that I didn't expect. C++ developers are often very focused on performance, and performance was never the main goal of my library, so that discussion never quite goes away. With end-user software, the people using it aren't developers at all — they have completely different backgrounds, and it gives something back that's harder to find in a library.
Support your open source libraries and the people who maintain them!
Every so often xkcd #2347 gets passed around — the one where a huge part of the industry rests on a project maintained by one unnamed person somewhere in Nebraska. That's real, and it isn't getting better.
So appreciate the work people are doing. Instead of filing issues the way you would with a paid vendor, be appreciative, ask for help, and be kind. People are doing this in their free time, and it's always easy to walk away.
Over the thirteen years I've been doing this, it hasn't always been the most motivating thing. The best times were the ones where new, enthusiastic people turned up who wanted to help — and who motivated me to take another look, commit something, and cut another release.
Subscribe to the C++ Doctor e-mail list below:
Thank you for subscribing!
If you enjoyed this, you might also like my interview with Klaus Iglberger, C++ expert, speaker, and author of C++ Software Design.