Files
json/tests/src/unit-binary_utf8_error_handler.cpp
T
Niels Lohmann d8d47be4a5 Fix CI on develop after merging the ready-to-merge PRs (#5754)
* Keep the serializer conversion for objects whose keys cannot be converted

#5591 added a test converting nlohmann::json into a basic_json whose
string type cannot be constructed from std::string. That instantiates
convert_iteratively(), whose members.emplace_back(next.key(), ...) needs
exactly that key conversion, and broke the build of unit-alt-string.
Dispatch on the key's constructibility and leave such conversions to the
serializers, as the levels above the nesting bound already do (#3425).

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

* Fix the remaining CI failures on develop

- unit-wstring: with a 16-bit wchar_t (Windows), a lone surrogate is
  reported as the ill-formed byte 0xFF since #5704; the std::wstring
  expectations still had the previous <U+0000>.
- ci_single_binaries: json_literals.hpp (#5610) and json.hpp include each
  other on purpose, and IWYU, not following the cycle, asks to replace
  json.hpp with json_fwd.hpp. Report its findings without failing the
  build, as already done for json.hpp.

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

* Fix the library warnings and noexcept specifications from the merged PRs

- binary_reader: rename the error_handler constructor parameter, which
  shadowed the member (-Wshadow, -Wshadow-field-in-constructor; #5746)
- basic_json(copy_construct_tag, ...): declare it noexcept when copying
  the base class is (GCC 16 -Wnoexcept; #5690)
- the scalar-on-left legacy comparison operators: noexcept only when
  converting the scalar is, like their member counterparts (#5682, #5751)
- compare_leaves: use std::is_eq/is_lt/is_gt instead of comparing a
  std::partial_ordering with 0 (-Wzero-as-null-pointer-constant; #5686)
- serializer: silence MSVC C4127 for the EnsureAscii template parameter
  (#5741, #5746)
- clang-tidy: return the sanitized reference in binary_writer, take the
  key of ordered_map::find_impl by const reference (#5727), and mark the
  switches over parse_array_index (#5728)
- ordered_map: keep <memory> for std::allocator (IWYU)

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

* Split unit-conversions.cpp so MinGW can link it

clang 18 with the MinGW linker failed to link test-conversions_cpp17
("relocation truncated to fit: IMAGE_REL_AMD64_REL32"). As windows.yml
recommends, keep the objects small by splitting the test file.

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

* Fix the tests added by the merged PRs for all CI configurations

- discard the results of dump() and from_*() in CHECK_THROWS with
  utils::ignore_return_value (GCC -Werror=unused-result)
- give unit-bson's huge_string_t a default constructor (MSVC C2512,
  GCC 5, clang 3.5)
- unit-disabled_exceptions: use the literals namespace when the global
  UDLs are off (ci_test_noglobaludls; #5700)
- unit-binary_utf8_strict: expect the JSON pointer prefix with
  JSON_DIAGNOSTICS (#5741)
- skip the tests that rely on exceptions under JSON_NOEXCEPTION
  (#5678, #5732)
- clang-tidy and clang -Werror: static test data, CAPTURE(...);,
  const-correctness, use-after-move alias, unused conversion operator,
  a missing <iterator> include

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

* Title the macro examples and add JSON_STRICT_BINARY_UTF8 to the docset

The documentation style check requires "Example: ..." titles on pages with several examples (#5741, #5591) and a docset entry for every macro page.

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

* Regenerate BUILD.bazel and nlohmann_json.natvis

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

#5746 added detail/output/error_handler.hpp and #5741 the json_abi_sbu8 ABI tag.

* Install libidn11 for the CMake 3.5.0 binary in ci_cmake_flags

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

#5733 moved ci_cmake_options from ubuntu:focal to ubuntu:24.04, which no longer ships libidn.so.11; the CMake 3.5.0 release binary links against it, so every ci_cmake_flags run has failed since. Install focal's libidn11 package for that matrix entry only.

* Suppress Infer's false STACK_VARIABLE_ADDRESS_ESCAPE in get_impl

get_impl() returns its local by value. A test added by the merged PRs instantiates it with a type Infer misreads, so ci_infer reported the 2021 code for the first time.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-04 17:18:19 +02:00

373 lines
18 KiB
C++

// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include "test_utils.hpp"
#include <nlohmann/json.hpp>
using nlohmann::json;
#include <string>
#include <vector>
namespace
{
struct ill_formed_case
{
const char* name;
std::string bytes;
};
// RFC 3629 ill-formed sequences used throughout this file, plus one
// well-formed sequence for contrast
std::vector<ill_formed_case> ill_formed_cases()
{
return
{
{"overlong", "\xC0\xAE"},
{"lone_0xFF", "\xFF"},
{"truncated", "\xE2\x82"},
{"surrogate", "\xED\xA0\x80"},
};
}
std::string valid_sequence()
{
return "\xC3\xA9"; // U+00E9, "é"
}
using eh = json::error_handler_t;
std::vector<eh> all_handlers()
{
return {eh::strict, eh::replace, eh::ignore, eh::keep};
}
// what dump()+parse() produces for a sanitizing error_handler; this is the
// ground truth every binary writer/reader is checked against
std::string dump_and_parse(const std::string& raw, eh error_handler)
{
return json::parse(json(raw).dump(-1, ' ', false, error_handler)).get<std::string>();
}
} // namespace
TEST_CASE("UTF-8 error_handler for the binary readers and writers")
{
SECTION("writers: string value")
{
for (const auto& c : ill_formed_cases())
{
CAPTURE(c.name)
const json jval = c.bytes;
CHECK_THROWS_AS(json::to_cbor(jval, eh::strict), json::type_error&);
CHECK_THROWS_AS(json::to_msgpack(jval, eh::strict), json::type_error&);
CHECK_THROWS_AS(json::to_ubjson(jval, false, false, eh::strict), json::type_error&);
CHECK_THROWS_AS(json::to_bjdata(jval, false, false, json::bjdata_version_t::draft2, eh::strict), json::type_error&);
{
json jobj;
jobj["k"] = jval;
CHECK_THROWS_AS(json::to_bson(jobj, eh::strict), json::type_error&);
}
for (const auto h :
{
eh::replace, eh::ignore
})
{
CAPTURE(static_cast<int>(h))
const std::string expected = dump_and_parse(c.bytes, h);
CHECK(json::from_cbor(json::to_cbor(jval, h)).get<std::string>() == expected);
CHECK(json::from_msgpack(json::to_msgpack(jval, h)).get<std::string>() == expected);
CHECK(json::from_ubjson(json::to_ubjson(jval, false, false, h)).get<std::string>() == expected);
CHECK(json::from_bjdata(json::to_bjdata(jval, false, false, json::bjdata_version_t::draft2, h)).get<std::string>() == expected);
{
json jobj;
jobj["k"] = jval;
const auto bytes = json::to_bson(jobj, h);
CHECK(json::from_bson(bytes)["k"].get<std::string>() == expected);
}
}
// keep: the writer passes the ill-formed bytes through unchanged,
// exactly as every binary writer did before this parameter existed
CHECK(json::from_cbor(json::to_cbor(jval, eh::keep)).get<std::string>() == c.bytes);
CHECK(json::from_msgpack(json::to_msgpack(jval, eh::keep)).get<std::string>() == c.bytes);
CHECK(json::from_ubjson(json::to_ubjson(jval, false, false, eh::keep)).get<std::string>() == c.bytes);
CHECK(json::from_bjdata(json::to_bjdata(jval, false, false, json::bjdata_version_t::draft2, eh::keep)).get<std::string>() == c.bytes);
{
json jobj;
jobj["k"] = jval;
const auto bytes = json::to_bson(jobj, eh::keep);
CHECK(json::from_bson(bytes)["k"].get<std::string>() == c.bytes);
}
}
}
SECTION("writers: object key")
{
for (const auto& c : ill_formed_cases())
{
CAPTURE(c.name)
json jobj;
jobj[c.bytes] = 1;
CHECK_THROWS_AS(json::to_cbor(jobj, eh::strict), json::type_error&);
CHECK_THROWS_AS(json::to_msgpack(jobj, eh::strict), json::type_error&);
CHECK_THROWS_AS(json::to_ubjson(jobj, false, false, eh::strict), json::type_error&);
CHECK_THROWS_AS(json::to_bjdata(jobj, false, false, json::bjdata_version_t::draft2, eh::strict), json::type_error&);
CHECK_THROWS_AS(json::to_bson(jobj, eh::strict), json::type_error&);
for (const auto h :
{
eh::replace, eh::ignore
})
{
CAPTURE(static_cast<int>(h))
const std::string expected = dump_and_parse(c.bytes, h);
CHECK(json::from_cbor(json::to_cbor(jobj, h)).begin().key() == expected);
CHECK(json::from_msgpack(json::to_msgpack(jobj, h)).begin().key() == expected);
CHECK(json::from_ubjson(json::to_ubjson(jobj, false, false, h)).begin().key() == expected);
CHECK(json::from_bjdata(json::to_bjdata(jobj, false, false, json::bjdata_version_t::draft2, h)).begin().key() == expected);
CHECK(json::from_bson(json::to_bson(jobj, h)).begin().key() == expected);
}
// keep: object keys round-trip unchanged too
CHECK(json::from_cbor(json::to_cbor(jobj, eh::keep)).begin().key() == c.bytes);
CHECK(json::from_msgpack(json::to_msgpack(jobj, eh::keep)).begin().key() == c.bytes);
CHECK(json::from_ubjson(json::to_ubjson(jobj, false, false, eh::keep)).begin().key() == c.bytes);
CHECK(json::from_bjdata(json::to_bjdata(jobj, false, false, json::bjdata_version_t::draft2, eh::keep)).begin().key() == c.bytes);
CHECK(json::from_bson(json::to_bson(jobj, eh::keep)).begin().key() == c.bytes);
}
}
SECTION("readers: string value")
{
for (const auto& c : ill_formed_cases())
{
CAPTURE(c.name)
// bytes produced the lenient (keep) way, as any binary reader
// accepted them before this parameter existed
const auto cbor_bytes = json::to_cbor(json(c.bytes), eh::keep);
const auto msgpack_bytes = json::to_msgpack(json(c.bytes)); // to_msgpack has no error_handler; always pass-through
const auto ubjson_bytes = json::to_ubjson(json(c.bytes), false, false, eh::keep);
const auto bjdata_bytes = json::to_bjdata(json(c.bytes), false, false, json::bjdata_version_t::draft2, eh::keep);
const auto bson_bytes = [&c]
{
json jobj;
jobj["k"] = c.bytes;
return json::to_bson(jobj, eh::keep);
}();
// keep (the default): bytes are kept unchanged
CHECK(json::from_cbor(cbor_bytes).get<std::string>() == c.bytes);
CHECK(json::from_msgpack(msgpack_bytes).get<std::string>() == c.bytes);
CHECK(json::from_ubjson(ubjson_bytes).get<std::string>() == c.bytes);
CHECK(json::from_bjdata(bjdata_bytes).get<std::string>() == c.bytes);
CHECK(json::from_bson(bson_bytes)["k"].get<std::string>() == c.bytes);
// strict: parse_error.113, discarded (not thrown) when allow_exceptions is false
CHECK_THROWS_AS(utils::ignore_return_value(json::from_cbor(cbor_bytes, true, true, json::cbor_tag_handler_t::error, eh::strict)), json::parse_error&);
CHECK(json::from_cbor(cbor_bytes, true, false, json::cbor_tag_handler_t::error, eh::strict).is_discarded());
CHECK_THROWS_AS(utils::ignore_return_value(json::from_msgpack(msgpack_bytes, true, true, eh::strict)), json::parse_error&);
CHECK(json::from_msgpack(msgpack_bytes, true, false, eh::strict).is_discarded());
CHECK_THROWS_AS(utils::ignore_return_value(json::from_ubjson(ubjson_bytes, true, true, eh::strict)), json::parse_error&);
CHECK(json::from_ubjson(ubjson_bytes, true, false, eh::strict).is_discarded());
CHECK_THROWS_AS(utils::ignore_return_value(json::from_bjdata(bjdata_bytes, true, true, eh::strict)), json::parse_error&);
CHECK(json::from_bjdata(bjdata_bytes, true, false, eh::strict).is_discarded());
CHECK_THROWS_AS(utils::ignore_return_value(json::from_bson(bson_bytes, true, true, eh::strict)), json::parse_error&);
CHECK(json::from_bson(bson_bytes, true, false, eh::strict).is_discarded());
// replace / ignore: match what dump() would have sanitized the same bytes to
for (const auto h :
{
eh::replace, eh::ignore
})
{
CAPTURE(static_cast<int>(h))
const std::string expected = dump_and_parse(c.bytes, h);
CHECK(json::from_cbor(cbor_bytes, true, true, json::cbor_tag_handler_t::error, h).get<std::string>() == expected);
CHECK(json::from_msgpack(msgpack_bytes, true, true, h).get<std::string>() == expected);
CHECK(json::from_ubjson(ubjson_bytes, true, true, h).get<std::string>() == expected);
CHECK(json::from_bjdata(bjdata_bytes, true, true, h).get<std::string>() == expected);
CHECK(json::from_bson(bson_bytes, true, true, h)["k"].get<std::string>() == expected);
}
}
}
SECTION("readers: object key")
{
for (const auto& c : ill_formed_cases())
{
CAPTURE(c.name)
json jobj;
jobj[c.bytes] = 1;
const auto cbor_bytes = json::to_cbor(jobj, eh::keep);
const auto msgpack_bytes = json::to_msgpack(jobj);
const auto ubjson_bytes = json::to_ubjson(jobj, false, false, eh::keep);
const auto bjdata_bytes = json::to_bjdata(jobj, false, false, json::bjdata_version_t::draft2, eh::keep);
const auto bson_bytes = json::to_bson(jobj, eh::keep);
CHECK(json::from_cbor(cbor_bytes).begin().key() == c.bytes);
CHECK(json::from_msgpack(msgpack_bytes).begin().key() == c.bytes);
CHECK(json::from_ubjson(ubjson_bytes).begin().key() == c.bytes);
CHECK(json::from_bjdata(bjdata_bytes).begin().key() == c.bytes);
CHECK(json::from_bson(bson_bytes).begin().key() == c.bytes);
CHECK_THROWS_AS(utils::ignore_return_value(json::from_cbor(cbor_bytes, true, true, json::cbor_tag_handler_t::error, eh::strict)), json::parse_error&);
CHECK_THROWS_AS(utils::ignore_return_value(json::from_msgpack(msgpack_bytes, true, true, eh::strict)), json::parse_error&);
CHECK_THROWS_AS(utils::ignore_return_value(json::from_ubjson(ubjson_bytes, true, true, eh::strict)), json::parse_error&);
CHECK_THROWS_AS(utils::ignore_return_value(json::from_bjdata(bjdata_bytes, true, true, eh::strict)), json::parse_error&);
CHECK_THROWS_AS(utils::ignore_return_value(json::from_bson(bson_bytes, true, true, eh::strict)), json::parse_error&);
for (const auto h :
{
eh::replace, eh::ignore
})
{
CAPTURE(static_cast<int>(h))
const std::string expected = dump_and_parse(c.bytes, h);
CHECK(json::from_cbor(cbor_bytes, true, true, json::cbor_tag_handler_t::error, h).begin().key() == expected);
CHECK(json::from_msgpack(msgpack_bytes, true, true, h).begin().key() == expected);
CHECK(json::from_ubjson(ubjson_bytes, true, true, h).begin().key() == expected);
CHECK(json::from_bjdata(bjdata_bytes, true, true, h).begin().key() == expected);
CHECK(json::from_bson(bson_bytes, true, true, h).begin().key() == expected);
}
}
}
SECTION("well-formed UTF-8 is unaffected by error_handler")
{
const json jval = valid_sequence();
json jobj;
jobj[valid_sequence()] = valid_sequence();
for (const auto h : all_handlers())
{
CAPTURE(static_cast<int>(h))
CHECK(json::from_cbor(json::to_cbor(jval, h)).get<std::string>() == valid_sequence());
CHECK(json::from_msgpack(json::to_msgpack(jval, h)).get<std::string>() == valid_sequence());
CHECK(json::from_ubjson(json::to_ubjson(jval, false, false, h)).get<std::string>() == valid_sequence());
CHECK(json::from_bjdata(json::to_bjdata(jval, false, false, json::bjdata_version_t::draft2, h)).get<std::string>() == valid_sequence());
CHECK(json::from_bson(json::to_bson(jobj, h)).begin().key() == valid_sequence());
CHECK(json::from_cbor(json::to_cbor(jval, eh::keep), true, true, json::cbor_tag_handler_t::error, h).get<std::string>() == valid_sequence());
CHECK(json::from_msgpack(json::to_msgpack(jval), true, true, h).get<std::string>() == valid_sequence());
}
}
SECTION("dump() with error_handler_t::keep writes raw bytes as is")
{
for (const auto& c : ill_formed_cases())
{
CAPTURE(c.name)
const json jval = c.bytes;
const std::string dumped = jval.dump(-1, ' ', false, eh::keep);
CHECK(dumped.find(c.bytes) != std::string::npos);
// even with ensure_ascii, the ill-formed bytes are written as is
const std::string dumped_ascii = jval.dump(-1, ' ', true, eh::keep);
CHECK(dumped_ascii.find(c.bytes) != std::string::npos);
}
// well-formed characters around an ill-formed sequence are still
// escaped as usual under ensure_ascii
const json mixed = valid_sequence() + ill_formed_cases()[1].bytes; // "é" + lone 0xFF
const std::string dumped_mixed = mixed.dump(-1, ' ', true, eh::keep);
CHECK(dumped_mixed.find("\\u00e9") != std::string::npos);
CHECK(dumped_mixed.find(ill_formed_cases()[1].bytes) != std::string::npos);
// the byte that ends an ill-formed sequence is read again, so a quote,
// a backslash, or a control character after it is still escaped, and
// a well-formed code point after it is escaped under ensure_ascii
for (const bool ensure_ascii :
{
false, true
})
{
CAPTURE(ensure_ascii)
CHECK(json("\xC3\"").dump(-1, ' ', ensure_ascii, eh::keep) == "\"\xC3\\\"\"");
CHECK(json("\xC3\\").dump(-1, ' ', ensure_ascii, eh::keep) == "\"\xC3\\\\\"");
CHECK(json("\xC3\n").dump(-1, ' ', ensure_ascii, eh::keep) == "\"\xC3\\n\"");
CHECK(json("\xE2\x82\"").dump(-1, ' ', ensure_ascii, eh::keep) == "\"\xE2\x82\\\"\"");
CHECK(json("\xFF\"").dump(-1, ' ', ensure_ascii, eh::keep) == "\"\xFF\\\"\"");
CHECK(json("a\xE2\x82").dump(-1, ' ', ensure_ascii, eh::keep) == "\"a\xE2\x82\"");
}
CHECK(json("\xC3\xC3\xA9").dump(-1, ' ', false, eh::keep) == "\"\xC3\xC3\xA9\"");
CHECK(json("\xC3\xC3\xA9").dump(-1, ' ', true, eh::keep) == "\"\xC3\\u00e9\"");
}
SECTION("to_msgpack defaults to keep; to_bon8 is not affected by error_handler")
{
const json jval = ill_formed_cases()[1].bytes; // lone 0xFF
// to_msgpack's error_handler defaults to keep, as MessagePack's spec
// allows any bytes in a str, so the bytes are passed through
CHECK(json::to_msgpack(jval) == json::to_msgpack(jval, eh::keep));
CHECK(json::from_msgpack(json::to_msgpack(jval)).get<std::string>() == ill_formed_cases()[1].bytes);
// the diagnostics context of an ill-formed key is the object
json jobj;
jobj["\xFF"] = 1;
CHECK_THROWS_WITH_AS(json::to_msgpack(jobj, eh::strict), "[json.exception.type_error.316] invalid UTF-8 byte at index 0: 0xFF", json::type_error&);
// to_bon8 has no error_handler parameter; UTF-8 is structural for
// BON8, so it always rejects ill-formed input
CHECK_THROWS_AS(json::to_bon8(jval), json::type_error&);
}
SECTION("allow_exceptions=false with error_handler_t::strict discards the value")
{
const auto bytes = json::to_cbor(json(ill_formed_cases()[0].bytes), eh::keep);
const json result = json::from_cbor(bytes, true, false, json::cbor_tag_handler_t::error, eh::strict);
CHECK(result.is_discarded());
}
SECTION("default parameters are unchanged")
{
const json jval = ill_formed_cases()[0].bytes;
// to_*: the default error_handler is keep, so ill-formed bytes are
// written unchanged, exactly as in release 3.12.0 (it is strict only
// if JSON_STRICT_BINARY_UTF8 is enabled, see
// unit-binary_utf8_strict.cpp)
CHECK(json::to_cbor(jval) == json::to_cbor(jval, eh::keep));
CHECK(json::to_ubjson(jval) == json::to_ubjson(jval, false, false, eh::keep));
CHECK(json::to_bjdata(jval) == json::to_bjdata(jval, false, false, json::bjdata_version_t::draft2, eh::keep));
{
json jobj;
jobj["k"] = jval;
CHECK(json::to_bson(jobj) == json::to_bson(jobj, eh::keep));
}
// from_*: the default error_handler is keep, so ill-formed bytes are
// still accepted unchanged, exactly as in release 3.12.0
const auto cbor_bytes = json::to_cbor(jval, eh::keep);
CHECK(json::from_cbor(cbor_bytes).get<std::string>() == ill_formed_cases()[0].bytes);
const auto ubjson_bytes = json::to_ubjson(jval, false, false, eh::keep);
CHECK(json::from_ubjson(ubjson_bytes).get<std::string>() == ill_formed_cases()[0].bytes);
const auto bjdata_bytes = json::to_bjdata(jval, false, false, json::bjdata_version_t::draft2, eh::keep);
CHECK(json::from_bjdata(bjdata_bytes).get<std::string>() == ill_formed_cases()[0].bytes);
const auto msgpack_bytes = json::to_msgpack(jval);
CHECK(json::from_msgpack(msgpack_bytes).get<std::string>() == ill_formed_cases()[0].bytes);
json bson_obj;
bson_obj["k"] = jval;
const auto bson_bytes = json::to_bson(bson_obj, eh::keep);
CHECK(json::from_bson(bson_bytes)["k"].get<std::string>() == ill_formed_cases()[0].bytes);
}
}