Compare commits

..

6 Commits

Author SHA1 Message Date
Niels Lohmann 8861d3ec5a 🎓 fix warning
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-07-09 11:45:42 +02:00
Niels Lohmann d8b274e7c2 Fix #3868: Remove operator!= to enable P2468R2 rewritten candidate synthesis
Under C++20 P2468R2, a hand-written operator!= suppresses the compiler's
rewritten-candidate synthesis for operator==, preventing heterogeneous
comparisons like `std::string s; json j; s == j;` from compiling.

Fix by removing the hand-written operator!=, allowing the compiler to
synthesize != as !(a==b) in all language modes (C++20 member functions
and pre-C++20 friend functions).

Behavior change: operator!= now returns !(a==b) unconditionally, including
for special values like NaN and discarded. This means:
- NaN != NaN now returns true (matches IEEE-754 semantics)
- discarded != x now returns true for any x (matches !(discarded == x))

This also fixes underlying defects in previously-working code:
- Restores direct == comparison for views vs json (reverts std::ranges::equal
  workaround added in PR #3950 to dodge this bug)
- Re-enables std::string == json comparisons (uncomments check in
  unit-constructor1.cpp)

Fixes: #3868, #3979

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-07-09 10:09:36 +02:00
Niels Lohmann fe0299545a 📡 Document cross-basic_json conversion limitation (#3425) (#5249)
When converting objects or strings between different basic_json specializations,
the target's object_t::key_type or string_t must be directly constructible from
the source's corresponding type. If this requirement is not met, the conversion
silently falls back to the array-conversion path, producing incorrect results.

This documents the limitation and provides references to issue #3425, which tracks
this behavior. The comment in unit-alt-string.cpp is clarified to reference the
known limitation with a link to the issue, and suggests the parse() workaround.

Fixes #3425 (documentation; full fix deferred pending type-trait redesign)

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-07-09 06:44:51 +02:00
Niels Lohmann 366f3d26e5 Replace snprintf with a branch-free writer for \uXXXX escapes (#5235)
* Replace snprintf with a branch-free writer for \uXXXX escapes

dump_escaped called std::snprintf(..., "\u%04x", ...) once per escaped
code point in the string serialization hot path. snprintf re-parses
the format string and pulls in locale/printf machinery on every call,
which is far heavier than the fixed 6-/12-byte output warrants. This
is hot for any string containing control characters, and for all
non-ASCII text when ensure_ascii is set.

Replace it with write_u_escape, a small helper that writes the escape
directly into string_buffer via a nibble-to-hex lookup table, mirroring
the existing hand-rolled dump_integer fast path in the same file.

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

* Fix clang-tidy avoid-c-arrays warning in write_u_escape

Use a const char* rather than a char[] lookup table, matching the
existing hex_bytes helper in the same file.

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

* ♻️ adjust write_u_escape signature

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-07-08 20:25:13 +02:00
Niels Lohmann 7c9208bfb3 📡 make documentation more LLM friendly (#5244)
Implement the scoped agent-readiness subset for json.nlohmann.me:
- Add the mkdocs-llmstxt plugin to generate llms.txt from the nav
  (full_output/llms-full.txt deliberately omitted to avoid dumping
  500+ API reference pages into one giant file).
- Add a permissive robots.txt with a Sitemap reference.
- Add a build hook (hooks/copy_markdown_source.py) that copies each
  page's Markdown source into the built site as a `<path>.md` sibling
  of its HTML output, so agents/tools can fetch raw Markdown directly.

sitemap.xml was already emitted by default and needed no change.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-07-08 20:18:24 +02:00
Niels Lohmann bb60941f0e 👪 fix security findings (#5245)
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-07-08 16:40:58 +02:00
21 changed files with 246 additions and 118 deletions
+12
View File
@@ -4,6 +4,8 @@ updates:
directory: / directory: /
schedule: schedule:
interval: daily interval: daily
cooldown:
default-days: 7
groups: groups:
codeql-action: codeql-action:
patterns: patterns:
@@ -13,23 +15,33 @@ updates:
directory: /docs/mkdocs directory: /docs/mkdocs
schedule: schedule:
interval: daily interval: daily
cooldown:
default-days: 7
- package-ecosystem: pip - package-ecosystem: pip
directory: /tools/astyle directory: /tools/astyle
schedule: schedule:
interval: daily interval: daily
cooldown:
default-days: 7
- package-ecosystem: pip - package-ecosystem: pip
directory: /tools/generate_natvis directory: /tools/generate_natvis
schedule: schedule:
interval: daily interval: daily
cooldown:
default-days: 7
- package-ecosystem: pip - package-ecosystem: pip
directory: /tools/serve_header directory: /tools/serve_header
schedule: schedule:
interval: daily interval: daily
cooldown:
default-days: 7
- package-ecosystem: pip - package-ecosystem: pip
directory: /cmake/requirements directory: /cmake/requirements
schedule: schedule:
interval: daily interval: daily
cooldown:
default-days: 7
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
# SEMGREP_APP_TOKEN is still passed through so registry auth works if a # SEMGREP_APP_TOKEN is still passed through so registry auth works if a
# token is ever added. # token is ever added.
- name: Install Semgrep - name: Install Semgrep
run: python3 -m pip install --user semgrep run: python3 -m pip install --user semgrep==1.168.0
# `semgrep scan --sarif` always exits 0 even with findings; continue-on-error # `semgrep scan --sarif` always exits 0 even with findings; continue-on-error
# is a safety net so the SARIF upload still runs if the scan itself errors. # is a safety net so the SARIF upload still runs if the scan itself errors.
+12 -1
View File
@@ -82,7 +82,13 @@ basic_json(basic_json&& other) noexcept;
4. This is a constructor for existing `basic_json` types. It does not hijack copy/move constructors, since the parameter 4. This is a constructor for existing `basic_json` types. It does not hijack copy/move constructors, since the parameter
has different template arguments than the current ones. has different template arguments than the current ones.
The constructor tries to convert the internal `m_value` of the parameter. The constructor tries to convert the internal `m_value` of the parameter. Each member value (object, array, string,
etc.) is serialized via the corresponding `to_json()` overload. For objects and strings, the conversion requires
that the *target* `basic_json` type's `object_t::key_type` (or `string_t`) be directly constructible from the
*source* type's corresponding member type via `is_constructible`. If this requirement is not met, the conversion
does not fail to compile; instead, it silently falls back to the array-conversion path, which represents objects
as arrays of `[key, value]` pairs and strings as arrays of character codes. This is a known limitation tracked in
[issue #3425](https://github.com/nlohmann/json/issues/3425).
5. Creates a JSON value of type array or object from the passed initializer list `init`. In case `type_deduction` is 5. Creates a JSON value of type array or object from the passed initializer list `init`. In case `type_deduction` is
`#!cpp true` (default), the type of the JSON value to be created is deducted from the initializer list `init` `#!cpp true` (default), the type of the JSON value to be created is deducted from the initializer list `init`
@@ -147,6 +153,11 @@ basic_json(basic_json&& other) noexcept;
- `BasicJsonType` is a `basic_json` type. - `BasicJsonType` is a `basic_json` type.
- `BasicJsonType` has different template arguments than `basic_json_t`. - `BasicJsonType` has different template arguments than `basic_json_t`.
**Note:** For cross-`basic_json` conversions to produce correct results, the target `basic_json`'s
`object_t::key_type` and `string_t` must be directly constructible from the source `basic_json`'s
corresponding types. See the description of overload (4) above for details on what happens when
this requirement is not met.
`U`: `U`:
: `uncvref_t<CompatibleType>` : `uncvref_t<CompatibleType>`
@@ -93,6 +93,15 @@ alphabetical order as `std::map` with `std::less` is used by default. Please not
[RFC 8259](https://tools.ietf.org/html/rfc8259), because any order implements the specified "unordered" nature of JSON [RFC 8259](https://tools.ietf.org/html/rfc8259), because any order implements the specified "unordered" nature of JSON
objects. objects.
#### Cross-`basic_json` conversion requirements
When converting an object from one `basic_json` specialization to another via the
[converting constructor](basic_json.md#overload-4), the target `object_t`'s `key_type` must be
directly constructible from the source `basic_json`'s `string_t` type (or more generally, from the
source object's key type). If this requirement is not met, the conversion does not fail; instead,
the object is silently converted as an array of key-value pairs, which is incorrect. See
[issue #3425](https://github.com/nlohmann/json/issues/3425) for details and an example.
## Examples ## Examples
??? example ??? example
+11 -12
View File
@@ -19,10 +19,8 @@ class basic_json {
}; };
``` ```
1. Compares two JSON values for inequality according to the following rules: 1. Compares two JSON values for inequality. Returns `#!cpp !(lhs == rhs)` (until C++20) or `#!cpp !(*this == rhs)` (since C++20).
- The comparison always yields `#!cpp false` if (1) either operand is discarded, or (2) either operand is `NaN` and - This means the comparison is simply the logical negation of `operator==`, including for special values like `NaN` and `discarded`.
the other operand is either `NaN` or any other number.
- Otherwise, returns the result of `#!cpp !(lhs == rhs)` (until C++20) or `#!cpp !(*this == rhs)` (since C++20).
2. Compares a JSON value and a scalar or a scalar and a JSON value for inequality by converting the scalar to a JSON 2. Compares a JSON value and a scalar or a scalar and a JSON value for inequality by converting the scalar to a JSON
value and comparing both JSON values according to 1. value and comparing both JSON values according to 1.
@@ -54,13 +52,12 @@ Linear.
## Notes ## Notes
!!! note "Comparing `NaN`" !!! note "Comparing `NaN` and `discarded`"
`NaN` values are unordered within the domain of numbers. Since `operator!=` is defined as `!(a == b)`, the behavior for special values follows that of `operator==`:
The following comparisons all yield `#!cpp false`:
1. Comparing a `NaN` with itself. - For `NaN` values: `NaN == NaN` yields `#!cpp false`, so `NaN != NaN` yields `#!cpp true`.
2. Comparing a `NaN` with another `NaN`. - For `discarded` values: `discarded == x` yields `#!cpp false` for any `x`, so `discarded != x` yields `#!cpp true`.
3. Comparing a `NaN` and any other number.
## Examples ## Examples
@@ -94,5 +91,7 @@ Linear.
## Version history ## Version history
1. Added in version 1.0.0. Added C++20 member functions in version 3.11.0. 1. Added in version 1.0.0. Added C++20 member functions in version 3.11.0. Changed in version 3.12.x to remove
2. Added in version 1.0.0. Added C++20 member functions in version 3.11.0. special-casing for `NaN` and `discarded` values; `operator!=` now consistently means `!(a == b)`.
2. Added in version 1.0.0. Added C++20 member functions in version 3.11.0. Changed in version 3.12.x to remove
special-casing for `NaN` and `discarded` values; `operator!=` now consistently means `!(a == b)`.
@@ -45,6 +45,15 @@ This implementation is interoperable as it does compare strings code unit by cod
String values are stored as pointers in a `basic_json` type. That is, for any access to string values, a pointer of type String values are stored as pointers in a `basic_json` type. That is, for any access to string values, a pointer of type
`string_t*` must be dereferenced. `string_t*` must be dereferenced.
#### Cross-`basic_json` conversion requirements
When converting a string value from one `basic_json` specialization to another via the
[converting constructor](basic_json.md#overload-4), the target `string_t` must be directly
constructible from the source `basic_json`'s `string_t` type. If this requirement is not met, the
conversion does not fail; instead, the string is silently converted as an array of character codes,
which is incorrect. See [issue #3425](https://github.com/nlohmann/json/issues/3425) for details
and an example.
## Examples ## Examples
??? example ??? example
-18
View File
@@ -66,24 +66,6 @@ which forces the explicit `get` form and can catch unintended conversions at com
floating-point value as an integer truncates it, and narrowing conversions may overflow. See floating-point value as an integer truncates it, and narrowing conversions may overflow. See
[number conversion](types/number_handling.md#number-conversion) for details and how to guard against it. [number conversion](types/number_handling.md#number-conversion) for details and how to guard against it.
!!! warning "std::optional direct construction from JSON null throws"
Constructing or assigning `std::optional<T>` directly from a JSON value does not correctly produce
`std::nullopt` for a JSON `null`:
```cpp
json j_null;
std::optional<std::string> opt = j_null; // ❌ throws type_error 302
```
This is due to C++ language rules: `std::optional<T>` has its own converting constructor that is chosen over
`basic_json::operator T()` when both are viable. Use `get<std::optional<T>>()` or `get_to()` instead:
```cpp
auto opt = j_null.get<std::optional<std::string>>(); // ✅ std::nullopt
j_null.get_to(opt); // ✅ std::nullopt
```
## Putting values in ## Putting values in
The reverse direction works the same way: assigning or constructing a `json` from a C++ value converts it to JSON. The reverse direction works the same way: assigning or constructing a `json` from a C++ value converts it to JSON.
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://json.nlohmann.me/sitemap.xml
+25
View File
@@ -0,0 +1,25 @@
"""Copy each documentation page's Markdown source into the built site."""
# Creates a `<path>.md` sibling of each HTML output (for example,
# `features/comments/` becomes `features/comments.md`) so agents and tools can
# fetch the raw Markdown directly instead of parsing rendered HTML.
import os
import shutil
_pages = []
def on_files(files, config):
global _pages
_pages = [f for f in files if f.is_documentation_page()]
return files
def on_post_build(config):
site_dir = config["site_dir"]
for file in _pages:
url = file.url.rstrip("/")
target = os.path.join(site_dir, (url or "index") + ".md")
os.makedirs(os.path.dirname(target), exist_ok=True)
shutil.copyfile(file.abs_src_path, target)
+30
View File
@@ -367,6 +367,9 @@ markdown_extensions:
auto_append: auto_append:
- ../includes/glossary.md - ../includes/glossary.md
hooks:
- hooks/copy_markdown_source.py
plugins: plugins:
- search: - search:
separator: '[\s\-\.]' separator: '[\s\-\.]'
@@ -389,6 +392,33 @@ plugins:
- https://nlohmann.github.io/json/* - https://nlohmann.github.io/json/*
- mailto:* - mailto:*
- privacy - privacy
- llmstxt:
markdown_description: >
JSON for Modern C++ is a C++11 header-only library implementing a JSON
value type with an STL-like API, JSON Pointer/Patch, CBOR/MessagePack/
BSON/UBJSON/BJData binary format support, and a SAX-style parser interface.
sections:
Home:
- index.md
- home/*.md
Features:
- features/*.md
- features/binary_formats/*.md
- features/element_access/*.md
- features/parsing/*.md
- features/types/*.md
Integration:
- integration/*.md
API Documentation:
- api/*.md
- api/basic_json/*.md
- api/adl_serializer/*.md
- api/byte_container_with_subtype/*.md
- api/json_pointer/*.md
- api/json_sax/*.md
- api/macros/*.md
Community:
- community/*.md
extra_css: extra_css:
- css/custom.css - css/custom.css
+1
View File
@@ -7,5 +7,6 @@ mkdocs-material-extensions==1.3.1 # extensions
mkdocs-minify-plugin==0.8.0 # plugin "minify" mkdocs-minify-plugin==0.8.0 # plugin "minify"
mkdocs-redirects==1.2.3 # plugin "redirects" mkdocs-redirects==1.2.3 # plugin "redirects"
mkdocs-htmlproofer-plugin==1.5.0 # plugin "htmlproofer" mkdocs-htmlproofer-plugin==1.5.0 # plugin "htmlproofer"
mkdocs-llmstxt==0.5.0 # plugin "llmstxt"
PyYAML==6.0.3 # linter PyYAML==6.0.3 # linter
+29 -9
View File
@@ -465,18 +465,12 @@ class serializer
{ {
if (codepoint <= 0xFFFF) if (codepoint <= 0xFFFF)
{ {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) write_u_escape(bytes, static_cast<std::uint16_t>(codepoint));
static_cast<void>((std::snprintf)(string_buffer.data() + bytes, 7, "\\u%04x",
static_cast<std::uint16_t>(codepoint)));
bytes += 6;
} }
else else
{ {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) write_u_escape(bytes, static_cast<std::uint16_t>(0xD7C0u + (codepoint >> 10u)));
static_cast<void>((std::snprintf)(string_buffer.data() + bytes, 13, "\\u%04x\\u%04x", write_u_escape(bytes, static_cast<std::uint16_t>(0xDC00u + (codepoint & 0x3FFu)));
static_cast<std::uint16_t>(0xD7C0u + (codepoint >> 10u)),
static_cast<std::uint16_t>(0xDC00u + (codepoint & 0x3FFu))));
bytes += 12;
} }
} }
else else
@@ -683,6 +677,32 @@ class serializer
return result; return result;
} }
/*!
* @brief write a lowercase "\uXXXX" escape sequence into @a string_buffer
*
* Branch-free replacement for `snprintf(buf, 7, "\\u%04x", codeunit)` in the
* string escaping hot path. It writes exactly six characters ('\\', 'u' and
* four hex digits) at position @a pos of @a string_buffer via a nibble
* lookup table, avoiding the format-string parsing and locale machinery of
* `snprintf`. Advances @a pos by the number of bytes written (6).
*
* @param[in] pos position in @a string_buffer to write at; there must
* be at least 6 bytes of headroom
* @param[in] codeunit 16-bit value to encode
*/
void write_u_escape(std::size_t& pos, std::uint16_t codeunit) noexcept
{
JSON_ASSERT(string_buffer.size() - pos >= 6);
constexpr const char* nibble_to_hex = "0123456789abcdef";
string_buffer[pos + 0] = '\\';
string_buffer[pos + 1] = 'u';
string_buffer[pos + 2] = nibble_to_hex[(codeunit >> 12u) & 0x0Fu];
string_buffer[pos + 3] = nibble_to_hex[(codeunit >> 8u) & 0x0Fu];
string_buffer[pos + 4] = nibble_to_hex[(codeunit >> 4u) & 0x0Fu];
string_buffer[pos + 5] = nibble_to_hex[codeunit & 0x0Fu];
pos += 6;
}
// templates to avoid warnings about useless casts // templates to avoid warnings about useless casts
template <typename NumberType, enable_if_t<std::is_signed<NumberType>::value, int> = 0> template <typename NumberType, enable_if_t<std::is_signed<NumberType>::value, int> = 0>
bool is_negative_number(NumberType x) bool is_negative_number(NumberType x)
-15
View File
@@ -3776,17 +3776,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return *this == basic_json(rhs); return *this == basic_json(rhs);
} }
/// @brief comparison: not equal
/// @sa https://json.nlohmann.me/api/basic_json/operator_ne/
bool operator!=(const_reference rhs) const noexcept
{
if (compares_unordered(rhs, true))
{
return false;
}
return !operator==(rhs);
}
/// @brief comparison: 3-way /// @brief comparison: 3-way
/// @sa https://json.nlohmann.me/api/basic_json/operator_spaceship/ /// @sa https://json.nlohmann.me/api/basic_json/operator_spaceship/
std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD* std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD*
@@ -3892,10 +3881,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// @sa https://json.nlohmann.me/api/basic_json/operator_ne/ /// @sa https://json.nlohmann.me/api/basic_json/operator_ne/
friend bool operator!=(const_reference lhs, const_reference rhs) noexcept friend bool operator!=(const_reference lhs, const_reference rhs) noexcept
{ {
if (compares_unordered(lhs, rhs, true))
{
return false;
}
return !(lhs == rhs); return !(lhs == rhs);
} }
+29 -24
View File
@@ -19962,18 +19962,12 @@ class serializer
{ {
if (codepoint <= 0xFFFF) if (codepoint <= 0xFFFF)
{ {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) write_u_escape(bytes, static_cast<std::uint16_t>(codepoint));
static_cast<void>((std::snprintf)(string_buffer.data() + bytes, 7, "\\u%04x",
static_cast<std::uint16_t>(codepoint)));
bytes += 6;
} }
else else
{ {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) write_u_escape(bytes, static_cast<std::uint16_t>(0xD7C0u + (codepoint >> 10u)));
static_cast<void>((std::snprintf)(string_buffer.data() + bytes, 13, "\\u%04x\\u%04x", write_u_escape(bytes, static_cast<std::uint16_t>(0xDC00u + (codepoint & 0x3FFu)));
static_cast<std::uint16_t>(0xD7C0u + (codepoint >> 10u)),
static_cast<std::uint16_t>(0xDC00u + (codepoint & 0x3FFu))));
bytes += 12;
} }
} }
else else
@@ -20180,6 +20174,32 @@ class serializer
return result; return result;
} }
/*!
* @brief write a lowercase "\uXXXX" escape sequence into @a string_buffer
*
* Branch-free replacement for `snprintf(buf, 7, "\\u%04x", codeunit)` in the
* string escaping hot path. It writes exactly six characters ('\\', 'u' and
* four hex digits) at position @a pos of @a string_buffer via a nibble
* lookup table, avoiding the format-string parsing and locale machinery of
* `snprintf`. Advances @a pos by the number of bytes written (6).
*
* @param[in] pos position in @a string_buffer to write at; there must
* be at least 6 bytes of headroom
* @param[in] codeunit 16-bit value to encode
*/
void write_u_escape(std::size_t& pos, std::uint16_t codeunit) noexcept
{
JSON_ASSERT(string_buffer.size() - pos >= 6);
constexpr const char* nibble_to_hex = "0123456789abcdef";
string_buffer[pos + 0] = '\\';
string_buffer[pos + 1] = 'u';
string_buffer[pos + 2] = nibble_to_hex[(codeunit >> 12u) & 0x0Fu];
string_buffer[pos + 3] = nibble_to_hex[(codeunit >> 8u) & 0x0Fu];
string_buffer[pos + 4] = nibble_to_hex[(codeunit >> 4u) & 0x0Fu];
string_buffer[pos + 5] = nibble_to_hex[codeunit & 0x0Fu];
pos += 6;
}
// templates to avoid warnings about useless casts // templates to avoid warnings about useless casts
template <typename NumberType, enable_if_t<std::is_signed<NumberType>::value, int> = 0> template <typename NumberType, enable_if_t<std::is_signed<NumberType>::value, int> = 0>
bool is_negative_number(NumberType x) bool is_negative_number(NumberType x)
@@ -24602,17 +24622,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
return *this == basic_json(rhs); return *this == basic_json(rhs);
} }
/// @brief comparison: not equal
/// @sa https://json.nlohmann.me/api/basic_json/operator_ne/
bool operator!=(const_reference rhs) const noexcept
{
if (compares_unordered(rhs, true))
{
return false;
}
return !operator==(rhs);
}
/// @brief comparison: 3-way /// @brief comparison: 3-way
/// @sa https://json.nlohmann.me/api/basic_json/operator_spaceship/ /// @sa https://json.nlohmann.me/api/basic_json/operator_spaceship/
std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD* std::partial_ordering operator<=>(const_reference rhs) const noexcept // *NOPAD*
@@ -24718,10 +24727,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// @sa https://json.nlohmann.me/api/basic_json/operator_ne/ /// @sa https://json.nlohmann.me/api/basic_json/operator_ne/
friend bool operator!=(const_reference lhs, const_reference rhs) noexcept friend bool operator!=(const_reference lhs, const_reference rhs) noexcept
{ {
if (compares_unordered(lhs, rhs, true))
{
return false;
}
return !(lhs == rhs); return !(lhs == rhs);
} }
+3
View File
@@ -232,6 +232,9 @@ TEST_CASE("algorithms")
// only the first four elements are expected to be sorted, the rest are // only the first four elements are expected to be sorted, the rest are
// unspecified by the standard // unspecified by the standard
const json expected({nullptr, false, true, 3}); const json expected({nullptr, false, true, 3});
// std::equal below only bounds-checks the first range; assert the
// second range is at least as long to rule out an over-read (CWE-126)
CHECK(std::distance(begin(expected), end(expected)) >= 4);
CHECK(std::equal(j.begin(), j.begin() + 4, begin(expected))); CHECK(std::equal(j.begin(), j.begin() + 4, begin(expected)));
} }
} }
+6 -8
View File
@@ -322,14 +322,12 @@ TEST_CASE("alternative string type")
SECTION("JSON pointer") SECTION("JSON pointer")
{ {
// conversion from json to alt_json fails to compile (see #3425); // Direct conversion from a json literal to alt_json is not supported due to issue #3425:
// attempted fix(*) produces: [[['b','a','r'],['b','a','z']]] (with each char being an integer) // alt_json's string_t (alt_string) is not directly constructible from std::string, so the
// (*) disable implicit conversion for json_refs of any basic_json type // cross-basic_json conversion falls back to the array-conversion path, incorrectly representing
// alt_json j = R"( // objects as arrays of [key, value] pairs and strings as arrays of character codes.
// { // See https://github.com/nlohmann/json/issues/3425 for details.
// "foo": ["bar", "baz"] // Workaround: use alt_json::parse() instead of implicit conversion.
// }
// )"_json;
auto j = alt_json::parse(R"({"foo": ["bar", "baz"]})"); auto j = alt_json::parse(R"({"foo": ["bar", "baz"]})");
CHECK(j.at(alt_json::json_pointer("/foo/0")) == j["foo"][0]); CHECK(j.at(alt_json::json_pointer("/foo/0")) == j["foo"][0]);
+32 -13
View File
@@ -369,6 +369,7 @@ TEST_CASE("lexicographical comparison operators")
SECTION("comparison: not equal") SECTION("comparison: not equal")
{ {
// check that two values compare unequal as expected // check that two values compare unequal as expected
// operator!= now means exactly !(a==b) without special cases for NaN/discarded
for (size_t i = 0; i < j_values.size(); ++i) for (size_t i = 0; i < j_values.size(); ++i)
{ {
for (size_t j = 0; j < j_values.size(); ++j) for (size_t j = 0; j < j_values.size(); ++j)
@@ -376,25 +377,12 @@ TEST_CASE("lexicographical comparison operators")
CAPTURE(i) CAPTURE(i)
CAPTURE(j) CAPTURE(j)
if (json::compares_unordered(j_values[i], j_values[j], true))
{
// if two values compare unordered,
// check that the boolean comparison result is always false
CHECK_FALSE(j_values[i] != j_values[j]);
}
else
{
// otherwise, check that they compare according to their definition
// as the inverse of equal
CHECK((j_values[i] != j_values[j]) == !(j_values[i] == j_values[j])); CHECK((j_values[i] != j_values[j]) == !(j_values[i] == j_values[j]));
} }
} }
}
// compare with null pointer // compare with null pointer
const json j_null; const json j_null;
CHECK((j_null != nullptr) == false);
CHECK((nullptr != j_null) == false);
CHECK((j_null != nullptr) == !(j_null == nullptr)); CHECK((j_null != nullptr) == !(j_null == nullptr));
CHECK((nullptr != j_null) == !(nullptr == j_null)); CHECK((nullptr != j_null) == !(nullptr == j_null));
} }
@@ -594,3 +582,34 @@ TEST_CASE("lexicographical comparison operators")
} }
#endif #endif
} }
#if JSON_HAS_THREE_WAY_COMPARISON
// JSON_HAS_CPP_20 (do not remove; see note at top of file)
TEST_CASE("regression #3868 - heterogeneous comparisons compile under C++20 (P2468R2)")
{
// Issue #3868: operator!= was preventing compiler from synthesizing reversed
// operator== candidates under C++20's P2468R2 rewritten candidate rules.
// Verify that heterogeneous comparisons now work.
SECTION("string vs json")
{
std::string s = "string";
json j = "string";
CHECK(s == j);
CHECK(j == s);
CHECK_FALSE(s != j);
CHECK_FALSE(j != s);
}
SECTION("other heterogeneous types")
{
int i = 42;
json j = 42;
CHECK(i == j);
CHECK(j == i);
CHECK_FALSE(i != j);
CHECK_FALSE(j != i);
}
}
#endif
+1 -1
View File
@@ -278,7 +278,7 @@ TEST_CASE("constructors")
const auto t = j.get<std::tuple<int, float, std::string>>(); const auto t = j.get<std::tuple<int, float, std::string>>();
CHECK(std::get<0>(t) == j[0]); CHECK(std::get<0>(t) == j[0]);
CHECK(std::get<1>(t) == j[1]); CHECK(std::get<1>(t) == j[1]);
// CHECK(std::get<2>(t) == j[2]); // commented out due to CI issue, see https://github.com/nlohmann/json/pull/3985 and https://github.com/nlohmann/json/issues/4025 CHECK(std::get<2>(t) == j[2]);
} }
SECTION("std::tuple tie") SECTION("std::tuple tie")
+26
View File
@@ -168,6 +168,32 @@ TEST_CASE("convenience functions")
CHECK_THROWS_WITH_AS(check_escaped("\xC2"), "[json.exception.type_error.316] incomplete UTF-8 string; last byte: 0xC2", json::type_error&); CHECK_THROWS_WITH_AS(check_escaped("\xC2"), "[json.exception.type_error.316] incomplete UTF-8 string; last byte: 0xC2", json::type_error&);
} }
SECTION("string escape with ensure_ascii")
{
// control characters are escaped regardless of ensure_ascii
check_escaped("\x01", "\\u0001", true);
check_escaped("\x1f", "\\u001f", true);
// non-ASCII code points in the Basic Multilingual Plane are emitted as
// a single lowercase \uXXXX escape (exercises every nibble position)
check_escaped("\xC2\x80", "\\u0080", true); // U+0080
check_escaped("\xC3\xBF", "\\u00ff", true); // U+00FF (ÿ)
check_escaped("\xDF\xBF", "\\u07ff", true); // U+07FF
check_escaped("\xE4\xBD\xA0", "\\u4f60", true); // U+4F60 (你)
check_escaped("\xEA\xAF\x8D", "\\uabcd", true); // U+ABCD
check_escaped("\xEF\xBF\xBD", "\\ufffd", true); // U+FFFD (replacement char, all-f nibbles)
// code points outside the BMP are emitted as a UTF-16 surrogate pair
// of two lowercase \uXXXX escapes
check_escaped("\xF0\x90\x80\x80", "\\ud800\\udc00", true); // U+10000 (lowest astral)
check_escaped("\xF0\x9F\x98\x80", "\\ud83d\\ude00", true); // U+1F600 (😀)
check_escaped("\xF4\x8F\xBF\xBF", "\\udbff\\udfff", true); // U+10FFFF (highest code point)
// with ensure_ascii disabled, non-ASCII input is passed through verbatim
check_escaped("\xE4\xBD\xA0", "\xE4\xBD\xA0", false);
check_escaped("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", false);
}
SECTION("string concat") SECTION("string concat")
{ {
using nlohmann::detail::concat; using nlohmann::detail::concat;
+4 -14
View File
@@ -1761,27 +1761,16 @@ TEST_CASE("std::filesystem::path")
} }
#endif #endif
#if !JSON_USE_IMPLICIT_CONVERSIONS
TEST_CASE("std::optional") TEST_CASE("std::optional")
{ {
SECTION("null") SECTION("null")
{ {
const json j_null; json j_null;
const std::optional<std::string> opt_null; std::optional<std::string> opt_null;
CHECK(json(opt_null) == j_null); CHECK(json(opt_null) == j_null);
CHECK(j_null.get<std::optional<std::string>>() == std::nullopt); CHECK(j_null.get<std::optional<std::string>>() == std::nullopt);
// Constructing std::optional<T> directly from JSON null throws because
// std::optional's own converting constructor is chosen over basic_json's
// operator T(). This is a language-level limitation (std::optional<T> is
// constructible from T, and T is constructible from basic_json via the
// operator); there is no SFINAE path that distinguishes "call from inside
// std::optional's constructor" from "direct call". Use get<std::optional<T>>()
// or get_to() instead for correct null handling. See #4864 and #5246.
CHECK_THROWS_WITH_AS(std::optional<std::string>(j_null),
"[json.exception.type_error.302] type must be string, but is null", json::type_error&);
CHECK_THROWS_WITH_AS(std::optional<int>(j_null),
"[json.exception.type_error.302] type must be number, but is null", json::type_error&);
} }
SECTION("string") SECTION("string")
@@ -1830,6 +1819,7 @@ TEST_CASE("std::optional")
} }
} }
#endif #endif
#endif
#ifdef JSON_HAS_CPP_17 #ifdef JSON_HAS_CPP_17
#undef JSON_HAS_CPP_17 #undef JSON_HAS_CPP_17
+1 -1
View File
@@ -942,7 +942,7 @@ TEST_CASE("iterators 2")
json j_expected{5, 4, 3, 2, 1}; json j_expected{5, 4, 3, 2, 1};
auto reversed = j | std::views::reverse; auto reversed = j | std::views::reverse;
CHECK(std::ranges::equal(reversed, j_expected)); CHECK(reversed == j_expected);
} }
SECTION("transform") SECTION("transform")