Skip to content

Add element access, iteration, values, and JSON pointers to json_view - #5622

Open
nlohmann wants to merge 15 commits into
json-view/08-view-builderfrom
json-view/11-view-access
Open

nlohmann wants to merge 15 commits into
json-view/08-view-builderfrom
json-view/11-view-access

Conversation

@nlohmann

@nlohmann nlohmann commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Part of the stack for the zero-copy view (#5295). This adds element and member access to basic_json_view, and the value conversions built on it.

Summary

  • basic_json_view gains the read-only access functions of basic_json: operator[] and at() with keys, indices, and JSON pointers, front() and back(), find(), contains(), and count(), begin()/end() and cbegin()/cend(), items() with structured bindings from C++17 on, and type_name().
  • Index types. operator[] and at() accept an index of any integer type (int, unsigned, long, std::int64_t, ...) through a template; a single int overload would have been ambiguous for unsigned, long, and int64_t. Negative values are out of range.
  • Exceptions: the ids and messages of the const functions of basic_json. Where basic_json has undefined behavior, the view answers safely: operator[] with a missing key or an index out of range returns a discarded view; front()/back() of an empty container throw invalid_iterator.214.
  • Chained access is safe. operator[] (key, index, JSON pointer) on a discarded view returns a discarded view, so v["a"]["b"][0] can be tested once at the end. at() still throws.
  • Order: objects are iterated in document order, and all members are visited.
  • Duplicate keys (please review): lookups return the first member, with early exit, so a lookup can stop at the first match; yyjson and simdjson do the same. parse() keeps the last value, and so do materialize() and the conversions to maps. This is documented prominently in a "Duplicate keys" section of the feature page, with a recipe for getting the last member. Returning the last member would mean scanning every object to its end on each lookup, which made lookups in small objects 1.6 to 3.4 times slower.
  • Lookup cost: keys of up to 16 bytes are compared with two overlapping loads, and most keys are rejected by their length alone, which the index already holds.
  • Array access: operator[](index) is linear in the index, because elements are not fixed-size in the flat index. The docs say so.
  • get<T>(), get_to(): convert directly, without building a basic_json value: arithmetic types (booleans become 0/1, as in basic_json); bool and std::nullptr_t; strings (std::basic_string as a copy, string_view_t without a copy); BasicJsonType (the same as materialize()); views themselves; std::vector, and maps with string keys, e.g. std::map<std::string, json_view>. Everything else, including user types with from_json(), goes through materialize() of that subtree. Floats are bit-identical to parse().
  • get_string(): the string without a copy, pointing into the source text or, for strings with escapes, into the document's buffer of decoded strings.
  • number_token(): the number as it appears in the source ("1.50", "1E2", all digits of a long integer) -- basic_json cannot provide this.
  • value(): with keys and with JSON pointers; the pointer overload also accepts arrays, as since Extend value to arrays when using JSON pointers #5223.
  • JSON pointers: operator[], at(), and contains(). The exceptions are those of basic_json, message included; where const basic_json::operator[] has undefined behavior, the result is a discarded view; value() returns the default wherever basic_json::value() catches out_of_range; contains() never throws; array indices follow json_pointer's rules (parse_error.106/109, out_of_range.404/410).
  • Core files touched: json.hpp, json_pointer.hpp, and value_t.hpp gain two small internal helpers. detail::json_pointer_access gives code outside basic_json the reference tokens of a pointer, so they need not be parsed from to_string() again. detail::value_type_name(value_t) holds the names that basic_json::type_name() used to switch over; type_name() now calls it, so the view uses the same names (no behavior change).
  • Other: NLOHMANN_VIEW_ALWAYS_INLINE is plain inline on MSVC, because __forceinline triggers C4714 (an error under /WX) for templates MSVC cannot inline. IWYU pragmas are added. Docs: API pages and examples for every new function, the "Duplicate keys" and "Getting values out without copying" sections, and fixes of the operator[] and is_discarded pages.

Performance

Measured on Apple M1 Max (Apple clang, -O2) and an x86-64 KVM Haswell VPS pinned to one core (GCC 13 / Clang 18, -O2; the VPS is noisy, about ±5-10%). µs, best of 3 interleaved rounds of 15 runs; files from nativejson-benchmark. Traversing every value (numbers read as double, strings copied) at this PR:

Apple M1 x86-64 GCC x86-64 Clang
canada 2074 4296 4331
citm_catalog 128 261 265
twitter 107 149 171

get<T>() of arithmetic types is inlined down to the conversion, so its checks of the node kind merge with those of the caller, and reading an integer needs no call.

The core library is unaffected: the three core files only gain the helpers above, and basic_json::type_name() returns the same strings.

Tests

  • Every element and member of 2,000 generated documents, compared with ordered_json.
  • Keys of every length around the load sizes.
  • Duplicate keys: lookups find the first member, iteration visits all.
  • The exceptions, compared with const basic_json.
  • Chained access on discarded views, and every integer type as an array index.
  • Iterators, items, and structured bindings.
  • The conversions of 2,000 generated documents.
  • 20,000 float tokens, compared bit for bit with parse() for double and for float.
  • Every JSON pointer (leaves and their parents) of 1,000 documents.
  • 27 malformed pointers, with the exceptions of at(), operator[], contains(), and value() compared with basic_json, also with JSON_DIAGNOSTICS and JSON_DIAGNOSTIC_POSITIONS.

Public API

No breaking changes to the existing API: this PR adds members of basic_json_view (access functions and value conversions), plus the internal detail::json_pointer_access and detail::value_type_name(). basic_json::type_name() behaves as before. The view API is new in this stack, so its decisions are not breaking relative to a release, but they are visible to users:

  • operator[] and at() accept any integer type as an index.
  • operator[] on a missing key, an out-of-range index, an unresolvable pointer, or a discarded view returns a discarded view instead of invoking undefined behavior as const basic_json::operator[] does; at() throws.
  • Lookups with duplicate keys return the first member; parse(), materialize(), and map conversions keep the last.
  • Exceptions thrown by the view's access functions never carry the JSON_DIAGNOSTICS path.

Written by Claude Code.

🤖 Generated with Claude Code

@nlohmann
nlohmann added this pull request to stack #5636 September 29, 2026 14:20
@nlohmann nlohmann changed the title json view/11 view access Add element access, lookup, and iteration to json_view Sep 29, 2026
@nlohmann

Copy link
Copy Markdown
Owner Author

CI fixes for this PR (commit 958fbc3, plus the lower fixes merged in):

Verified with gcc:16 (linux/amd64, CI flags, C++11/C++20) and with a JSON_NOEXCEPTION build run as --no-throw.

— posted by Claude Code on behalf of @nlohmann

@github-actions

Copy link
Copy Markdown

🔴 Amalgamation check failed! 🔴

The source code has not been amalgamated and/or formatted correctly, or BUILD.bazel is out of date.

@nlohmann
nlohmann removed this pull request from stack #5636 September 30, 2026 13:19
@nlohmann
nlohmann force-pushed the json-view/11-view-access branch from 29c684f to 4d99fe9 Compare September 30, 2026 13:20
@nlohmann
nlohmann added this pull request to stack #5739 September 30, 2026 13:21
@nlohmann
nlohmann force-pushed the json-view/11-view-access branch 3 times, most recently from 5dce18c to a8beec0 Compare September 30, 2026 18:06
@nlohmann nlohmann added the review needed It would be great if someone could review the proposed changes. label Sep 30, 2026
@nlohmann
nlohmann marked this pull request as ready for review September 30, 2026 18:17
@nlohmann
nlohmann force-pushed the json-view/11-view-access branch from a8beec0 to e17e23a Compare September 30, 2026 19:15
Comment thread include/nlohmann/detail/view/lookup.hpp Dismissed
Comment thread single_include/nlohmann/json_view.hpp Dismissed
@nlohmann
nlohmann removed this pull request from stack #5739 October 6, 2026 09:26
@nlohmann
nlohmann changed the base branch from json-view/10-view-document to json-view/08-view-builder October 6, 2026 09:26
@nlohmann
nlohmann force-pushed the json-view/11-view-access branch from 5286c28 to 0adb778 Compare October 6, 2026 09:28
@nlohmann nlohmann changed the title Add element access, lookup, and iteration to json_view Add element access, iteration, values, and JSON pointers to json_view Oct 6, 2026
@nlohmann
nlohmann added this pull request to stack #5768 October 6, 2026 09:29
@nlohmann
nlohmann force-pushed the json-view/11-view-access branch from 0adb778 to 77acd45 Compare October 7, 2026 14:44
@nlohmann
nlohmann force-pushed the json-view/11-view-access branch from c6c0a6d to 237370a Compare October 14, 2026 15:38
@nlohmann
nlohmann force-pushed the json-view/11-view-access branch 2 times, most recently from 29b3c0c to 9e71c7e Compare October 16, 2026 06:46
@nlohmann
nlohmann force-pushed the json-view/11-view-access branch 2 times, most recently from 0539b2d to 2ab1482 Compare October 17, 2026 06:01
@nlohmann
nlohmann force-pushed the json-view/11-view-access branch 2 times, most recently from bc7a04e to 130e073 Compare October 17, 2026 12:28

@gregmarr gregmarr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are too many commits to try to review this commit by commit, since things keep changing. I'm going to submit these now. I hope they still make sense. I was reading commit "Make view lookups last-wins and chained access safe" or earlier when I wrote these. I'm going to switch to reading all the commit notes and then review all of the code at once.


## Exception safety

Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per previous PR discussion, remove the "if an exception is thrown"?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 7b75573: the eight view pages that said "if an exception is thrown, there are no changes ..." now say only "Strong guarantee: there are no changes to the view or the document it refers to", as materialize() does since the review of #5620.

— posted by Claude Code on behalf of @nlohmann


1. The function can throw the following exceptions, both with the same message as the corresponding call to
[`BasicJsonType::at`](../basic_json/at.md):
- Throws [`type_error.304`](../../home/exceptions.md#jsonexceptiontype_error304) if the value is not an object.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worthwhile to reference the new enum in all the docs? That would probably require moving the enum out of detail. Totally separate PR for that I think.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and for the basic_json pages as well, so in a separate PR: exception_id would first have to move out of detail. I'll leave the view pages like the basic_json ones for now.

— posted by Claude Code on behalf of @nlohmann


None of these exceptions carry a [`JSON_DIAGNOSTICS`](../macros/json_diagnostics.md) path: the view has no
`BasicJsonType` value to point at, so the exception is created without one, even if `BasicJsonType` was built with
`JSON_DIAGNOSTICS` enabled.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the data not available through the index? Maybe this can be put alongside that other item to maybe add diagnostics to materialize() in the future.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The index has no parent links, so the path isn't stored, but it could be derived by walking down from the document root, and only on the throw path. I'll track it with the materialize() diagnostics follow-up (#5864) rather than doing it here.

— posted by Claude Code on behalf of @nlohmann


1. Linear in the number of members: as for [`ordered_json`](../ordered_json.md), members are compared one after
another, in document order, stopping at the first match. Each comparison first checks the key's length --
already known from the index, without reading the key bytes -- before comparing its content.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it were to iterate in the reverse order, would it produce the same results of a basic_json::parse() where later ones overwrite earlier ones?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see in another docs section that there are no reverse iterators. I suspect that means that this isn't possible.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and yet in the next commit, there it is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then it disappears again

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the policy went back and forth across the commits. It is first-wins again since 9cc0ff9 (lookups stop at the first match, and there are no reverse iterators), with the reasons in Duplicate keys. Sorry for the churn while you were reading.

— posted by Claude Code on behalf of @nlohmann


## Exception safety

Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exceptions again, should probably just look at all of them.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I went through all of them in 7b75573 (see the reply on at.md).

— posted by Claude Code on behalf of @nlohmann


!!! danger "Lifetime issues"

As for `BasicJsonType::items()`, calling `items()` on a temporary view (or a temporary document) is dangerous:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we disable it then?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isn't needed: the range returned by items() holds a copy of the view, so v["a"].items() in a range-for is safe. Calling root() on a temporary document is already rejected (root() const&& is deleted). The "Lifetime issues" box was wrong; it says this now (7b75573).

— posted by Claude Code on behalf of @nlohmann

[runtime assertion](../../features/assertions.md)) for a missing key on a **const** value, this operator always
returns a safe, testable result: a [discarded](is_discarded.md) view, which is `#!cpp false` in a boolean context.
There is also no non-const overload that inserts a missing key or extends an array -- a view never modifies the
document.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel the foreshadowing...

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in this stack: editing (#5630 and later) goes through the document (set, insert, erase), and views stay read-only.

— posted by Claude Code on behalf of @nlohmann

a string representation of the type ([`value_t`](../basic_json/value_t.md)):

| Value type | return value |
|-----------------------------------------------------|---------------|

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix table alignment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7b75573.

— posted by Claude Code on behalf of @nlohmann

@@ -24,7 +24,7 @@ Constant.

For an object, iteration visits **every** member, including all occurrences of a duplicate key -- unlike

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole paragraph now needs to be rewritten.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably was in a later commit, after the decision was reversed.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the paragraph says "first" again since 9cc0ff9.

— posted by Claude Code on behalf of @nlohmann

## Return value
An iterator to the member with key `key`, or [`end()`](end.md) if there is none or the value is not an object.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last member?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reversed

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Settled on "first" (see the reply on at.md); find.md says "the first one" now.

— posted by Claude Code on behalf of @nlohmann

@nlohmann
nlohmann force-pushed the json-view/11-view-access branch from 130e073 to 426dec2 Compare October 17, 2026 21:25

@gregmarr gregmarr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions can produce a different result in basic_json_view than the materialized basic_json will. Does this need to be called out in the see also text?

Comment thread docs/mkdocs/docs/api/basic_json/find.md Outdated

- [count](count.md) returns the number of occurrences of a key
- [contains](contains.md) checks whether a key exists
- [basic_json_view::find](../basic_json_view/find.md) - the same lookup on a zero-copy view

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But behaves differently.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The see-also entries on the four basic_json pages now say how the view differs (7b75573): find/operator[] resolve a duplicate key to the first member while parse() keeps the last, operator[] returns a discarded view instead of undefined behavior, and front/items follow document order and visit duplicates.

— posted by Claude Code on behalf of @nlohmann

## See also

- [back](back.md) to access the last element
- [basic_json_view::front](../basic_json_view/front.md) - the same access on a zero-copy view

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but can produce a different result?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, called out in 7b75573 (see the reply on find.md).

— posted by Claude Code on behalf of @nlohmann


- [begin](begin.md) returns an iterator to the first element
- [end](end.md) returns an iterator to one past the last element
- [basic_json_view::items](../basic_json_view/items.md) - the same range on a zero-copy view (`#!cpp const auto`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But can produce different iteration order.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, called out in 7b75573 (see the reply on find.md).

— posted by Claude Code on behalf of @nlohmann

- see [`at`](at.md) for access by reference with range checking
- see [`value`](value.md) for access with default value
- [basic_json_view::operator[]](../basic_json_view/operator%5B%5D.md) - the same access on a zero-copy view (always
returns a discarded view instead of assuming undefined behavior)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But can produce a different element

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, called out in 7b75573 (see the reply on find.md).

— posted by Claude Code on behalf of @nlohmann


Linear in the size of the array or object: unlike [`front()`](front.md), which only ever looks at the first
element, `back()` has to walk every element to find where they end, since elements are not a fixed size in the
index.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this happen enough that it should have an index entry? I assume this would make the entry bigger. This bigger entry might make some of the other difficult things easier, but will make it take up more space, and might have other performance impacts due to larger reads or more reads.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not with the current node: it is 16 bytes, containers already use off, len and next, and the 16-bit extra holds the hash index number of a large object (#5628). A wider node would cost every value for one rarely used function. I'd keep back() linear and documented, and revisit if a benchmark shows it matters.

— posted by Claude Code on behalf of @nlohmann


Unlike [`BasicJsonType::back()`](../basic_json/back.md), which has undefined behavior for an empty array or object,
`back()` throws `invalid_iterator.214` in that case -- the same way it already does for `#!json null` and for a
discarded view, where `BasicJsonType::back()` also throws.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems overly complex.

Unlike BasicJsonType::back(), which has undefined behavior for an empty array or object, back() throws invalid_iterator.214 for an empty array, an empty object, a null value, or a discarded view.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, taken as you wrote it in 7b75573.

— posted by Claude Code on behalf of @nlohmann


Because objects are iterated in document order rather than sorted by key, the order seen here can differ from what
iterating the [`materialize()`](materialize.md)d `BasicJsonType` value would produce: a `#!cpp basic_json` object
(`std::map`-backed by default) sorts its keys, while a view does not.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention that ordered_json doesn't sort either, but does remove duplicates?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 7b75573: ordered_json keeps document order too, but only one entry per key (the last value, at the first occurrence's position), as parse() does.

— posted by Claude Code on behalf of @nlohmann

an `ObjectType` that allows multiple entries per key, `count()` here never does: it is exactly
[`contains()`](contains.md) as `#!cpp 0`/`#!cpp 1`. This holds even if the source text has a duplicate key -- see the
[Notes on duplicate keys](operator[].md#notes) of `operator[]` -- because a `#!cpp count() > 1` result would require
counting every member with a matching key (the lookup functions resolve to the *first* one).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reason to use this function over contains is to find out how many actually exist, so it seems like it should be the actual result. This matches the complexity of unordered_multimap.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, done in 7b75573 (see the reply above): count() returns the number of members with the key, linear in the number of members, like unordered_multimap::count.

— posted by Claude Code on behalf of @nlohmann

Returns an iterator to one past the last element of an array, one past the last member value of an object, in
**document order** -- see [`begin()`](begin.md). A primitive value iterates as a range of one element (itself);
`#!json null` and a [discarded](is_discarded.md) view iterate as an empty range, so `#!cpp begin() == end()` for
them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be end() = begin() + 1 for a primitive or you can't iterate over them.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is already: for a primitive, end() is one past the node (m_node + 1), so begin()/end() is a range of one element. Only null and a discarded view are empty ranges, as for basic_json. end.md says so in its first lines.

— posted by Claude Code on behalf of @nlohmann

return index_status::unresolved;
}
std::uint64_t v = 0;
for (std::size_t i = 0; i < s.size(); ++i)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

range for?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, done in 7b75573.

— posted by Claude Code on behalf of @nlohmann

Comment thread include/nlohmann/json_view.hpp Outdated
{
if (is_discarded())
{
return basic_json_view();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this just be return {} to avoid repeating the return type?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, done for all three in 7b75573.

— posted by Claude Code on behalf of @nlohmann


/// any other integer type, see operator[]; negative values are out of
/// range
template < typename IntegerType, typename std::enable_if < detail::view::is_index_type<IntegerType>::value, int >::type = 0 >

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

astyle?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That spacing (template < typename T, typename std::enable_if < ... >::type = 0 >) is what the pinned astyle produces for this pattern; json.hpp has the same. make pretty leaves it like this.

— posted by Claude Code on behalf of @nlohmann

/// the value a JSON pointer refers to converted to T, or the default
/// value if the pointer cannot be resolved. Throws type_error.306 if this
/// is neither an object nor an array.
template < typename T, typename std::enable_if < !std::is_same<typename std::decay<T>::type, const char*>::value, int >::type = 0 >

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

astyle

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: astyle's output for this pattern.

— posted by Claude Code on behalf of @nlohmann

}

template<typename T>
T& get_to(T& v) const

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the whole point of get_to<> was to use the existing storage of v.

Oh, also to add the json's data to the object, which may have values already, using from_json().

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, it differed from basic_json::get_to(). Since 7b75573, types that get<T>() converts directly (numbers, strings, std::vector, maps with string keys, ...) are assigned, and all others go to materialize().get_to(v), i.e. from_json with the existing value, so a from_json that sets only the fields present keeps the others. There's a test for exactly that case.

— posted by Claude Code on behalf of @nlohmann

Give basic_json_view the read-only access functions of basic_json:
operator[] and at() with keys and indices, front()/back(), find(),
contains(), count(), begin()/end() and cbegin()/cend(), items()
with structured bindings from C++17 on, and type_name().

Exceptions have the ids and messages of the const functions of
basic_json. Where basic_json has undefined behavior the view
answers safely: operator[] with a missing key or an out-of-range
index returns a discarded view, and front()/back() of an empty
container throw invalid_iterator.214. Objects are iterated in
document order, and all members are visited; duplicate-key lookups
find the first member (as yyjson and simdjson do), while parse(),
materialize(), and the map conversions keep the last value, as
parse() does. Keys of up to 16 bytes are compared with two
overlapping loads.

Add value conversions: get<T>()/get_to() for arithmetic types,
bool, nullptr_t, strings (std::basic_string copied,
string_view_t without a copy), BasicJsonType, views, std::vector,
and maps with string keys; get_string() for the string without a
copy; number_token() for the number exactly as written in the
source; value() with keys and JSON pointers; and operator[]/at()/
contains() with JSON pointers. Everything else, including types
with from_json(), goes through materialize() of that subtree.
get<T>() of arithmetic types is inlined down to the conversion, so
reading an integer needs no call.

detail::json_pointer_access exposes a pointer's reference tokens
to code outside basic_json.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
* Lookups (operator[], at, find, contains, count, value, JSON pointers)
  return the last member of a duplicate key, as materialize() and
  parse() keep it.
* operator[] on a discarded view returns a discarded view instead of
  throwing, so v["a"]["b"] is safe for a missing "a".
* operator[] and at() take any integer type (not only int and size_t),
  fixing ambiguous calls with unsigned, long, std::int64_t, ...
* Fix the operator[] documentation, which claimed a discarded view for
  a type mismatch where type_error.305 is thrown.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…d its tests

A view is tested with is_discarded(); the lookups in at(), value() and
contains(json_pointer) and the unit tests no longer rely on the explicit
conversion to bool.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…mporary

root() of an rvalue document is deleted, because the views would dangle.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…xamples

The explicit conversion to bool of basic_json_view is gone.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Looking up the last member of a duplicate key cannot stop at a match, so
every lookup scanned the whole object (1.6 to 3.4 times slower for small
objects). operator[](key), at, find, value, contains, count, and JSON
pointer resolution return the first member again, as yyjson and simdjson
do; materialize() and get<map>() keep the last value, as parse().

The documentation says so in the feature page and on each lookup page,
and explains how to get the value parse() would give. The integer index
templates and the discarded chaining of operator[] stay.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
__forceinline makes MSVC report C4714 for function templates it cannot
inline (get<std::string>), an error under /WX. The forced inlining is
only a performance hint, so use plain inline there.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Mark the includes that are needed by design (json.hpp, the macro
unscope, <tuple> for the tuple_size specialization), take size_t from
<cstring>, and drop the forward declaration of basic_json_document that
the friend declaration makes redundant.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
ci_single_binaries runs IWYU with --error on every header. For json_view.hpp
it suggested adding <array> (std::array is used), the headers that json.hpp
already provides (abi_config, abi_macros, input_adapters, json_pointer,
cpp_future, string_concat, value_t, json_fwd), and <version> for
std::nullptr_t, and removing <cstddef>.

- include <array>
- keep <cstddef> (nullptr_t, size_t; IWYU attributes them to <version> and <cstring>)
- tell IWYU not to suggest the headers that json.hpp provides: the amalgamated
  json_view.hpp only includes json.hpp, so including them here would duplicate
  their definitions

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The same changes as on the dump branch, where they were first made, so
that this branch passes clang-tidy on its own.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
… view

Develop's #5800 made basic_json report a JSON pointer array index such as
"/a" as parse_error.109 instead of out_of_range.404, as longer ones were;
the view's array_index() follows, with "-" and the empty token still
unresolved.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann
nlohmann force-pushed the json-view/11-view-access branch from 426dec2 to f2582da Compare October 18, 2026 04:25
From gregmarr's review of #5622:

- count() counts every member with the key, so count(key) > 1 detects
  duplicate keys; it was contains() as 0/1 before.
- get_to() passes types that get<T>() does not convert directly to
  from_json with the existing value, as basic_json::get_to() does, so that
  a from_json which sets only the fields present keeps the others.
- array_index() checks the empty token and "-" first; the results are
  unchanged.
- Docs: no "if an exception is thrown" where a view never changes
  anything; the borrowed source must also stay unmodified; items() on a
  temporary view is safe; back()/front() notes shortened; the basic_json
  see-also entries say how the view's results can differ.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CMake documentation L review needed It would be great if someone could review the proposed changes. tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants