nlohmann / nlohmann/json

Idea: lossless number round-tripping (raw / arbitrary-precision number mode)

Open
#5,296 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
50.6k
Forks
7.5k
Avg merge
4d 17h
Merged PRs (30d)
58

Description

Status: brainstorming, not a plan

This is an idea to move forward on, not a sanctioned TODO or a commitment. It is a hypothesis to investigate, measure, and discuss before anything is decided. Treat this as a starting point for discussion.

Motivation

The lexer scans a number token and immediately converts it to number_integer_t / number_unsigned_t / number_float_t; the original lexeme is then discarded. Consequences:

  • A 30-digit integer overflows uint64_t and silently degrades to double — precision gone.
  • 1e999 becomes inf (or a parse error, depending on path) and is unrecoverable.
  • 1.10, 100.0, 1.0e2, 1E2 all collapse to the same double and re-emit as 1.1 / 100.0, so a document cannot be round-tripped byte-for-byte. This breaks signing, diffing, and "don't touch fields I didn't edit" workflows.

Note: the integer-vs-float distinction is already preserved (separate value_ts). This idea is specifically about magnitude and textual fidelity.

Prior art

  • serde_json arbitrary_precision feature — stores the original number string, parses lazily.
  • Boost.JSON parse options / number_precision.
  • Python json parse_float/parse_int hooks (e.g. Decimal).

Sketch — a "raw number" value

Keep the token. The union already stores a string_t* (include/nlohmann/json.hpp:463), so the cheapest encoding is a new value_t::number_raw whose payload reuses the string pointer to hold the original lexeme:

  • parser (raw mode on): don't convert — store the token verbatim.
  • dump(): emit the token byte-for-byte → perfect round-trip.
  • get<double>() / get<int64_t>(): parse on demand from the stored token.
  • get<json::number_string>() (new): hand back the raw lexeme.

Make it a compile-time / parse-time policy, not a new default

A value_t::number_raw that every consumer must now handle would be an API/ABI break and adds a branch to every numeric access. Gate it behind a parse option / policy, e.g.:

auto j = json::parse(s, /*cb=*/nullptr, /*allow_exceptions=*/true,
                     /*ignore_comments=*/false,
                     json::number_policy::raw);   // illustrative

so code that doesn't ask for it keeps seeing exactly the current three numeric types.

The hard part: comparison and hashing

This lands directly on top of existing numeric-correctness work — #5256 (std::hash contract), #5210 / #5211 (mixed number_unsigned vs number_integer comparison). Once number_raw exists we must define:

  • Does raw 1e3 equal integer 1000? Equal double 1000.0?
  • operator== almost certainly wants numeric equality (parse-and-compare), which then forces std::hash to hash the numeric value, not the string — otherwise we re-introduce the exact "equal but different hash" bug.

Recommendation: settle the general numeric comparison/hash model first (a canonical numeric key across integer/unsigned/float), then slot number_raw into it, rather than building fidelity on a cracked foundation.

Open questions

  • Storage: reuse the string pointer + a new value_t, or a dedicated NumberRawType template slot?
  • Lazy-parse caching: parse once and memoize, or re-parse per access?
  • What does type() / is_number() / is_number_integer() report for a raw value before it's been inspected?
  • Behavior on dump() after the value has been assigned a normal numeric type (fidelity only survives untouched values).

Dependencies / interactions

  • Shares "keep the token, parse lazily" with the borrowed-view idea (build together).
  • Deliberately conflicts with the canonical-serialization (JCS) idea: canonical mode must re-canonicalize raw numbers (parse → ECMAScript format), not echo the token. That override needs to be explicit in both designs.
  • Gated by resolving the numeric comparison/hash model (#5256, #5210/#5211).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading include/nlohmann/json.hpp around line 463 and the parser, value representation, dump(), and get() entry points. Review the numeric comparison and hashing work in #5256, #5210, and #5211, then resolve the policy, storage, type behavior, and comparison questions; done means an agreed design with precision and textual round-trip tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend-api-design, data
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.