diff --git a/BUILD.bazel b/BUILD.bazel
index 055646978..9b76c1c6d 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -68,6 +68,7 @@ cc_library(
"include/nlohmann/detail/string_utils.hpp",
"include/nlohmann/detail/value_t.hpp",
"include/nlohmann/detail/view/builder.hpp",
+ "include/nlohmann/detail/view/compare.hpp",
"include/nlohmann/detail/view/document_data.hpp",
"include/nlohmann/detail/view/errors.hpp",
"include/nlohmann/detail/view/input.hpp",
@@ -80,6 +81,7 @@ cc_library(
"include/nlohmann/detail/view/number.hpp",
"include/nlohmann/detail/view/pointer.hpp",
"include/nlohmann/detail/view/scan.hpp",
+ "include/nlohmann/detail/view/serializer.hpp",
"include/nlohmann/detail/view/string_ref.hpp",
"include/nlohmann/detail/view/value.hpp",
"include/nlohmann/json.hpp",
diff --git a/docs/docset/docSet.sql b/docs/docset/docSet.sql
index 4764185e1..2f8685b40 100644
--- a/docs/docset/docSet.sql
+++ b/docs/docset/docSet.sql
@@ -154,6 +154,7 @@ INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::cbegin', 'Me
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::cend', 'Method', 'api/basic_json_view/cend/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::contains', 'Method', 'api/basic_json_view/contains/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::count', 'Method', 'api/basic_json_view/count/index.html');
+INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::dump', 'Method', 'api/basic_json_view/dump/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::empty', 'Method', 'api/basic_json_view/empty/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::end', 'Method', 'api/basic_json_view/end/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::find', 'Method', 'api/basic_json_view/find/index.html');
@@ -176,9 +177,13 @@ INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::is_string',
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::is_structured', 'Method', 'api/basic_json_view/is_structured/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::items', 'Method', 'api/basic_json_view/items/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::materialize', 'Method', 'api/basic_json_view/materialize/index.html');
+INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::number_format', 'Enum', 'api/basic_json_view/number_format/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::number_token', 'Method', 'api/basic_json_view/number_token/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::operator bool', 'Method', 'api/basic_json_view/operator_bool/index.html');
+INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::operator<<', 'Operator', 'api/basic_json_view/operator_ltlt/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::operator[]', 'Operator', 'api/basic_json_view/operator[]/index.html');
+INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::operator==', 'Operator', 'api/basic_json_view/operator_eq/index.html');
+INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::operator!=', 'Operator', 'api/basic_json_view/operator_ne/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::size', 'Method', 'api/basic_json_view/size/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::source_offset', 'Method', 'api/basic_json_view/source_offset/index.html');
INSERT INTO searchIndex(name, type, path) VALUES ('basic_json_view::type', 'Method', 'api/basic_json_view/type/index.html');
diff --git a/docs/mkdocs/docs/api/basic_json/dump.md b/docs/mkdocs/docs/api/basic_json/dump.md
index eb13717e4..23c34cd0c 100644
--- a/docs/mkdocs/docs/api/basic_json/dump.md
+++ b/docs/mkdocs/docs/api/basic_json/dump.md
@@ -91,6 +91,8 @@ Binary values are serialized as an object containing two keys:
- [to_string](to_string.md) returns a string representation of a JSON value
- [operator<<](../operator_ltlt.md) serialize to stream
+- [`basic_json_view::dump`](../basic_json_view/dump.md) the corresponding function of `basic_json_view`, serializing
+ directly from a flat index without building a `basic_json` value
- [Serialization](../../features/serialization.md) - the serialization article
## Version history
diff --git a/docs/mkdocs/docs/api/basic_json/operator_eq.md b/docs/mkdocs/docs/api/basic_json/operator_eq.md
index 58a7e74da..78002fd6c 100644
--- a/docs/mkdocs/docs/api/basic_json/operator_eq.md
+++ b/docs/mkdocs/docs/api/basic_json/operator_eq.md
@@ -171,6 +171,8 @@ Linear.
- [operator!=](operator_ne.md) compare for inequality
- [operator<=>](operator_spaceship.md) comparison: 3-way (C++20)
+- [basic_json_view::operator==](../basic_json_view/operator_eq.md) - the same comparison on a zero-copy view, without
+ building a `basic_json` value for it
## Version history
diff --git a/docs/mkdocs/docs/api/basic_json/operator_ne.md b/docs/mkdocs/docs/api/basic_json/operator_ne.md
index ceeb31ab3..e759a6327 100644
--- a/docs/mkdocs/docs/api/basic_json/operator_ne.md
+++ b/docs/mkdocs/docs/api/basic_json/operator_ne.md
@@ -95,6 +95,8 @@ Linear.
- [operator==](operator_eq.md) comparison: equal
- [operator<=>](operator_spaceship.md) comparison: 3-way (C++20)
+- [basic_json_view::operator!=](../basic_json_view/operator_ne.md) - the same comparison on a zero-copy view, without
+ building a `basic_json` value for it
## Version history
diff --git a/docs/mkdocs/docs/api/basic_json_view/dump.md b/docs/mkdocs/docs/api/basic_json_view/dump.md
new file mode 100644
index 000000000..7cc0b6b38
--- /dev/null
+++ b/docs/mkdocs/docs/api/basic_json_view/dump.md
@@ -0,0 +1,102 @@
+# nlohmann::basic_json_view::dump
+
+```cpp
+string_t dump(const int indent = -1,
+ const char indent_char = ' ',
+ const bool ensure_ascii = false,
+ const number_format numbers = number_format::shortest) const;
+```
+
+Serializes this value (and its subtree) directly from the flat index, without ever building a `BasicJsonType` value
+first. With the default `#!cpp numbers == number_format::shortest`, the result is the same string
+[`BasicJsonType::dump`](../basic_json/dump.md) would produce for the value
+[`BasicJsonType::parse()`](../basic_json/parse.md) builds from the same source text, called with the same `indent`,
+`indent_char`, and `ensure_ascii` -- except that members of an object appear in document order rather than sorted by
+key, and *every* occurrence of a repeated key is written rather than only the last one (see
+[Notes on duplicate keys](operator[].md#notes)). For a `json_view` (whose `BasicJsonType` is not ordered), this means
+`dump()` can print an object's members in a different order than [`materialize()`](materialize.md)`.dump()` of the
+same subtree.
+
+## Parameters
+
+`indent` (in)
+: If `indent` is nonnegative, array elements and object members are pretty-printed with that indent level. An
+ indent level of `0` only inserts newlines. `-1` (the default) selects the most compact representation.
+
+`indent_char` (in)
+: The character used for indentation if `indent` is greater than `0`. The default is ` ` (space).
+
+`ensure_ascii` (in)
+: If `ensure_ascii` is `#!cpp true`, all non-ASCII characters in the output are escaped with `\uXXXX` sequences, and
+ the result consists of ASCII characters only.
+
+`numbers` (in)
+: how to write numbers, see [`number_format`](number_format.md): `shortest` (the default) writes them the way
+ [`BasicJsonType::dump`](../basic_json/dump.md) would; `source` copies every number exactly as it appears in the
+ source text.
+
+## Return value
+
+string containing the serialization of this value, or `#!cpp ""` if the view is
+[discarded](is_discarded.md).
+
+## Exception safety
+
+Strong exception safety: if an exception is thrown, there are no changes to the view or the document it refers to.
+
+## Exceptions
+
+May throw `#!cpp std::bad_alloc` if allocating the output string fails. Unlike
+[`BasicJsonType::dump`](../basic_json/dump.md), there is no `error_handler` parameter and no
+[`type_error.316`](../../home/exceptions.md#jsonexceptiontype_error316): the view only ever holds text the parser
+already validated as UTF-8, so there is nothing to replace or ignore.
+
+## Complexity
+
+Linear in the size of the output text.
+
+## Notes
+
+The walk over the subtree is iterative, so the nesting depth it can write is limited by available memory only, not by
+the call stack -- as for [`materialize()`](materialize.md).
+
+Strings are escaped by the same rules as [`BasicJsonType::dump`](../basic_json/dump.md). With
+`#!cpp numbers == number_format::shortest`, floats are written with the library's shortest round-trip conversion,
+exactly as [`BasicJsonType::dump`](../basic_json/dump.md) would (e.g. `#!cpp 1.5`, `#!cpp 100.0`, `#!cpp 1e+100`), and
+integers are copied from the source text -- already canonical in JSON, so this matches their shortest form too --
+except that `#!cpp -0` is written as `#!cpp 0`, the way [`BasicJsonType::parse()`](../basic_json/parse.md) reads it.
+`#!cpp number_format::source` copies every number exactly as written in the source text instead, with no exception
+for `#!cpp -0` -- `#!cpp 1.50`, `#!cpp 1E2`, `#!cpp -0.0`, `#!cpp -0`, or all digits of an integer literal with more
+digits than any number type holds (such a literal is itself classified as a float, see
+[What is different](../../features/json_view.md#what-is-different)) -- something `BasicJsonType` cannot do, since
+parsing already reduces every number to its parsed value.
+
+## Examples
+
+??? example
+
+ The example below forwards a single record out of a larger batch, and re-serializes a configuration file, both
+ without ever building a `BasicJsonType` value for the surrounding array or for the parts of it that were not
+ needed. It also shows that [`materialize()`](materialize.md)`.dump()` of the configuration sorts its keys, where
+ `dump()` on the view keeps the order they appear in the source text.
+
+ ```cpp
+ --8<-- "examples/basic_json_view__dump.cpp"
+ ```
+
+ Output:
+
+ ```json
+ --8<-- "examples/basic_json_view__dump.output"
+ ```
+
+## See also
+
+- [`number_format`](number_format.md) - how `dump()` writes numbers
+- [operator<<](operator_ltlt.md) - serialize this value to a stream
+- [materialize](materialize.md) - build a `BasicJsonType` value, e.g. to use `BasicJsonType::dump`'s `error_handler`
+- [`BasicJsonType::dump`](../basic_json/dump.md) - the corresponding function of `basic_json`
+
+## Version history
+
+- Added in version 3.13.0.
diff --git a/docs/mkdocs/docs/api/basic_json_view/index.md b/docs/mkdocs/docs/api/basic_json_view/index.md
index 50bcaa24b..56760db2f 100644
--- a/docs/mkdocs/docs/api/basic_json_view/index.md
+++ b/docs/mkdocs/docs/api/basic_json_view/index.md
@@ -20,11 +20,12 @@ Moving the document itself does not invalidate its views: the index is heap-allo
`basic_json_document` object.
`basic_json_view` provides the read-only part of the `BasicJsonType` interface: the type-inspection functions, element
-access, lookup, iteration, and conversion -- [`get()`](get.md), [`get_string()`](get_string.md),
+access, lookup, iteration, conversion, and comparison -- [`get()`](get.md), [`get_string()`](get_string.md),
[`number_token()`](number_token.md), and [`materialize()`](materialize.md) to build the `BasicJsonType` value of a
subtree on demand. [`operator[]`](operator%5B%5D.md), [`at`](at.md), [`contains`](contains.md), and
-[`value`](value.md) also accept a [`json_pointer`](../json_pointer/index.md). It does not (yet) provide `dump()` or
-comparison.
+[`value`](value.md) also accept a [`json_pointer`](../json_pointer/index.md). [`operator==`](operator_eq.md) and
+[`operator!=`](operator_ne.md) compare two views, or a view and a `BasicJsonType` value, without ever building a
+`BasicJsonType` value for a view; no ordering comparison (`#!cpp operator<`) is provided.
## Template parameters
@@ -47,6 +48,7 @@ comparison.
- **iterator**, **const_iterator** - a forward iterator over the elements of an array or the member values of an
object, in document order; both names refer to the same type, since a view is always read-only
- **item** - a (key, value) pair produced by [`items()`](items.md)
+- [**number_format**](number_format.md) - how [`dump()`](dump.md) writes numbers
## Member functions
@@ -106,6 +108,16 @@ comparison.
- [**number_token**](number_token.md) - get a number's token text without a copy
- [**materialize**](materialize.md) - build the `BasicJsonType` value of this subtree
+### Comparison
+
+- [**operator==**](operator_eq.md) - comparison: equal
+- [**operator!=**](operator_ne.md) - comparison: not equal
+
+### Serialization
+
+- [**dump**](dump.md) - serialize to a JSON-formatted string
+- [**operator<<**](operator_ltlt.md) - serialize to stream
+
### Source access
- [**source_offset**](source_offset.md) - byte offset of this value in the document's source text
diff --git a/docs/mkdocs/docs/api/basic_json_view/number_format.md b/docs/mkdocs/docs/api/basic_json_view/number_format.md
new file mode 100644
index 000000000..6b8c6805d
--- /dev/null
+++ b/docs/mkdocs/docs/api/basic_json_view/number_format.md
@@ -0,0 +1,51 @@
+# nlohmann::basic_json_view::number_format
+
+```cpp
+enum class number_format {
+ shortest,
+ source
+};
+```
+
+This enumeration is used in [`dump`](dump.md) to choose how numbers are written. Two values are differentiated:
+
+shortest
+: integers are copied from the source text -- already canonical in JSON -- except that `#!cpp -0` becomes
+ `#!cpp 0`, the way [`BasicJsonType::parse()`](../basic_json/parse.md) reads it; floats are written with the
+ library's shortest round-trip conversion, exactly as [`BasicJsonType::dump()`](../basic_json/dump.md) would (e.g.
+ `#!cpp 1.5`, `#!cpp 100.0`, `#!cpp 1e+100`)
+
+source
+: every number is copied exactly as it appears in the source text -- `#!cpp 1.50`, `#!cpp 1E2`, `#!cpp -0`, all
+ digits of an integer literal with more digits than any number type holds -- something `BasicJsonType` cannot do,
+ since parsing already reduces every number to its parsed value
+
+## Examples
+
+??? example
+
+ The example below writes back a price list received from a supplier: with `number_format::shortest` (the
+ default), a trailing zero and scientific notation are normalized away and a long account number that overflows
+ every number type is rounded, the same way `#!cpp materialize().dump()` (or `basic_json::dump()`) would;
+ `number_format::source` keeps every number exactly as it was written in the source text instead.
+
+ ```cpp
+ --8<-- "examples/basic_json_view__number_format.cpp"
+ ```
+
+ Output:
+
+ ```json
+ --8<-- "examples/basic_json_view__number_format.output"
+ ```
+
+## See also
+
+- [dump](dump.md) - serialize to a JSON-formatted string
+- [number_token](number_token.md) - get a single number's token text without dumping the whole value
+- [`BasicJsonType::error_handler_t`](../basic_json/error_handler_t.md) - the analogous enumeration for
+ `BasicJsonType::dump`'s decoding-error behavior
+
+## Version history
+
+- Added in version 3.13.0.
diff --git a/docs/mkdocs/docs/api/basic_json_view/operator_eq.md b/docs/mkdocs/docs/api/basic_json_view/operator_eq.md
new file mode 100644
index 000000000..6f96f4692
--- /dev/null
+++ b/docs/mkdocs/docs/api/basic_json_view/operator_eq.md
@@ -0,0 +1,107 @@
+# nlohmann::basic_json_view::operator==
+
+```cpp
+// (1)
+bool operator==(const basic_json_view& lhs, const basic_json_view& rhs);
+
+// (2)
+bool operator==(const basic_json_view& lhs, const BasicJsonType& rhs);
+bool operator==(const BasicJsonType& lhs, const basic_json_view& rhs);
+```
+
+1. Compares two views for equality: whether the values [`BasicJsonType::parse()`](../basic_json/parse.md) would
+ produce for `lhs` and `rhs` are equal, according to `BasicJsonType`'s [`operator==`](../basic_json/operator_eq.md).
+2. Compares a view and a `BasicJsonType` value for equality, in either order: whether the value `parse()` would
+ produce for the view and the other operand are equal, according to `BasicJsonType`'s
+ [`operator==`](../basic_json/operator_eq.md).
+
+Neither overload builds a `BasicJsonType` value for a view to do the comparison (see [Notes](#notes) below). Numbers
+compare by value across their types (`#!cpp 1 == 1.0`), and an object compares by its members, with duplicate keys
+resolved exactly as `parse()` resolves them -- the last value, at the position of the first occurrence of the key.
+
+## Parameters
+
+`lhs` (in)
+: first value to consider
+
+`rhs` (in)
+: second value to consider
+
+## Return value
+
+whether the values `lhs` and `rhs` are equal
+
+## Exception safety
+
+Strong exception safety: if an exception is thrown, there are no changes to either operand, or to the document(s) a
+view refers to.
+
+## Exceptions
+
+May throw `#!cpp std::bad_alloc`. Unlike the other comparison and most other `basic_json_view` functions,
+`operator==` is not `#!cpp noexcept`: resolving an object's members needs a temporary array to sort them by key (see
+[Complexity](#complexity) below), and that allocation can fail.
+
+## Complexity
+
+Linear in the size of the compared values: every number, string, array element, and object member is visited at most
+once, and the walk is iterative, so the nesting depth it can compare is limited by available memory only, not by the
+call stack (as for [`materialize()`](materialize.md)). Resolving an object's members takes an additional O(n log n)
+in the number of members at that level, since they are sorted by key to detect and resolve duplicates before being
+compared. Two arrays of different [`size()`](size.md) are rejected without visiting either one's elements.
+
+## Notes
+
+Only a single number, boolean, or `#!cpp null` value is ever materialized into a `BasicJsonType`, to reuse its
+`operator==` -- for numbers, so that values written differently in the source text but equal in value (e.g. an
+integer and a floating-point literal) still compare equal, following the same rules `BasicJsonType` does for special
+values such as `#!cpp NaN`. Constructing one of these scalars never allocates. Strings are compared directly, without
+allocating, either from the source text on both sides or, for overload 2, against `BasicJsonType`'s own string.
+Arrays and objects are never materialized at all; only their elements or members are visited, one pair at a time.
+
+!!! info "How objects are compared"
+
+ For a [`json_view`](../json_view.md) (`BasicJsonType::object_t` is `#!cpp std::map`), members are compared by
+ key, regardless of the order they appear in the source text. For an
+ [`ordered_json_view`](../ordered_json_view.md) (`object_t` is `ordered_map`), they are compared in the order
+ they occur, so the very same two objects with their members reordered can compare equal as `json_view`s but not
+ as `ordered_json_view`s. This is exactly how [`json`](../json.md) and [`ordered_json`](../ordered_json.md)
+ compare, see ["Comparing different `basic_json` specializations"](../basic_json/operator_eq.md#notes).
+
+!!! info "Discarded views"
+
+ A [discarded](is_discarded.md) view compares the same way a discarded `BasicJsonType` value does, which is
+ governed by
+ [`JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON`](../macros/json_use_legacy_discarded_value_comparison.md): by
+ default, a discarded view is never equal to anything, not even another discarded view.
+
+No ordering comparison (`#!cpp operator<`) is provided for `basic_json_view`; [`materialize()`](materialize.md) is
+the way to get a `BasicJsonType` value that supports it.
+
+## Examples
+
+??? example
+
+ The example below checks whether a newly received configuration differs from the previous one, and whether a
+ received document matches what a test expects -- directly on views, without ever materializing a `BasicJsonType`
+ value for either side.
+
+ ```cpp
+ --8<-- "examples/basic_json_view__operator_eq.cpp"
+ ```
+
+ Output:
+
+ ```json
+ --8<-- "examples/basic_json_view__operator_eq.output"
+ ```
+
+## See also
+
+- [operator!=](operator_ne.md) - compare for inequality
+- [materialize](materialize.md) - build a `BasicJsonType` value, e.g. to keep comparing after the document is gone
+- [`BasicJsonType::operator==`](../basic_json/operator_eq.md) - the corresponding function of `basic_json`
+
+## Version history
+
+- Added in version 3.13.0.
diff --git a/docs/mkdocs/docs/api/basic_json_view/operator_ltlt.md b/docs/mkdocs/docs/api/basic_json_view/operator_ltlt.md
new file mode 100644
index 000000000..c2735b360
--- /dev/null
+++ b/docs/mkdocs/docs/api/basic_json_view/operator_ltlt.md
@@ -0,0 +1,74 @@
+# nlohmann::basic_json_view::operator<<
+
+```cpp
+std::ostream& operator<<(std::ostream& o, const basic_json_view& v);
+```
+
+Not available when [`JSON_NO_IO`](../macros/json_no_io.md) is defined.
+
+Serializes the given view `v` to the output stream `o`, using [`dump`](dump.md) -- exactly as
+`#!cpp operator<<(std::ostream&, const basic_json&)` does for a `basic_json` value.
+
+- The indentation of the output can be controlled with the member variable `width` of the output stream `o`. For
+ instance, using the manipulator `std::setw(4)` on `o` sets the indentation level to `4`, and the serialization
+ result is the same as calling `#!cpp v.dump(4)`. A `width` of `0` or less (the default) selects the most compact
+ representation, as `#!cpp v.dump(-1)` does.
+- The indentation character can be controlled with the member variable `fill` of the output stream `o`. For instance,
+ the manipulator `std::setfill('\t')` sets indentation to use a tab character rather than the default space
+ character.
+- As for `basic_json`, `o`'s `width` is reset to `0` after this call, whether or not it was greater than `0` before.
+
+Numbers are always written as `#!cpp v.dump()` writes them by default, i.e. as with
+[`number_format::shortest`](number_format.md); there is no way to select `#!cpp number_format::source` through the
+stream.
+
+## Parameters
+
+`o` (in, out)
+: stream to write to
+
+`v` (in)
+: view to serialize
+
+## Return value
+
+the stream `o`
+
+## Exceptions
+
+May throw `#!cpp std::bad_alloc`, propagated from [`dump`](dump.md#exceptions). Unlike
+`#!cpp operator<<(std::ostream&, const basic_json&)`, there is no UTF-8 decoding step that could throw
+[`type_error.316`](../../home/exceptions.md#jsonexceptiontype_error316), and no `error_handler` to choose between --
+see the [Exceptions](dump.md#exceptions) of `dump`.
+
+## Complexity
+
+Linear, as [`dump`](dump.md#complexity).
+
+## Examples
+
+??? example
+
+ The example below writes one record out of a larger batch straight to a log stream -- compact for a one-line
+ entry, and pretty-printed with `std::setw`/`std::setfill` for a readable dump -- without ever building a
+ `BasicJsonType` value for the record, or for the rest of the batch.
+
+ ```cpp
+ --8<-- "examples/basic_json_view__operator_ltlt.cpp"
+ ```
+
+ Output:
+
+ ```json
+ --8<-- "examples/basic_json_view__operator_ltlt.output"
+ ```
+
+## See also
+
+- [dump](dump.md) - serialize to a JSON-formatted string
+- [`operator<<(std::ostream&)`](../operator_ltlt.md) - the corresponding operator for `basic_json`
+- [`JSON_NO_IO`](../macros/json_no_io.md) - switch off functions relying on certain C++ I/O headers
+
+## Version history
+
+- Added in version 3.13.0.
diff --git a/docs/mkdocs/docs/api/basic_json_view/operator_ne.md b/docs/mkdocs/docs/api/basic_json_view/operator_ne.md
new file mode 100644
index 000000000..2e85f8abe
--- /dev/null
+++ b/docs/mkdocs/docs/api/basic_json_view/operator_ne.md
@@ -0,0 +1,82 @@
+# nlohmann::basic_json_view::operator!=
+
+```cpp
+// (1)
+bool operator!=(const basic_json_view& lhs, const basic_json_view& rhs);
+
+// (2)
+bool operator!=(const basic_json_view& lhs, const BasicJsonType& rhs);
+bool operator!=(const BasicJsonType& lhs, const basic_json_view& rhs);
+```
+
+1. Compares two views for inequality. Returns `#!cpp !(lhs == rhs)`, see [operator==](operator_eq.md).
+2. Compares a view and a `BasicJsonType` value for inequality, in either order. Returns `#!cpp !(lhs == rhs)` (or,
+ for the reversed order, `#!cpp !(rhs == lhs)`), see [operator==](operator_eq.md).
+
+Since `operator!=` is defined as the negation of [`operator==`](operator_eq.md), it follows the same rules for
+special cases: for instance, since a [discarded](is_discarded.md) view is never equal to anything by default (see
+[operator=='s Notes](operator_eq.md#notes)), it is never *unequal* to anything either -- `#!cpp discarded != discarded`
+is also `#!cpp false`, exactly as for a discarded `BasicJsonType` value.
+
+## Parameters
+
+`lhs` (in)
+: first value to consider
+
+`rhs` (in)
+: second value to consider
+
+## Return value
+
+whether the values `lhs` and `rhs` are not equal
+
+## Exception safety
+
+Strong exception safety: if an exception is thrown, there are no changes to either operand, or to the document(s) a
+view refers to.
+
+## Exceptions
+
+May throw `#!cpp std::bad_alloc`, propagated from [`operator==`](operator_eq.md#exceptions). Unlike most other
+`basic_json_view` functions, `operator!=` is not `#!cpp noexcept`.
+
+## Complexity
+
+Linear, as [`operator==`](operator_eq.md#complexity).
+
+## Notes
+
+See the [Notes](operator_eq.md#notes) of `operator==` -- in particular for how an object's members are compared
+(order matters for [`ordered_json_view`](../ordered_json_view.md) but not for [`json_view`](../json_view.md)) and
+for how discarded views compare.
+
+No ordering comparison (`#!cpp operator<`) is provided for `basic_json_view`; [`materialize()`](materialize.md) is
+the way to get a `BasicJsonType` value that supports it.
+
+## Examples
+
+??? example
+
+ The example below asserts, as a test would, that a received document differs from an unwanted value, and shows
+ that -- as for [`json`](../json.md)/[`ordered_json`](../ordered_json.md) -- reordering an object's members is
+ detected as a difference for an `ordered_json_view` but not for a `json_view`.
+
+ ```cpp
+ --8<-- "examples/basic_json_view__operator_ne.cpp"
+ ```
+
+ Output:
+
+ ```json
+ --8<-- "examples/basic_json_view__operator_ne.output"
+ ```
+
+## See also
+
+- [operator==](operator_eq.md) - compare for equality
+- [materialize](materialize.md) - build a `BasicJsonType` value, e.g. to keep comparing after the document is gone
+- [`BasicJsonType::operator!=`](../basic_json/operator_ne.md) - the corresponding function of `basic_json`
+
+## Version history
+
+- Added in version 3.13.0.
diff --git a/docs/mkdocs/docs/api/operator_ltlt.md b/docs/mkdocs/docs/api/operator_ltlt.md
index da53d7ee1..6a821b7fb 100644
--- a/docs/mkdocs/docs/api/operator_ltlt.md
+++ b/docs/mkdocs/docs/api/operator_ltlt.md
@@ -86,6 +86,8 @@ Linear.
## See also
- [dump](basic_json/dump.md) - serialize to a JSON-formatted string
+- [`basic_json_view::operator<<`](basic_json_view/operator_ltlt.md) - the corresponding operator for
+ `basic_json_view`
- [Serialization](../features/serialization.md) - the serialization article
## Version history
diff --git a/docs/mkdocs/docs/examples/basic_json_view__dump.cpp b/docs/mkdocs/docs/examples/basic_json_view__dump.cpp
new file mode 100644
index 000000000..c9bd8198f
--- /dev/null
+++ b/docs/mkdocs/docs/examples/basic_json_view__dump.cpp
@@ -0,0 +1,25 @@
+#include
+#include
+
+using json_document = nlohmann::json_document;
+using json_view = nlohmann::json_view;
+
+int main()
+{
+ // a large batch of sensor readings -- forward just the one that changed,
+ // without ever building a basic_json value for the batch or for the
+ // readings that are not needed
+ const json_document batch = json_document::parse(R"(
+ [{"id": 1, "temp": 21.5}, {"id": 2, "temp": 87.3}, {"id": 3, "temp": 21.7}]
+ )");
+ const json_view readings = batch.root();
+ std::cout << readings[1].dump() << '\n';
+
+ // a configuration file -- dump() on the view keeps the member order of
+ // the source text; a json value's object_t is std::map, so
+ // materialize().dump() of the very same view sorts the keys instead
+ const json_document config = json_document::parse(
+ R"({"name": "cache", "host": "db1", "port": 6379, "timeout": 30})");
+ std::cout << config.root().dump(2) << "\n\n";
+ std::cout << config.root().materialize().dump(2) << '\n';
+}
diff --git a/docs/mkdocs/docs/examples/basic_json_view__dump.output b/docs/mkdocs/docs/examples/basic_json_view__dump.output
new file mode 100644
index 000000000..e6e03522c
--- /dev/null
+++ b/docs/mkdocs/docs/examples/basic_json_view__dump.output
@@ -0,0 +1,14 @@
+{"id":2,"temp":87.3}
+{
+ "name": "cache",
+ "host": "db1",
+ "port": 6379,
+ "timeout": 30
+}
+
+{
+ "host": "db1",
+ "name": "cache",
+ "port": 6379,
+ "timeout": 30
+}
diff --git a/docs/mkdocs/docs/examples/basic_json_view__number_format.cpp b/docs/mkdocs/docs/examples/basic_json_view__number_format.cpp
new file mode 100644
index 000000000..f67ea4e84
--- /dev/null
+++ b/docs/mkdocs/docs/examples/basic_json_view__number_format.cpp
@@ -0,0 +1,28 @@
+#include
+#include
+
+using json_document = nlohmann::json_document;
+using json_view = nlohmann::json_view;
+
+int main()
+{
+ // a price list received from a supplier feed -- prices and account
+ // numbers must be forwarded exactly, e.g. into an invoice
+ const json_document doc = json_document::parse(R"(
+ [{"sku": "A1", "price": 19.90, "account_id": 12345678901234567890123456},
+ {"sku": "A2", "price": 1E2, "account_id": 98765432109876543210987654}]
+ )");
+ const json_view list = doc.root();
+
+ // number_format::shortest (the default) writes numbers the way
+ // basic_json::dump() would: "19.90" becomes "19.9", "1E2" becomes
+ // "100.0", and each account number -- far beyond any 64-bit integer --
+ // is rounded to the nearest double, exactly as materialize().dump()
+ // (or a plain nlohmann::json) would round it
+ std::cout << list.dump() << '\n';
+
+ // number_format::source copies every number exactly as it was written
+ // in the source text instead -- something basic_json cannot do at all,
+ // since parsing already reduces every number to its parsed value
+ std::cout << list.dump(-1, ' ', false, json_view::number_format::source) << '\n';
+}
diff --git a/docs/mkdocs/docs/examples/basic_json_view__number_format.output b/docs/mkdocs/docs/examples/basic_json_view__number_format.output
new file mode 100644
index 000000000..b4ab72d54
--- /dev/null
+++ b/docs/mkdocs/docs/examples/basic_json_view__number_format.output
@@ -0,0 +1,2 @@
+[{"sku":"A1","price":19.9,"account_id":1.2345678901234568e+25},{"sku":"A2","price":100.0,"account_id":9.876543210987655e+25}]
+[{"sku":"A1","price":19.90,"account_id":12345678901234567890123456},{"sku":"A2","price":1E2,"account_id":98765432109876543210987654}]
diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_eq.cpp b/docs/mkdocs/docs/examples/basic_json_view__operator_eq.cpp
new file mode 100644
index 000000000..846d40fc0
--- /dev/null
+++ b/docs/mkdocs/docs/examples/basic_json_view__operator_eq.cpp
@@ -0,0 +1,30 @@
+#include
+#include
+
+using json_document = nlohmann::json_document;
+using json = nlohmann::json;
+
+int main()
+{
+ // two snapshots of a polled configuration endpoint -- compare them
+ // directly as views, without ever building a nlohmann::json value for
+ // either one
+ const json_document previous = json_document::parse(
+ R"({"name": "cache", "port": 6379, "timeout": 30})");
+ const json_document current = json_document::parse(
+ R"({"port": 6379.0, "timeout": 30, "name": "cache"})");
+
+ // same members, reordered, and 6379 written as a float -- operator==
+ // treats them the same way BasicJsonType::operator== would
+ std::cout << std::boolalpha << (previous.root() == current.root()) << '\n';
+
+ // an actually changed value is detected the same way
+ const json_document changed = json_document::parse(
+ R"({"name": "cache", "port": 6380, "timeout": 30})");
+ std::cout << (previous.root() == changed.root()) << '\n';
+
+ // comparing a view directly against an expected json value -- handy in a
+ // test, without materializing the received document at all
+ const json expected = {{"name", "cache"}, {"port", 6379}, {"timeout", 30}};
+ std::cout << (previous.root() == expected) << '\n';
+}
diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_eq.output b/docs/mkdocs/docs/examples/basic_json_view__operator_eq.output
new file mode 100644
index 000000000..87f8d93a0
--- /dev/null
+++ b/docs/mkdocs/docs/examples/basic_json_view__operator_eq.output
@@ -0,0 +1,3 @@
+true
+false
+true
diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.cpp b/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.cpp
new file mode 100644
index 000000000..97100d536
--- /dev/null
+++ b/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.cpp
@@ -0,0 +1,26 @@
+#include
+#include
+#include
+
+using json_document = nlohmann::json_document;
+using json_view = nlohmann::json_view;
+
+int main()
+{
+ // one order out of a large incoming batch -- write it straight to a log
+ // stream without ever building a basic_json value for it, or for the
+ // rest of the batch
+ const json_document doc = json_document::parse(R"(
+ [{"id": 1, "item": "cable"}, {"id": 2, "item": "adapter"}]
+ )");
+ const json_view orders = doc.root();
+
+ // compact, for a one-line log entry
+ std::cout << orders[1] << '\n';
+
+ // std::setw sets the indentation level, exactly as for basic_json
+ std::cout << std::setw(2) << orders[1] << "\n\n";
+
+ // std::setfill changes the indentation character
+ std::cout << std::setw(1) << std::setfill('\t') << orders[1] << '\n';
+}
diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.output b/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.output
new file mode 100644
index 000000000..d791fc312
--- /dev/null
+++ b/docs/mkdocs/docs/examples/basic_json_view__operator_ltlt.output
@@ -0,0 +1,10 @@
+{"id":2,"item":"adapter"}
+{
+ "id": 2,
+ "item": "adapter"
+}
+
+{
+ "id": 2,
+ "item": "adapter"
+}
diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_ne.cpp b/docs/mkdocs/docs/examples/basic_json_view__operator_ne.cpp
new file mode 100644
index 000000000..faa1d61cf
--- /dev/null
+++ b/docs/mkdocs/docs/examples/basic_json_view__operator_ne.cpp
@@ -0,0 +1,28 @@
+#include
+#include
+
+using json_document = nlohmann::json_document;
+using ordered_json_document = nlohmann::ordered_json_document;
+using json = nlohmann::json;
+
+int main()
+{
+ // assert, as a test would, that a received document differs from an
+ // unwanted shape -- without ever materializing it into a json value just
+ // to compare
+ const json_document received = json_document::parse(
+ R"({"status": "ok", "code": 200})");
+ const json unwanted = {{"status", "error"}, {"code", 500}};
+ std::cout << std::boolalpha << (received.root() != unwanted) << '\n';
+
+ // json (std::map) compares object members regardless of order ...
+ const json_document a = json_document::parse(R"({"a": 1, "b": 2})");
+ const json_document b = json_document::parse(R"({"b": 2, "a": 1})");
+ std::cout << (a.root() != b.root()) << '\n';
+
+ // ... but ordered_json (ordered_map) compares them in the order they
+ // appear, so the very same reordering is detected as a difference
+ const ordered_json_document oa = ordered_json_document::parse(R"({"a": 1, "b": 2})");
+ const ordered_json_document ob = ordered_json_document::parse(R"({"b": 2, "a": 1})");
+ std::cout << (oa.root() != ob.root()) << '\n';
+}
diff --git a/docs/mkdocs/docs/examples/basic_json_view__operator_ne.output b/docs/mkdocs/docs/examples/basic_json_view__operator_ne.output
new file mode 100644
index 000000000..87f8d93a0
--- /dev/null
+++ b/docs/mkdocs/docs/examples/basic_json_view__operator_ne.output
@@ -0,0 +1,3 @@
+true
+false
+true
diff --git a/docs/mkdocs/docs/features/json_view.md b/docs/mkdocs/docs/features/json_view.md
index 7dbea9241..8030fe115 100644
--- a/docs/mkdocs/docs/features/json_view.md
+++ b/docs/mkdocs/docs/features/json_view.md
@@ -139,8 +139,12 @@ whenever any of the other conditions above was not met.
element access and lookup functions never carry the JSON Pointer path `JSON_DIAGNOSTICS` would otherwise add: the
view has no `basic_json` value to point at, so the exception is created without one, regardless of how
`BasicJsonType` was built.
-- **`dump()` and comparison are not (yet) provided** by `basic_json_view`. For now,
- [`materialize()`](../api/basic_json_view/materialize.md) is the way to get a value you can do those things with.
+- **Ordering comparisons are not provided** by `basic_json_view` -- there is no `#!cpp operator<`.
+ [`operator==`](../api/basic_json_view/operator_eq.md) and [`operator!=`](../api/basic_json_view/operator_ne.md) are
+ provided, though: two views, or a view and a `BasicJsonType` value, compare equal exactly when
+ [`materialize()`](../api/basic_json_view/materialize.md) or [`parse()`](../api/basic_json/parse.md) would produce
+ equal values for them, without ever building a tree to do it. For ordering, too,
+ [`materialize()`](../api/basic_json_view/materialize.md) is the way to get a value you can compare.
## Getting values out without copying
@@ -166,6 +170,23 @@ Two conversions never copy at all:
Both results are only valid as long as the view -- and, for a string with no escapes, the borrowed source text -- is.
+## Writing a view back
+
+[`dump()`](../api/basic_json_view/dump.md) serializes a view directly from the flat index, without ever building a
+`basic_json` value. An object's members are written in document order, not sorted by key, and *every* occurrence of a
+repeated key is written, not only the last one -- the same two ways [iteration](#what-is-different) already differs
+from a [`materialize()`](../api/basic_json_view/materialize.md)d value, see above. `#!cpp materialize().dump()` gives
+a different result in both respects for a `json_view`.
+
+By default, numbers are written the way [`basic_json::dump()`](../api/basic_json/dump.md) would.
+[`number_format::source`](../api/basic_json_view/number_format.md) instead copies every number exactly as it was
+written in the source text -- a price like `#!cpp 19.90`, a long order or account ID with more digits than any number
+type holds, or a high-precision coordinate -- something `basic_json` cannot do at all, since parsing already reduces
+a number to its parsed `#!cpp double`/`#!cpp int64_t` value.
+
+[`operator<<`](../api/basic_json_view/operator_ltlt.md) writes a view to a stream the way `basic_json`'s does, using
+the stream's `width`/`fill` for indentation.
+
## Choosing between `json`, `ordered_json`, the SAX interface, and `json_view`
| | [`json`](../api/json.md) / [`ordered_json`](../api/ordered_json.md) | [SAX interface](parsing/sax_interface.md) | [`json_document`](../api/json_document.md) / [`json_view`](../api/json_view.md) |
diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml
index 32da88c70..d5adb9476 100644
--- a/docs/mkdocs/mkdocs.yml
+++ b/docs/mkdocs/mkdocs.yml
@@ -257,6 +257,7 @@ nav:
- 'cend': api/basic_json_view/cend.md
- 'contains': api/basic_json_view/contains.md
- 'count': api/basic_json_view/count.md
+ - 'dump': api/basic_json_view/dump.md
- 'empty': api/basic_json_view/empty.md
- 'end': api/basic_json_view/end.md
- 'find': api/basic_json_view/find.md
@@ -279,9 +280,13 @@ nav:
- 'is_structured': api/basic_json_view/is_structured.md
- 'items': api/basic_json_view/items.md
- 'materialize': api/basic_json_view/materialize.md
+ - 'number_format': api/basic_json_view/number_format.md
- 'number_token': api/basic_json_view/number_token.md
- 'operator bool': api/basic_json_view/operator_bool.md
+ - 'operator<<': api/basic_json_view/operator_ltlt.md
- 'operator[]': api/basic_json_view/operator[].md
+ - 'operator==': api/basic_json_view/operator_eq.md
+ - 'operator!=': api/basic_json_view/operator_ne.md
- 'size': api/basic_json_view/size.md
- 'source_offset': api/basic_json_view/source_offset.md
- 'type': api/basic_json_view/type.md
diff --git a/include/nlohmann/detail/view/compare.hpp b/include/nlohmann/detail/view/compare.hpp
new file mode 100644
index 000000000..9a50590a3
--- /dev/null
+++ b/include/nlohmann/detail/view/compare.hpp
@@ -0,0 +1,317 @@
+// __ _____ _____ _____
+// __| | __| | | | JSON for Modern C++
+// | | |__ | | | | | | version 3.12.0
+// |_____|_____|_____|_|___| https://github.com/nlohmann/json
+//
+// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include // sort, stable_sort
+#include // size_t
+#include // string
+#include // move, pair
+#include // vector
+
+#include
+#include
+
+NLOHMANN_JSON_NAMESPACE_BEGIN
+namespace detail
+{
+namespace view
+{
+
+// Equality of views, and of views with basic_json values, with the semantics
+// of basic_json's operator== applied to the values parse() would produce:
+// numbers compare by value across their types, an object is compared by its
+// members with duplicate keys resolved as parse() resolves them (the last
+// value, at the position of the first occurrence), and in document order if
+// the object type keeps an order (ordered_json), by key otherwise.
+
+/// one side of a comparison: a view
+template
+class view_side
+{
+ public:
+ using string_view_t = typename View::string_view_t;
+
+ explicit view_side(const View& v) noexcept
+ : m_view(v)
+ {}
+
+ value_t type() const noexcept
+ {
+ return m_view.type();
+ }
+
+ std::size_t size() const noexcept
+ {
+ return m_view.size();
+ }
+
+ string_view_t string() const
+ {
+ return m_view.get_string();
+ }
+
+ /// a number, boolean, or null as a basic_json value (no allocation)
+ BasicJsonType scalar() const
+ {
+ switch (m_view.type())
+ {
+ case value_t::number_integer:
+ return BasicJsonType(m_view.template get());
+ case value_t::number_unsigned:
+ return BasicJsonType(m_view.template get());
+ case value_t::number_float:
+ return BasicJsonType(m_view.template get());
+ case value_t::boolean:
+ return BasicJsonType(m_view.template get());
+ case value_t::null:
+ case value_t::object:
+ case value_t::array:
+ case value_t::string:
+ case value_t::binary:
+ case value_t::discarded:
+ default:
+ return BasicJsonType(nullptr);
+ }
+ }
+
+ void elements(std::vector& out) const
+ {
+ out.reserve(m_view.size());
+ for (const View e : m_view)
+ {
+ out.emplace_back(e);
+ }
+ }
+
+ /// the members as parse() keeps them: one per key, the last value at the
+ /// position of the first occurrence; in that order, or sorted by key
+ void members(std::vector>& out, bool ordered) const
+ {
+ struct member
+ {
+ string_view_t key;
+ View value;
+ std::size_t position;
+ };
+ std::vector all;
+ all.reserve(m_view.size());
+ std::size_t position = 0;
+ for (auto it = m_view.begin(); it != m_view.end(); ++it)
+ {
+ all.push_back(member{it.key(), it.value(), position++});
+ }
+ std::stable_sort(all.begin(), all.end(), [](const member & a, const member & b)
+ {
+ return a.key < b.key;
+ });
+ std::vector unique;
+ unique.reserve(all.size());
+ for (std::size_t i = 0; i < all.size();)
+ {
+ std::size_t last = i;
+ while (last + 1 < all.size() && all[last + 1].key == all[i].key)
+ {
+ ++last;
+ }
+ unique.push_back(member{all[i].key, all[last].value, all[i].position});
+ i = last + 1;
+ }
+ if (ordered)
+ {
+ std::sort(unique.begin(), unique.end(), [](const member & a, const member & b)
+ {
+ return a.position < b.position;
+ });
+ }
+ out.reserve(unique.size());
+ for (const member& m : unique)
+ {
+ out.emplace_back(m.key, view_side(m.value));
+ }
+ }
+
+ private:
+ View m_view;
+};
+
+/// the other side of a comparison: a basic_json value
+template
+class json_side
+{
+ public:
+ using string_view_t = StringView;
+
+ explicit json_side(const BasicJsonType& j) noexcept
+ : m_json(&j)
+ {}
+
+ value_t type() const noexcept
+ {
+ return m_json->type();
+ }
+
+ std::size_t size() const noexcept
+ {
+ return m_json->size();
+ }
+
+ string_view_t string() const
+ {
+ const auto& s = m_json->template get_ref();
+ return string_view_t(s.data(), s.size());
+ }
+
+ BasicJsonType scalar() const
+ {
+ return *m_json;
+ }
+
+ void elements(std::vector& out) const
+ {
+ out.reserve(m_json->size());
+ for (const auto& e : *m_json)
+ {
+ out.emplace_back(e);
+ }
+ }
+
+ void members(std::vector>& out, bool ordered) const
+ {
+ out.reserve(m_json->size());
+ for (auto it = m_json->cbegin(); it != m_json->cend(); ++it)
+ {
+ out.emplace_back(string_view_t(it.key().data(), it.key().size()), json_side(it.value()));
+ }
+ if (!ordered)
+ {
+ std::sort(out.begin(), out.end(), [](const std::pair& a, const std::pair& b)
+ {
+ return a.first < b.first;
+ });
+ }
+ }
+
+ private:
+ const BasicJsonType* m_json;
+};
+
+/// whether two sides are equal; iterative, so that the nesting depth is
+/// limited by memory only
+template
+bool equal(const A& a0, const B& b0)
+{
+ using string_view_t = typename A::string_view_t;
+ const bool ordered = is_ordered_map::value;
+
+ struct frame
+ {
+ std::vector elements_a{};
+ std::vector elements_b{};
+ std::vector> members_a{};
+ std::vector> members_b{};
+ bool object = false;
+ std::size_t next = 0;
+ };
+ std::vector stack;
+ A a = a0;
+ B b = b0;
+ for (;;)
+ {
+ const value_t ta = a.type();
+ const value_t tb = b.type();
+ const bool numbers = (ta == value_t::number_integer || ta == value_t::number_unsigned || ta == value_t::number_float)
+ && (tb == value_t::number_integer || tb == value_t::number_unsigned || tb == value_t::number_float);
+ if (ta == value_t::discarded || tb == value_t::discarded)
+ {
+ // basic_json decides (JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON)
+ if (ta != tb || !(BasicJsonType(value_t::discarded) == BasicJsonType(value_t::discarded)))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (!numbers && ta != tb)
+ {
+ return false;
+ }
+ if (ta == value_t::string)
+ {
+ if (!(a.string() == b.string()))
+ {
+ return false;
+ }
+ }
+ else if (ta == value_t::array || ta == value_t::object)
+ {
+ if (a.size() != b.size() && ta == value_t::array)
+ {
+ return false;
+ }
+ frame f;
+ f.object = ta == value_t::object;
+ if (f.object)
+ {
+ a.members(f.members_a, ordered);
+ b.members(f.members_b, ordered);
+ if (f.members_a.size() != f.members_b.size())
+ {
+ return false;
+ }
+ }
+ else
+ {
+ a.elements(f.elements_a);
+ b.elements(f.elements_b);
+ }
+ stack.push_back(std::move(f));
+ }
+ else if (!(a.scalar() == b.scalar())) // numbers (also of different types), null, boolean
+ {
+ return false;
+ }
+ }
+
+ // the next pair of values
+ for (;;)
+ {
+ if (stack.empty())
+ {
+ return true;
+ }
+ frame& f = stack.back();
+ const std::size_t count = f.object ? f.members_a.size() : f.elements_a.size();
+ if (f.next == count)
+ {
+ stack.pop_back();
+ continue;
+ }
+ if (f.object)
+ {
+ if (!(f.members_a[f.next].first == f.members_b[f.next].first))
+ {
+ return false;
+ }
+ a = f.members_a[f.next].second;
+ b = f.members_b[f.next].second;
+ }
+ else
+ {
+ a = f.elements_a[f.next];
+ b = f.elements_b[f.next];
+ }
+ ++f.next;
+ break;
+ }
+ }
+}
+
+} // namespace view
+} // namespace detail
+NLOHMANN_JSON_NAMESPACE_END
diff --git a/include/nlohmann/detail/view/materialize.hpp b/include/nlohmann/detail/view/materialize.hpp
index e9ffe0668..79fafee1b 100644
--- a/include/nlohmann/detail/view/materialize.hpp
+++ b/include/nlohmann/detail/view/materialize.hpp
@@ -81,7 +81,7 @@ BasicJsonType materialize(const document_data& d, const node* n)
++n;
break;
case value_t::number_float:
- sax.number_float(float_value(d.str(*n), *n), no_token);
+ sax.number_float(float_value(d, *n), no_token);
++n;
break;
case value_t::boolean:
diff --git a/include/nlohmann/detail/view/number.hpp b/include/nlohmann/detail/view/number.hpp
index c9befdd49..24a87a7b8 100644
--- a/include/nlohmann/detail/view/number.hpp
+++ b/include/nlohmann/detail/view/number.hpp
@@ -9,11 +9,15 @@
#pragma once
#include // size_t
+#include // int64_t, uint64_t
#include // string
+#include // integral_constant
#include
+#include
#include
#include
+#include
NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
@@ -62,6 +66,96 @@ NLOHMANN_VIEW_NOINLINE FloatType float_value(const char* first, const node& n)
return convert_float(first, last, dot, mantissa_end);
}
+/*!
+@brief the digits of a float token with at most 19 digits, from its layout
+
+The digit layout recorded while parsing says where the integer digits, the
+fraction digits, and the exponent are, so the digits are read eight at a
+time without scanning.
+
+@param[in] p first character of the token
+@param[in] e end of the token
+@param[in] limit end of the readable memory (the source text)
+*/
+NLOHMANN_VIEW_ALWAYS_INLINE float_significand layout_decimal(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept
+{
+ const bool negative = *p == '-';
+ p += negative ? 1 : 0;
+ std::uint64_t w = parse_upto19(p, int_digits, limit);
+ p += int_digits;
+ std::int64_t q = 0;
+ if (frac_digits != 0)
+ {
+ w = (w * int_pow10(frac_digits)) + parse_upto19(p + 1, frac_digits, limit);
+ p += 1 + frac_digits;
+ q = -static_cast(frac_digits);
+ }
+ if (p != e)
+ {
+ // [eE][+-]digits; huge exponents saturate (the parser rejected overflow)
+ ++p;
+ const bool exp_negative = *p == '-';
+ p += (*p == '-' || *p == '+') ? 1 : 0;
+ std::int64_t exp_value = 0;
+ for (; p != e; ++p)
+ {
+ if (exp_value < 0x10000000)
+ {
+ exp_value = (exp_value * 10) + (*p - '0');
+ }
+ }
+ q += exp_negative ? -exp_value : exp_value;
+ }
+
+ float_significand d;
+ d.w = w;
+ d.exponent = q;
+ d.negative = negative;
+ return d;
+}
+
+/*!
+@brief the value of a float token with at most 19 digits, from its layout
+
+The result is correctly rounded by the lexer's conversion
+(detail::decimal_to_float(): Clinger's fast path where both operands are
+exact, else the Eisel-Lemire algorithm, which needs no fallback for up to 19
+digits), so it is the value parse() produces.
+*/
+template
+NLOHMANN_VIEW_ALWAYS_INLINE FloatType layout_float(const unsigned char* p, const unsigned char* e, unsigned int_digits, unsigned frac_digits, const unsigned char* limit) noexcept
+{
+ return decimal_to_float(layout_decimal(p, e, int_digits, frac_digits, limit));
+}
+
+/// the value of the float token of a node, as parse() converts it; floats and
+/// doubles with at most 19 digits are converted from the digit layout
+template
+FloatType float_value(const document_data& d, const node& n)
+{
+ return float_value(d, n, std::integral_constant::value> {});
+}
+
+template
+FloatType float_value(const document_data& d, const node& n, std::true_type /*binary32 or binary64*/)
+{
+ const unsigned int_digits = n.extra & 0xFFu;
+ const unsigned frac_digits = n.extra >> 8u;
+ if (NLOHMANN_VIEW_LIKELY(int_digits + frac_digits <= 19)) // (255 marks "many")
+ {
+ // (a float token not written by an edit is in the text)
+ const auto* const first = reinterpret_cast(d.src + n.off); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
+ return layout_float(first, first + n.len, int_digits, frac_digits, reinterpret_cast(d.src + d.size)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
+ }
+ return float_value(d.str(n), n);
+}
+
+template
+FloatType float_value(const document_data& d, const node& n, std::false_type /*other*/)
+{
+ return float_value(d.str(n), n);
+}
+
} // namespace view
} // namespace detail
NLOHMANN_JSON_NAMESPACE_END
diff --git a/include/nlohmann/detail/view/serializer.hpp b/include/nlohmann/detail/view/serializer.hpp
new file mode 100644
index 000000000..26bfb3225
--- /dev/null
+++ b/include/nlohmann/detail/view/serializer.hpp
@@ -0,0 +1,419 @@
+// __ _____ _____ _____
+// __| | __| | | | JSON for Modern C++
+// | | |__ | | | | | | version 3.12.0
+// |_____|_____|_____|_|___| https://github.com/nlohmann/json
+//
+// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include // max
+#include // array
+#include // isfinite
+#include // size_t
+#include // uint8_t, uint32_t
+#include // memcpy, memset
+#include // numeric_limits
+#include // integral_constant
+#include // vector
+
+#include
+#include
+#include
+#include
+#include
+
+NLOHMANN_JSON_NAMESPACE_BEGIN
+namespace detail
+{
+namespace view
+{
+
+/// append-only output buffer: writes through a raw pointer into a string that
+/// is resized ahead, and trimmed by finish()
+template
+class output_buffer
+{
+ public:
+ output_buffer(StringType& out, std::size_t estimate)
+ : m_out(sized(out, estimate))
+ , m_pos(&m_out[0])
+ , m_end(m_pos + m_out.size())
+ {}
+
+ void finish()
+ {
+ m_out.resize(static_cast(m_pos - m_out.data()));
+ }
+
+ NLOHMANN_VIEW_ALWAYS_INLINE void reserve(std::size_t n)
+ {
+ if (NLOHMANN_VIEW_UNLIKELY(static_cast(m_end - m_pos) < n))
+ {
+ grow(n);
+ }
+ }
+
+ NLOHMANN_VIEW_ALWAYS_INLINE void put(char c)
+ {
+ reserve(1);
+ *m_pos++ = c;
+ }
+
+ NLOHMANN_VIEW_ALWAYS_INLINE void put(const char* s, std::size_t n)
+ {
+ reserve(n);
+ std::memcpy(m_pos, s, n);
+ m_pos += n;
+ }
+
+ void put_repeated(char c, std::size_t n)
+ {
+ reserve(n);
+ std::memset(m_pos, c, n);
+ m_pos += n;
+ }
+
+ private:
+ static StringType& sized(StringType& out, std::size_t estimate)
+ {
+ out.resize((std::max)(estimate, static_cast(64)));
+ return out;
+ }
+
+ NLOHMANN_VIEW_NOINLINE void grow(std::size_t n)
+ {
+ const auto used = static_cast(m_pos - m_out.data());
+ m_out.resize((std::max)(m_out.size() * 2, used + n + 256));
+ m_pos = &m_out[0] + used;
+ m_end = &m_out[0] + m_out.size();
+ }
+
+ StringType& m_out;
+ char* m_pos;
+ char* m_end;
+};
+
+/// how the view's dump() writes a value
+struct dump_style
+{
+ bool pretty = false; ///< indent >= 0
+ std::size_t indent = 0; ///< characters per level
+ char indent_char = ' ';
+ bool ensure_ascii = false;
+ bool source_numbers = false; ///< copy number tokens from the source
+};
+
+/*!
+@brief write a view's subtree as basic_json::dump() writes the value
+
+The output of a subtree equals ordered_json::parse(text).dump() of it for
+the same arguments (members in document order): strings are escaped by the
+same rules, with the library's scanning kernels; floats are written with
+the library's conversion; integers are copied from the source, where they
+are canonical (except "-0", which parse() reads as 0). The walk is
+iterative, so the nesting depth is limited by memory only.
+*/
+template
+class view_serializer
+{
+ using string_t = typename BasicJsonType::string_t;
+ using number_float_t = typename BasicJsonType::number_float_t;
+
+ public:
+ view_serializer(const document_data& d, string_t& out, std::size_t estimate, const dump_style& style)
+ : m_doc(d), m_out(out, estimate), m_style(style)
+ {}
+
+ void dump(const node* root)
+ {
+ struct frame
+ {
+ const node* pos; ///< next element, or key of the next member
+ const node* end;
+ bool object;
+ bool first; ///< nothing written yet
+ };
+ std::vector stack;
+ const node* n = root;
+ for (;;)
+ {
+ // write the value at n
+ if (is_container(*n))
+ {
+ const bool object = n->kind == static_cast(value_t::object);
+ if (n->len == 0)
+ {
+ m_out.put(object ? "{}" : "[]", 2);
+ }
+ else
+ {
+ m_out.put(object ? '{' : '[');
+ stack.push_back(frame{document_data::first_child(n), document_data::child_end(n), object, true});
+ }
+ }
+ else
+ {
+ write_scalar(*n);
+ }
+
+ // go to the next value: close finished containers, then separate
+ for (;;)
+ {
+ if (stack.empty())
+ {
+ m_out.finish();
+ return;
+ }
+ frame& f = stack.back();
+ if (f.pos == f.end)
+ {
+ const bool object = f.object;
+ stack.pop_back();
+ newline(stack.size());
+ m_out.put(object ? '}' : ']');
+ continue;
+ }
+ if (!f.first)
+ {
+ m_out.put(',');
+ }
+ f.first = false;
+ newline(stack.size());
+ if (f.object)
+ {
+ write_string(*f.pos);
+ if (m_style.pretty)
+ {
+ m_out.put(": ", 2);
+ }
+ else
+ {
+ m_out.put(':');
+ }
+ n = f.pos + 1;
+ }
+ else
+ {
+ n = f.pos;
+ }
+ f.pos = document_data::after(n);
+ break;
+ }
+ }
+ }
+
+ private:
+ void newline(std::size_t level)
+ {
+ if (m_style.pretty)
+ {
+ m_out.put('\n');
+ m_out.put_repeated(m_style.indent_char, level * m_style.indent);
+ }
+ }
+
+ void write_scalar(const node& n)
+ {
+ switch (static_cast(n.kind))
+ {
+ case value_t::null:
+ m_out.put("null", 4);
+ break;
+ case value_t::boolean:
+ if ((n.flags & node_flags::is_true) != 0)
+ {
+ m_out.put("true", 4);
+ }
+ else
+ {
+ m_out.put("false", 5);
+ }
+ break;
+ case value_t::string:
+ write_string(n);
+ break;
+ case value_t::number_integer:
+ case value_t::number_unsigned:
+ {
+ const char* const token = m_doc.str(n);
+ const std::uint32_t len = number_length(n);
+ if (!m_style.source_numbers && len == 2 && token[0] == '-' && token[1] == '0')
+ {
+ m_out.put('0'); // parse() reads -0 as the integer 0
+ }
+ else
+ {
+ m_out.put(token, len);
+ }
+ break;
+ }
+ case value_t::number_float:
+ if (m_style.source_numbers)
+ {
+ m_out.put(m_doc.str(n), n.len);
+ }
+ else
+ {
+ write_float(float_value(m_doc, n));
+ }
+ break;
+ case value_t::object: // LCOV_EXCL_LINE (containers are written by dump())
+ case value_t::array: // LCOV_EXCL_LINE
+ case value_t::binary: // LCOV_EXCL_LINE (not in a document)
+ case value_t::discarded: // LCOV_EXCL_LINE
+ default: // LCOV_EXCL_LINE
+ break; // LCOV_EXCL_LINE
+ }
+ }
+
+ /// as serializer::dump_float()
+ void write_float(number_float_t x)
+ {
+ if (!std::isfinite(x))
+ {
+ m_out.put("null", 4);
+ return;
+ }
+ write_float(x, std::integral_constant < bool,
+ (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 24 && std::numeric_limits::max_exponent == 128)
+ || (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 53 && std::numeric_limits::max_exponent == 1024) > {});
+ }
+
+ void write_float(number_float_t x, std::true_type /*is_ieee_single_or_double*/)
+ {
+ std::array buf{};
+ const char* const end = ::nlohmann::detail::to_chars(buf.data(), buf.data() + buf.size(), x);
+ m_out.put(buf.data(), static_cast(end - buf.data()));
+ }
+
+ void write_float(number_float_t x, std::false_type /*is_ieee_single_or_double*/)
+ {
+ // other types (e.g. long double) are rare: the library writes them
+ const string_t s = BasicJsonType(x).dump();
+ m_out.put(s.data(), s.size());
+ }
+
+ void write_string(const node& n)
+ {
+ const char* const s = m_doc.str(n);
+ m_out.put('"');
+ if ((n.flags & node_flags::escaped) == 0 && !m_style.ensure_ascii)
+ {
+ // a string without escape sequences has nothing to escape
+ m_out.put(s, n.len);
+ }
+ else if (m_style.ensure_ascii)
+ {
+ write_escaped(reinterpret_cast(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
+ }
+ else
+ {
+ write_escaped(reinterpret_cast(s), n.len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
+ }
+ m_out.put('"');
+ }
+
+ /// as serializer::dump_escaped() for valid UTF-8 (the view has no other)
+ template
+ void write_escaped(const unsigned char* s, std::size_t n)
+ {
+ std::size_t i = 0;
+ while (i < n)
+ {
+ std::size_t run = 0;
+ if (!EnsureAscii)
+ {
+ run = string_bulk_run(s + i, n - i);
+ }
+ else if (is_ascii_copyable(s[i]))
+ {
+ run = find_ascii_copyable_run(s + i, n - i);
+ }
+ if (run != 0)
+ {
+ m_out.put(reinterpret_cast(s + i), run); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
+ i += run;
+ continue;
+ }
+ std::uint32_t codepoint = s[i];
+ std::size_t len = 1;
+ if (codepoint >= 0xC0)
+ {
+ len = 2;
+ if (codepoint >= 0xE0)
+ {
+ len = codepoint >= 0xF0 ? 4 : 3;
+ }
+ codepoint &= 0xFFu >> (len + 1);
+ for (std::size_t k = 1; k < len; ++k)
+ {
+ codepoint = (codepoint << 6u) | (s[i + k] & 0x3Fu);
+ }
+ }
+ write_codepoint(codepoint, s + i, len);
+ i += len;
+ }
+ }
+
+ template
+ void write_codepoint(std::uint32_t codepoint, const unsigned char* bytes, std::size_t len)
+ {
+ switch (codepoint)
+ {
+ case 0x08:
+ m_out.put("\\b", 2);
+ return;
+ case 0x09:
+ m_out.put("\\t", 2);
+ return;
+ case 0x0A:
+ m_out.put("\\n", 2);
+ return;
+ case 0x0C:
+ m_out.put("\\f", 2);
+ return;
+ case 0x0D:
+ m_out.put("\\r", 2);
+ return;
+ case 0x22:
+ m_out.put("\\\"", 2);
+ return;
+ case 0x5C:
+ m_out.put("\\\\", 2);
+ return;
+ default:
+ break;
+ }
+ if (codepoint <= 0x1F || (EnsureAscii && codepoint >= 0x7F))
+ {
+ if (codepoint <= 0xFFFF)
+ {
+ write_u_escape(codepoint);
+ }
+ else
+ {
+ write_u_escape(0xD7C0u + (codepoint >> 10u));
+ write_u_escape(0xDC00u + (codepoint & 0x3FFu));
+ }
+ return;
+ }
+ m_out.put(reinterpret_cast(bytes), len); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) LCOV_EXCL_LINE (printable characters are copied in runs)
+ }
+
+ void write_u_escape(std::uint32_t u)
+ {
+ static constexpr const char* hex = "0123456789abcdef";
+ const std::array e = {{'\\', 'u', hex[(u >> 12u) & 0xFu], hex[(u >> 8u) & 0xFu], hex[(u >> 4u) & 0xFu], hex[u & 0xFu]}};
+ m_out.put(e.data(), e.size());
+ }
+
+ const document_data& m_doc;
+ output_buffer m_out;
+ const dump_style m_style;
+};
+
+} // namespace view
+} // namespace detail
+NLOHMANN_JSON_NAMESPACE_END
diff --git a/include/nlohmann/detail/view/value.hpp b/include/nlohmann/detail/view/value.hpp
index b43a9c4df..23d291634 100644
--- a/include/nlohmann/detail/view/value.hpp
+++ b/include/nlohmann/detail/view/value.hpp
@@ -49,7 +49,7 @@ NLOHMANN_VIEW_ALWAYS_INLINE T arithmetic_value(const document_data& d, const nod
case value_t::number_integer:
return static_cast(static_cast(static_cast(integer_bits(n))));
case value_t::number_float:
- return static_cast(float_value(d.str(n), n));
+ return static_cast(float_value(d, n));
case value_t::boolean:
return static_cast((n.flags & node_flags::is_true) != 0);
case value_t::null:
diff --git a/include/nlohmann/json_view.hpp b/include/nlohmann/json_view.hpp
index b51ff446b..882101336 100644
--- a/include/nlohmann/json_view.hpp
+++ b/include/nlohmann/json_view.hpp
@@ -29,6 +29,9 @@
#include // distance, input_iterator_tag, iterator_traits
#include