Skip to content

Create objects for pointer tokens that cannot be array indices - #5358

Draft
nlohmann wants to merge 1 commit into
developfrom
fix/json_pointer_create_object_5357
Draft

Create objects for pointer tokens that cannot be array indices#5358
nlohmann wants to merge 1 commit into
developfrom
fix/json_pointer_create_object_5357

Conversation

@nlohmann

@nlohmann nlohmann commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Important

This is a draft to support the discussion in #5357 — not a change that is ready to merge. It implements one of several possible answers so we have something concrete to talk about. See "Open questions" below.

Fixes #5357 (discussed in #5356, related to #4446).

Problem

When operator[](json_pointer) traverses an intermediate level that does not exist yet, the null value has to be turned into either an array or an object. The rule in json_pointer::get_unchecked() was "all characters are digits → array, otherwise object".

That predicate is looser than what actually counts as an array index (RFC 6901, Sect. 4: a nonempty sequence of digits without a leading 0). So tokens that can never be a valid array index still selected an array, and the subsequent index conversion then failed:

json j;
j["/a/b/01/d"_json_pointer] = "value";
// [json.exception.parse_error.106] array index '01' must not begin with '0'

json j2;
j2["/"_json_pointer] = 1;
// [json.exception.out_of_range.404] unresolved reference token ''

Both 01 and "" are perfectly valid object keys, and both work if the level already exists as an object:

json j;
j["/a/b"_json_pointer] = json::object();
j["/a/b/01/d"_json_pointer] = "value";  // OK: {"a":{"b":{"01":{"d":"value"}}}}

So whether the pointer worked depended on whether the level had been created beforehand. unflatten() (which goes through get_and_create(), a different rule) already produced the object form for the same key.

Change

Test the reference token against the RFC 6901 grammar for array indices instead of "is all digits". Tokens that cannot be an array index now create an object, which is what they would resolve to on an existing object.

pointer on null before after
/0, /2, /-, /foo/0/0 array array (unchanged)
/one object object (unchanged)
/01 parse_error.106 {"01": …}
/ (empty token) out_of_range.404 {"": …}

Breaking changes

No breaking API changes. No signature, type, or macro changes.

Behavior changes only in cases that previously always threw: a pointer token matching 0[0-9]+ or the empty token, applied to a null value via the non-const operator[]. Any program that got a value out of such a pointer before still gets the same value; only programs relying on the parse_error.106 / out_of_range.404 exception in this specific situation would notice. Existing arrays are untouched — j["/01"_json_pointer] on {1,2,3} still throws parse_error.106.

Open questions (why this is a draft)

  1. Is "no working program changes behavior" a good enough bar? The change is observable, and someone may be relying on the throw to reject leading-zero tokens.
  2. get_and_create() (used by unflatten()) still uses a third rule: only the token "0" starts an array, so json{{"/foo/1", 1}}.unflatten() yields {"foo":{"1":1}} (object) while j["/foo/1"_json_pointer] yields an array. Aligning those two would be a genuine breaking change, so it is deliberately not part of this PR. Should it be a separate v4 item?
  3. The empty-token part could be split out. It is the same bug class, but it changes a 404 into a success, which is arguably a bigger surprise than the 01 case. Happy to drop it if we only want the reported issue fixed.
  4. The original request in Behavior differences in operator[] object creation with a json_pointer #5356 was a mode where all tokens are treated as strings. This PR does not do that; it only removes cases where the heuristic picks a type that provably cannot work. A real "always object" mode would need new API surface and could be discussed separately.

Also in this PR

  • Tests in unit-json_pointer.cpp covering array-index tokens, non-index tokens, and that creating a level now gives the same result as reusing an existing one.
  • The operator[] documentation note about creating intermediate levels now states the rule in terms of valid array indices and mentions the 01 / empty-token cases.

Drafted by Claude Code.

When operator[](json_pointer) traverses a level that does not exist yet,
the null value is turned into an array or an object depending on the
reference token. The check only tested whether all characters are digits,
so tokens that can never be a valid array index selected an array and
then failed:

- "01" (and any other token with a leading '0') threw parse_error.106
- the empty token threw out_of_range.404

Both tokens are valid object keys, and both work when the level already
exists as an object, so creating the level changed the outcome. Test the
token against the RFC 6901, Sect. 4 grammar for array indices instead, so
that such tokens create an object. This only affects pointers that threw
before.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Behavior differences in operator[] object creation with a json_pointer

1 participant