mirror of
https://github.com/nlohmann/json.git
synced 2026-10-03 22:44:26 +07:00
* Drop stale LCOV_EXCL_LINE from the json_pointer out_of_range.410 throw The comment said the size_type overflow check in array_index() is only triggered on special platforms like 32-bit, and the throw was excluded from coverage. On 64-bit platforms the check is true for SIZE_MAX itself, and unit-json_pointer.cpp has asserted that case four times since #5395, so the line is executed in the coverage job. Reword the comment and remove the exclusion marker so the coverage report notices if the tests stop reaching it. Part of #5725 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Name all three C-array check aliases in the enum-macro NOLINTs NLOHMANN_JSON_SERIALIZE_ENUM(_STRICT) suppressed the c-array warning under modernize-avoid-c-arrays only, but clang-tidy emits the same diagnostic under the aliases cppcoreguidelines-avoid-c-arrays and hicpp-avoid-c-arrays too. Any user running those checks got a false positive at every macro expansion, and our own tests needed a local NOLINT at each call site to work around it. Name all three aliases in the four macro comments instead, and drop the now-redundant c-array names from the five test call-site NOLINTs. Comment-only change; behavior, the public API, and the ABI do not change. Ran make amalgamate. Part of #5725 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Include doctest as a SYSTEM directory instead of disabling warnings for all tests test_main added -Wno-deprecated and -Wno-float-equal as PUBLIC compile options for every non-MSVC compiler, so they were applied to every translation unit, library headers included, and silenced the CI warnings meant to check the library's own -Wfloat-equal pragmas. The only code that actually needed the suppression was the vendored doctest.h, which was included as a normal (non-SYSTEM) directory. Include thirdparty/doctest as SYSTEM for test_main, matching what tests/abi/CMakeLists.txt already does, and drop the two suppressions from both targets. Verified locally that unit-comparison, unit-conversions and unit-constructor1 compile clean with -Werror -Weverything and doctest as -isystem, and that CMake still configures with JSON_BuildTests=ON. Part of #5725 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Remove the no-op ci_clang_analyze target ci_clang_analyze configured the build with the real compiler and only then wrapped ninja with scan-build. scan-build intercepts compiles by overriding CC/CXX, but build.ninja already had the compiler path baked in from the configure step, so every run bypassed the analyzer: CI logs show "No bugs found" after a normal build, never an analysis. The job also used Debian's frozen clang-tools-14 rather than the image's own clang, and CLANG_ANALYZER_CHECKS still named three valist.* checkers that current clang merged into security.VAList. ci_clang_tidy already runs every clang-analyzer-* check (via .clang-tidy's "Checks: '*'") with warnings as errors, so nothing is lost by removing the dead job. Delete ci_clang_analyze, CLANG_ANALYZER_CHECKS and the SCAN_BUILD_TOOL lookup from cmake/ci.cmake, drop it from the ubuntu.yml ci_static_analysis_clang matrix, and drop the now-unused clang-tools apt package (iwyu stays for ci_single_binaries). Reword quality_assurance.md and assurance_case.md, which described the dead job as a working control, to say the Clang Static Analyzer checks run through clang-tidy. Verified that `cmake -DJSON_CI=ON` still configures cleanly and that ci_clang_analyze no longer appears in the generated build or in any CMake/workflow file. Part of #5725 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Re-enable portability-template-virtual-member-function; remove redundant forwards .clang-tidy disabled three checks "to get the CI going" (#4489, 2024-11-13): portability-template-virtual-member-function, bugprone-use-after-move and its alias hicpp-invalid-access-moved. portability-template-virtual-member-function only flagged output_stream_adapter::write_character/write_characters; annotate both with NOLINT and re-enable the check. bugprone-use-after-move flagged several double forwards that have no effect at runtime: - from_json.hpp calls std::forward<BasicJsonType>(j).at(Idx) inside pack expansions; at() has no ref-qualified overloads and always returns an lvalue reference, so the forward is a no-op. Replace with plain j.at(Idx) in all four places. - the move constructor forwards the whole object to its base class and then reads other's members. That is item 9 of #5724 (together with its cppcheck suppressions) and is left to that change. - input_adapters.hpp forwards the container twice on purpose, so the begin/end iterator types match adapter_type; annotate with NOLINT and a comment instead of changing behavior. The check still flags the move constructor (see above) and two sites in at(KeyType&&) (both overloads, json.hpp, in the throw's string_t(std::forward<KeyType>(key)) after find(std::forward<KeyType>(key))). Open PR #5689 rewrites that hunk, so bugprone-use-after-move (and hicpp-invalid-access-moved) stay disabled for now, with a comment explaining why; re-enable them once #5689 and the #5724 move-constructor change have landed. Also resolve the portability-avoid-pragma-once TODO: single_include never has #pragma once (amalgamate.py strips it) and every supported compiler accepts it in include/, so keep it disabled with an explanatory comment instead of a TODO. Fix the stale "json.hpp, around line 1265" comment in unit-class_parser.cpp, which now points at the move constructor's actual line. Behavior, the public API and the ABI do not change. Verified with clang-tidy 22.1.8 that portability-template-virtual-member-function now reports nothing, that bugprone-use-after-move/ hicpp-invalid-access-moved report only the known at(KeyType&&) and move-constructor sites, and that unit-custom-base-class, unit-constructor1, unit-conversions, unit-element_access2, unit-class_parser and unit-diagnostic-positions (JSON_DIAGNOSTIC_POSITIONS=1) compile under ASan/UBSan and pass with the same assertion counts as before. Ran make amalgamate. Part of #5725 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Fix stale and malformed NOLINT comments json_sax.hpp named "-warnings-as-errors" in the NOLINT list on the two JSON_ASSERT(false) lines; that is the suffix clang-tidy appends to a diagnostic tag under WarningsAsErrors, not a check name, and every other JSON_ASSERT(false) omits it. unit-capacity.cpp carried 30 "// NOLINT(misc-const-correctness)" comments on "json j = ...;" declarations that are all used with non-const members afterwards, so the check has nothing to report there. unit-constructor2.cpp used a blanket "// NOLINT: access after move is OK here" on a use-after-move that hides every check on the line; naming bugprone-use-after-move and hicpp-invalid-access-moved keeps the intent once those checks are re-enabled (#5724). Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 10 * Remove stale .clang-tidy entries -google-runtime-references disabled a check that neither clang-tidy 22.1.8 nor 23.1.2 lists under --list-checks -checks='*'; it was removed upstream. The commented-out HeaderFilterRegex line has been unused since the active HeaderFilterRegex was introduced in #2561 (2021). Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 11 * Remove the GCC C++20 -Wignored-attributes pragma in json.hpp The pragma (added in #5164) claimed to work around the C++ modules redefinition errors of #5103, but #5103 is about hard errors (e.g. "redefinition of std::__is_constant_evaluated()", conflicting std::integral_constant) that ignoring a warning cannot suppress; they are traced to GCC PR 124430 and reproduce with <map> or <string> instead of json.hpp too. A GCC 16.2 -std=gnu++20 -fmodules build following #5103's repro steps still fails with the pragma in place, and a build of all test TUs with GCC_CXXFLAGS (which enable -Wignored-attributes) and the pragma removed produces no such warning. The block only hid a warning class from GCC C++20 users while suggesting #5103 was handled. Overlaps #5610, whose hunks touch the closing half of this pragma to insert the json_literals.hpp include. Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 9 * Fix stale doxygen comments hidden by the -Wdocumentation pragma macro_scope.hpp ignores -Wdocumentation and -Wdocumentation-unknown-command for the whole library, which also hides genuine documentation mistakes: - detail::unescape() documented "@return unescaped string" but returns void and unescapes its argument in place; reworded to "@param[in,out] s string to unescape in place" and dropped the bogus @return. - basic_json::get()'s copy-conversion overload wrote "converted to @tparam ValueType" inside @return, which Doxygen and Clang parse as a second, malformed @tparam; changed to "@a ValueType", matching the two other get() overloads a few lines above that already use it. This narrows the gap the -Wdocumentation pragma needs to cover; fully replacing the Doxygen-only commands it also hides (item 2c) is left for after #5267. Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 2 * Fix -Wextra-semi-stmt at its actual source, not assert() clang_flags.cmake blamed the global -Wno-extra-semi-stmt on assert(), but assert() expands to an expression under glibc and libc++ and does not trigger this warning. unit-assert_macro.cpp overrides JSON_ASSERT with "{if (!(x)) ++assert_counter; }", a bare block followed by a semicolon at every JSON_ASSERT(...) call site in the library; that was the actual source of 151 of the 208 -Wextra-semi-stmt sites found in a Clang 22 -Weverything sweep of the test suite with the flag removed. Switched to the standard do/while(false) macro idiom, which does not expand to a statement-plus-semicolon, and corrected the comment to name the remaining source instead: vendored Doctest's CAPTURE(x) shim, which already ends in a semicolon. Verified with clang++ -Wextra-semi-stmt (plus the file's other CI ignores) that unit-assert_macro.cpp now compiles without any -Wextra-semi-stmt diagnostic. Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 8 (step 1 of 2; step 2 covers the CAPTURE() call sites) * Drop the redundant semicolon from CAPTURE() call sites; remove -Wno-extra-semi-stmt doctest_compatibility.h defines CAPTURE(x) as DOCTEST_CAPTURE(x); (with a trailing semicolon baked into the macro), specifically so call sites do not need to add one themselves; most of the ~267 call sites already follow that convention. The remaining 64 call sites across 20 files wrote "CAPTURE(x);" anyway, turning into a statement plus an empty statement and triggering -Wextra-semi-stmt. Dropped the redundant semicolon at each of those sites. With item 6 having already made vendored Doctest a SYSTEM include, and this the last known source of -Wextra-semi-stmt findings, removed the flag from clang_flags.cmake entirely. Verified with clang++ -Wextra-semi-stmt (plus the file's other CI ignores) that all 20 touched files, plus a file with no CAPTURE() use (unit-json_pointer.cpp), compile without any -Wextra-semi-stmt diagnostic. Signed-off-by: Niels Lohmann <mail@nlohmann.me> #5725 item 8 (step 2 of 2) * Switch ci_static_analysis_clang off the frozen LLVM 22 dev image ubuntu.yml pinned the clang-tidy/clang-tidy-sanitizer/single-binaries job to silkeh/clang:dev, a tag last pushed 2026-02-18 that reports "clang version 22.0.0 (...+20251015...)", a pre-release snapshot from before the LLVM 22 release; the maintainer now updates dev-unstable, 22, and latest instead. Switched to silkeh/clang:22, matching the other clang jobs on :latest. Verified with clang-tidy 22.1.8 (the image's actual version) against this repository's .clang-tidy and library headers what the release image newly reports compared to :dev: - readability-redundant-typename fires at ~250 sites across the _cpp20-relevant conversion/to_chars headers; the library targets C++11 and keeps the typenames, so the check is disabled in .clang-tidy, matching how the file already handles checks that don't fit a C++11 codebase. - misc-anonymous-namespace-in-header fires on the two anonymous namespaces in from_json.hpp and to_json.hpp; added the alias to their existing NOLINT (cert-dcl59-cpp, fuchsia-header-anon-namespaces, google-build-namespaces). - bugprone-std-namespace-modification fires on every addition to namespace std: the std::hash, std::formatter and std::swap overloads in json.hpp, and the std::tuple_size/std::tuple_element specializations in iteration_proxy.hpp (this last file is not named in #5725's item 5, found by actually running clang-tidy 22.1.8 against the current tree). All six are legal, deliberate additions to namespace std (explicit/partial specializations of std types, or the pre-C++20 std::swap overload); annotated each with the check name next to its existing cert-dcl58-cpp NOLINT. - modernize-avoid-c-style-cast reported nothing new. Also added clang++-22/21, clang-tidy-22/21, g++-16 and gcov-16 to the find_program search lists in ci.cmake so a local "maximal warnings" configure prefers the current toolchain version over an older one on PATH. #5725 item 5 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Regenerate cmake/gcc_flags.cmake for GCC 16.2.0 GCC_CXXFLAGS was generated for GCC 15.1.0, but ci_test_gcc and ci_test_gcc_cxx{11..26} now run in gcc:latest, currently GCC 16.2.0, so the "maximal warnings" job was missing warnings introduced since 15.1.0 while carrying entries GCC 16 treats as duplicates or no-ops. Regenerated with https://github.com/nlohmann/gcc_flags (patched locally to not crash on an option whose "-x c++ <opt> -" probe fails before it reads stdin, e.g. -Wabi=; the tool otherwise raises BrokenPipeError instead of recording the option as an error) run against g++ 16.2.0 in the official gcc:16 Docker image, keeping the documented -Wno-* exclusions and the same alphabetical placement scheme as before. Also added three GCC 16 warnings the generator cannot discover on its own because it only probes value ranges/lists it finds in the -Q option name itself, not in the enum choices --help=warnings documents separately: - -Wbidi-chars=any, -Wleading-whitespace=spaces: manually verified these compile cleanly with g++ 16.2.0. - -Wstrict-flex-arrays: deliberately NOT added, unlike the other two. Without -fstrict-flex-arrays (which the library does not enable, as it would change codegen for flexible array members), GCC prints "'-Wstrict-flex-arrays' is ignored when '-fstrict-flex-arrays' is not present" on every translation unit, and under our -Werror that note itself aborts the build. This differs from the harmless no-op warnings already kept in the file (-Whsa, -Wsynth, -Wunreachable-code, -Wunsafe-loop-optimizations), which emit nothing; #5725 item 7 named -Wstrict-flex-arrays as one of the flags GCC 16 adds, but did not anticipate this failure mode. Verified: compiled the library header and a representative set of test translation units (including ones touched by items 1, 3, 8, 9, 10 of this issue) with the regenerated GCC_CXXFLAGS plus -Werror under g++ 16.2.0 at -std=c++11 through -std=c++26, with zero warnings; ran the full local test suite (129/129 passing, unrelated to this compiler) as a regression check. CI must still confirm the actual ci_test_gcc / ci_test_standards_gcc targets end to end, since this was verified with direct g++ invocations rather than through the CMake/ CXXFLAGS environment-variable plumbing in ci.cmake. #5725 item 7 Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Avoid std::basic_string<CharType> for non-character output_adapter CharType output_adapter<CharType, StringType> defaulted StringType to std::basic_string<CharType>, and (with JSON_NO_IO undefined) always declared a std::basic_ostream<CharType>&-taking constructor. For CharType with no non-deprecated std::char_traits specialization (only std::uint8_t is ever used this way, by the binary writers), simply naming either type - as an unused default template argument, or as an unused, never-called constructor's parameter type - instantiates std::char_traits<CharType> merely to name it, which some standard libraries mark deprecated: with the library-wide -Wdocumentation pragma (item 2's other half, left for a later commit) temporarily removed, an Apple clang 21 / libc++ TU calling json::to_cbor(j, vec) with std::vector<std::uint8_t>& got one -Wdeprecated-declarations warning per binary writer at the old output_adapters.hpp:193. Replaced the eager std::basic_string<CharType> / std::basic_ostream <CharType> defaults with a bool-tagged partial specialization (not std::conditional, which requires naming both branches' types up front regardless of which is selected, reproducing the same warning) that only ever names std::basic_string<CharType> / std::basic_ostream <CharType> when CharType is actually one of char, wchar_t, char16_t, char32_t, or (with __cpp_lib_char8_t) char8_t. For any other CharType, output_adapter's StringType and ostream-constructor parameter fall back to two distinct empty placeholder types, kept distinct so the two constructor overloads do not collide into a single redeclaration. Public API / behavior: passing a std::basic_string<std::uint8_t>& or std::basic_ostream<std::uint8_t>& directly to a binary writer's output_adapter now fails to compile instead of compiling with a deprecation warning; this was neither documented nor tested. All documented uses (std::vector<CharType>, std::basic_ostream<CharType> and StringType for character CharType) are unaffected. Verified with Apple clang 21 / libc++, with the two -Wdocumentation* "ignored" pragma lines in macro_scope.hpp temporarily removed and -std=c++11/c++20 plus the project's -Weverything flag set: calling to_cbor/to_msgpack/to_ubjson/to_bjdata/to_bson/to_bon8 on a std::vector<std::uint8_t> now produces no char_traits<unsigned char> (or any other) deprecation warning, while the char-based string- and ostream-adapter paths, and a to_cbor/from_cbor round trip, still compile and run correctly; also verified with GCC 16.2.0. Ran the full local test suite, including the binary-format unit tests (unit-cbor, unit-msgpack, unit-ubjson, unit-bjdata, unit-bson, unit-bon8, unit-binary_writer_sinks, unit-binary_formats, unit-custom-binary-type): 129/129 passing. #5725 item 2 (step a) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Remove the library-wide -Wdocumentation pragma; fix what it hid macro_scope.hpp / macro_unscope.hpp pushed and popped a Clang diagnostic region over the entire library that ignored -Wdocumentation and -Wdocumentation-unknown-command. Removed both pragmas and fixed every finding a full -Wdocumentation (which implies -Wdocumentation-unknown-command and -Wdocumentation-deprecated-sync) build reports, so the library now compiles clean under Clang's documentation checks without a blanket suppression. Overlaps #5267, which is still open and edits a nearby doc block (json.hpp's get()/get_impl() @return, already fixed in the item 2 step (b) commit of this branch); this commit does not touch that block again. Unknown Doxygen alias commands (Doxyfile removed in #3071, so these were never rendered by anything) rewritten as plain prose, keeping the same information: - @requirement REQ-JSON-01 / REQ-JSON-02 (iter_impl.hpp, json_reverse_iterator.hpp): now "This class satisfies the following concept requirements (REQ-JSON-0N):". - @liveexample{prose,example-id} (three sites in json.hpp): kept the prose, dropped the command wrapper and the trailing example-id (docs/mkdocs/docs/examples/*.cpp still exist and are used directly by the rendered docs, not through this in-header alias) and unescaped the "\," commas that were only needed for the old alias's comma-separated argument syntax. - @complexity X (json.hpp x4, json_pointer.hpp x2, serializer.hpp x1): now "Complexity: X". Backslash sequences Clang's comment lexer tried to parse as commands, escaped to render as literal backslashes: - lexer.hpp get_codepoint(): two `\u` occurrences. - binary_reader.hpp get_bson_cstr() / get_bson_cstr_bulk(): two `\x00` occurrences. - serializer.hpp: three `\uXXXX` occurrences (constructor @param, append_codepoint_to_string_buffer() @brief, and the ensure_ascii member comment). One finding remained after all of the above: Clang reports "declaration is marked with '@deprecated' command but does not have a deprecation attribute" on the deprecated sax_parse(span_input_adapter&&, ...) overload, even though JSON_HEDLEY_DEPRECATED_FOR does expand to __attribute__((deprecated(...))) for Clang. Several isolated reproductions of this exact declaration shape - doc comment, template<>, two stacked __attribute__ macros, an overload set sharing the name - did not reproduce the warning, so this looks like a Clang comment/declaration-association quirk specific to this overload inside the much larger basic_json class template, not an actual documentation defect. Rather than keep the pragma library-wide for one Clang false positive, added a tightly scoped -Wdocumentation-deprecated-sync push/pop around just that overload. Verified with Apple clang 21 and the project's actual -Weverything flag set (cmake/clang_flags.cmake) on the full header at -std=c++11 and -std=c++20: zero -Wdocumentation* diagnostics. Also compiled clean with GCC 16.2.0 (the pragmas are already __clang__-gated, so this only confirms no unrelated breakage). Ran make check-amalgamation and the full local test suite: 129/129 passing. #5725 item 2 (step c) Signed-off-by: Niels Lohmann <mail@nlohmann.me> * Take the JSON value by const reference in the array and tuple from_json paths Review feedback on #5737 (gregmarr): once the no-op std::forward calls are gone, the forwarding references have no purpose. from_json_fn passes the value as const BasicJsonType&, so these functions were only ever instantiated with a const lvalue anyway. The std::array, std::pair and std::tuple overloads of from_json and their helpers now take const BasicJsonType& and pass j on unchanged. Because the deduced BasicJsonType is now the plain type, tuple_type and the static_assert name const BasicJsonType& explicitly, so the reference checks are unchanged: get<std::tuple<const std::string&>>() still works, and get<std::tuple<std::string&>>() still fails the same static_assert. from_json_tuple_get_impl keeps its forwarding reference, since tuple_type calls it through std::declval. Behavior, the public API and the ABI do not change. unit-conversions, unit-constructor1, unit-udt, unit-udt_macro, unit-regression1/2/3, unit-deserialization, unit-noexcept, unit-items, unit-allocator, unit-custom-object-type, unit-ordered_json2 and unit-brace-init-copy-semantics pass at C++11, C++17 and C++20 with unchanged assertion counts. Ran make amalgamate. Signed-off-by: Niels Lohmann <mail@nlohmann.me> --------- Signed-off-by: Niels Lohmann <mail@nlohmann.me>
802 lines
30 KiB
C++
802 lines
30 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 <nlohmann/json.hpp>
|
|
using nlohmann::json;
|
|
|
|
#include <array>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
|
|
#include "test_utils.hpp"
|
|
|
|
TEST_CASE("serialization")
|
|
{
|
|
SECTION("operator<<")
|
|
{
|
|
SECTION("no given width")
|
|
{
|
|
std::stringstream ss;
|
|
const json j = {"foo", 1, 2, 3, false, {{"one", 1}}};
|
|
ss << j;
|
|
CHECK(ss.str() == "[\"foo\",1,2,3,false,{\"one\":1}]");
|
|
}
|
|
|
|
SECTION("given width")
|
|
{
|
|
std::stringstream ss;
|
|
const json j = {"foo", 1, 2, 3, false, {{"one", 1}}};
|
|
ss << std::setw(4) << j;
|
|
CHECK(ss.str() ==
|
|
"[\n \"foo\",\n 1,\n 2,\n 3,\n false,\n {\n \"one\": 1\n }\n]");
|
|
}
|
|
|
|
SECTION("given fill")
|
|
{
|
|
std::stringstream ss;
|
|
const json j = {"foo", 1, 2, 3, false, {{"one", 1}}};
|
|
ss << std::setw(1) << std::setfill('\t') << j;
|
|
CHECK(ss.str() ==
|
|
"[\n\t\"foo\",\n\t1,\n\t2,\n\t3,\n\tfalse,\n\t{\n\t\t\"one\": 1\n\t}\n]");
|
|
}
|
|
}
|
|
|
|
SECTION("operator>>")
|
|
{
|
|
SECTION("no given width")
|
|
{
|
|
std::stringstream ss;
|
|
const json j = {"foo", 1, 2, 3, false, {{"one", 1}}};
|
|
j >> ss;
|
|
CHECK(ss.str() == "[\"foo\",1,2,3,false,{\"one\":1}]");
|
|
}
|
|
|
|
SECTION("given width")
|
|
{
|
|
std::stringstream ss;
|
|
const json j = {"foo", 1, 2, 3, false, {{"one", 1}}};
|
|
ss.width(4);
|
|
j >> ss;
|
|
CHECK(ss.str() ==
|
|
"[\n \"foo\",\n 1,\n 2,\n 3,\n false,\n {\n \"one\": 1\n }\n]");
|
|
}
|
|
|
|
SECTION("given fill")
|
|
{
|
|
std::stringstream ss;
|
|
const json j = {"foo", 1, 2, 3, false, {{"one", 1}}};
|
|
ss.width(1);
|
|
ss.fill('\t');
|
|
j >> ss;
|
|
CHECK(ss.str() ==
|
|
"[\n\t\"foo\",\n\t1,\n\t2,\n\t3,\n\tfalse,\n\t{\n\t\t\"one\": 1\n\t}\n]");
|
|
}
|
|
}
|
|
|
|
SECTION("dump")
|
|
{
|
|
SECTION("invalid character")
|
|
{
|
|
const json j = "ä\xA9ü";
|
|
|
|
// dump() is nodiscard; the exception is thrown by dump() itself before it would return
|
|
CHECK_THROWS_WITH_AS(utils::ignore_return_value(j.dump()), "[json.exception.type_error.316] invalid UTF-8 byte at index 2: 0xA9", json::type_error&);
|
|
CHECK_THROWS_WITH_AS(utils::ignore_return_value(j.dump(1, ' ', false, json::error_handler_t::strict)), "[json.exception.type_error.316] invalid UTF-8 byte at index 2: 0xA9", json::type_error&);
|
|
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"äü\"");
|
|
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"ä\xEF\xBF\xBDü\"");
|
|
CHECK(j.dump(-1, ' ', true, json::error_handler_t::replace) == "\"\\u00e4\\ufffd\\u00fc\"");
|
|
}
|
|
|
|
SECTION("invalid character (regression guard for shared UTF-8 decoder, see #5529)")
|
|
{
|
|
// dump_escaped_impl() now calls the UTF-8 decoder shared with the
|
|
// binary readers (detail::decode() in string_utils.hpp) instead
|
|
// of a private copy; the exact type_error.316 message/behavior
|
|
// must stay byte-for-byte the same as before that extraction
|
|
const json j = "ä\xA9ü";
|
|
CHECK_THROWS_WITH_AS(utils::ignore_return_value(j.dump()), "[json.exception.type_error.316] invalid UTF-8 byte at index 2: 0xA9", json::type_error&);
|
|
}
|
|
|
|
SECTION("ending with incomplete character")
|
|
{
|
|
const json j = "123\xC2";
|
|
|
|
// dump() is nodiscard; the exception is thrown by dump() itself before it would return
|
|
CHECK_THROWS_WITH_AS(utils::ignore_return_value(j.dump()), "[json.exception.type_error.316] incomplete UTF-8 string; last byte: 0xC2", json::type_error&);
|
|
CHECK_THROWS_AS(utils::ignore_return_value(j.dump(1, ' ', false, json::error_handler_t::strict)), json::type_error&);
|
|
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"123\"");
|
|
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"123\xEF\xBF\xBD\"");
|
|
CHECK(j.dump(-1, ' ', true, json::error_handler_t::replace) == "\"123\\ufffd\"");
|
|
}
|
|
|
|
SECTION("unexpected character")
|
|
{
|
|
const json j = "123\xF1\xB0\x34\x35\x36";
|
|
|
|
// dump() is nodiscard; the exception is thrown by dump() itself before it would return
|
|
CHECK_THROWS_WITH_AS(utils::ignore_return_value(j.dump()), "[json.exception.type_error.316] invalid UTF-8 byte at index 5: 0x34", json::type_error&);
|
|
CHECK_THROWS_AS(utils::ignore_return_value(j.dump(1, ' ', false, json::error_handler_t::strict)), json::type_error&);
|
|
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"123456\"");
|
|
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"123\xEF\xBF\xBD\x34\x35\x36\"");
|
|
CHECK(j.dump(-1, ' ', true, json::error_handler_t::replace) == "\"123\\ufffd456\"");
|
|
}
|
|
|
|
SECTION("U+FFFD Substitution of Maximal Subparts")
|
|
{
|
|
// Some tests (mostly) from
|
|
// https://www.unicode.org/versions/Unicode11.0.0/ch03.pdf
|
|
// Section 3.9 -- U+FFFD Substitution of Maximal Subparts
|
|
|
|
auto test = [&](std::string const & input, std::string const & expected)
|
|
{
|
|
const json j = input;
|
|
CHECK(j.dump(-1, ' ', true, json::error_handler_t::replace) == "\"" + expected + "\"");
|
|
};
|
|
|
|
test("\xC2", "\\ufffd");
|
|
test("\xC2\x41\x42", "\\ufffd" "\x41" "\x42");
|
|
test("\xC2\xF4", "\\ufffd" "\\ufffd");
|
|
|
|
test("\xF0\x80\x80\x41", "\\ufffd" "\\ufffd" "\\ufffd" "\x41");
|
|
test("\xF1\x80\x80\x41", "\\ufffd" "\x41");
|
|
test("\xF2\x80\x80\x41", "\\ufffd" "\x41");
|
|
test("\xF3\x80\x80\x41", "\\ufffd" "\x41");
|
|
test("\xF4\x80\x80\x41", "\\ufffd" "\x41");
|
|
test("\xF5\x80\x80\x41", "\\ufffd" "\\ufffd" "\\ufffd" "\x41");
|
|
|
|
test("\xF0\x90\x80\x41", "\\ufffd" "\x41");
|
|
test("\xF1\x90\x80\x41", "\\ufffd" "\x41");
|
|
test("\xF2\x90\x80\x41", "\\ufffd" "\x41");
|
|
test("\xF3\x90\x80\x41", "\\ufffd" "\x41");
|
|
test("\xF4\x90\x80\x41", "\\ufffd" "\\ufffd" "\\ufffd" "\x41");
|
|
test("\xF5\x90\x80\x41", "\\ufffd" "\\ufffd" "\\ufffd" "\x41");
|
|
|
|
test("\xC0\xAF\xE0\x80\xBF\xF0\x81\x82\x41", "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\x41");
|
|
test("\xED\xA0\x80\xED\xBF\xBF\xED\xAF\x41", "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\x41");
|
|
test("\xF4\x91\x92\x93\xFF\x41\x80\xBF\x42", "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\x41" "\\ufffd""\\ufffd" "\x42");
|
|
test("\xE1\x80\xE2\xF0\x91\x92\xF1\xBF\x41", "\\ufffd" "\\ufffd" "\\ufffd" "\\ufffd" "\x41");
|
|
}
|
|
}
|
|
|
|
SECTION("to_string")
|
|
{
|
|
auto test = [&](std::string const & input, std::string const & expected)
|
|
{
|
|
using std::to_string;
|
|
const json j = input;
|
|
CHECK(to_string(j) == "\"" + expected + "\"");
|
|
};
|
|
|
|
test(R"({"x":5,"y":6})", R"({\"x\":5,\"y\":6})");
|
|
test("{\"x\":[10,null,null,null]}", R"({\"x\":[10,null,null,null]})");
|
|
test("test", "test");
|
|
test("[3,\"false\",false]", R"([3,\"false\",false])");
|
|
}
|
|
}
|
|
|
|
TEST_CASE_TEMPLATE("serialization for extreme integer values", T, int32_t, uint32_t, int64_t, uint64_t) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization)
|
|
{
|
|
SECTION("minimum")
|
|
{
|
|
constexpr auto minimum = (std::numeric_limits<T>::min)();
|
|
const json j = minimum;
|
|
CHECK(j.dump() == std::to_string(minimum));
|
|
}
|
|
|
|
SECTION("maximum")
|
|
{
|
|
constexpr auto maximum = (std::numeric_limits<T>::max)();
|
|
const json j = maximum;
|
|
CHECK(j.dump() == std::to_string(maximum));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("dump with binary values")
|
|
{
|
|
auto binary = json::binary({1, 2, 3, 4});
|
|
auto binary_empty = json::binary({});
|
|
auto binary_with_subtype = json::binary({1, 2, 3, 4}, 128);
|
|
auto binary_empty_with_subtype = json::binary({}, 128);
|
|
|
|
const json object = {{"key", binary}};
|
|
const json object_empty = {{"key", binary_empty}};
|
|
const json object_with_subtype = {{"key", binary_with_subtype}};
|
|
const json object_empty_with_subtype = {{"key", binary_empty_with_subtype}};
|
|
|
|
const json array = {"value", 1, binary};
|
|
const json array_empty = {"value", 1, binary_empty};
|
|
const json array_with_subtype = {"value", 1, binary_with_subtype};
|
|
const json array_empty_with_subtype = {"value", 1, binary_empty_with_subtype};
|
|
|
|
SECTION("normal")
|
|
{
|
|
CHECK(binary.dump() == "{\"bytes\":[1,2,3,4],\"subtype\":null}");
|
|
CHECK(binary_empty.dump() == "{\"bytes\":[],\"subtype\":null}");
|
|
CHECK(binary_with_subtype.dump() == "{\"bytes\":[1,2,3,4],\"subtype\":128}");
|
|
CHECK(binary_empty_with_subtype.dump() == "{\"bytes\":[],\"subtype\":128}");
|
|
|
|
CHECK(object.dump() == "{\"key\":{\"bytes\":[1,2,3,4],\"subtype\":null}}");
|
|
CHECK(object_empty.dump() == "{\"key\":{\"bytes\":[],\"subtype\":null}}");
|
|
CHECK(object_with_subtype.dump() == "{\"key\":{\"bytes\":[1,2,3,4],\"subtype\":128}}");
|
|
CHECK(object_empty_with_subtype.dump() == "{\"key\":{\"bytes\":[],\"subtype\":128}}");
|
|
|
|
CHECK(array.dump() == "[\"value\",1,{\"bytes\":[1,2,3,4],\"subtype\":null}]");
|
|
CHECK(array_empty.dump() == "[\"value\",1,{\"bytes\":[],\"subtype\":null}]");
|
|
CHECK(array_with_subtype.dump() == "[\"value\",1,{\"bytes\":[1,2,3,4],\"subtype\":128}]");
|
|
CHECK(array_empty_with_subtype.dump() == "[\"value\",1,{\"bytes\":[],\"subtype\":128}]");
|
|
}
|
|
|
|
SECTION("pretty-printed")
|
|
{
|
|
CHECK(binary.dump(4) == "{\n"
|
|
" \"bytes\": [1, 2, 3, 4],\n"
|
|
" \"subtype\": null\n"
|
|
"}");
|
|
CHECK(binary_empty.dump(4) == "{\n"
|
|
" \"bytes\": [],\n"
|
|
" \"subtype\": null\n"
|
|
"}");
|
|
CHECK(binary_with_subtype.dump(4) == "{\n"
|
|
" \"bytes\": [1, 2, 3, 4],\n"
|
|
" \"subtype\": 128\n"
|
|
"}");
|
|
CHECK(binary_empty_with_subtype.dump(4) == "{\n"
|
|
" \"bytes\": [],\n"
|
|
" \"subtype\": 128\n"
|
|
"}");
|
|
|
|
CHECK(object.dump(4) == "{\n"
|
|
" \"key\": {\n"
|
|
" \"bytes\": [1, 2, 3, 4],\n"
|
|
" \"subtype\": null\n"
|
|
" }\n"
|
|
"}");
|
|
CHECK(object_empty.dump(4) == "{\n"
|
|
" \"key\": {\n"
|
|
" \"bytes\": [],\n"
|
|
" \"subtype\": null\n"
|
|
" }\n"
|
|
"}");
|
|
CHECK(object_with_subtype.dump(4) == "{\n"
|
|
" \"key\": {\n"
|
|
" \"bytes\": [1, 2, 3, 4],\n"
|
|
" \"subtype\": 128\n"
|
|
" }\n"
|
|
"}");
|
|
CHECK(object_empty_with_subtype.dump(4) == "{\n"
|
|
" \"key\": {\n"
|
|
" \"bytes\": [],\n"
|
|
" \"subtype\": 128\n"
|
|
" }\n"
|
|
"}");
|
|
|
|
CHECK(array.dump(4) == "[\n"
|
|
" \"value\",\n"
|
|
" 1,\n"
|
|
" {\n"
|
|
" \"bytes\": [1, 2, 3, 4],\n"
|
|
" \"subtype\": null\n"
|
|
" }\n"
|
|
"]");
|
|
CHECK(array_empty.dump(4) == "[\n"
|
|
" \"value\",\n"
|
|
" 1,\n"
|
|
" {\n"
|
|
" \"bytes\": [],\n"
|
|
" \"subtype\": null\n"
|
|
" }\n"
|
|
"]");
|
|
CHECK(array_with_subtype.dump(4) == "[\n"
|
|
" \"value\",\n"
|
|
" 1,\n"
|
|
" {\n"
|
|
" \"bytes\": [1, 2, 3, 4],\n"
|
|
" \"subtype\": 128\n"
|
|
" }\n"
|
|
"]");
|
|
CHECK(array_empty_with_subtype.dump(4) == "[\n"
|
|
" \"value\",\n"
|
|
" 1,\n"
|
|
" {\n"
|
|
" \"bytes\": [],\n"
|
|
" \"subtype\": 128\n"
|
|
" }\n"
|
|
"]");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("dump for basic_json with long double number_float_t")
|
|
{
|
|
// Custom basic_json instantiation with long double as NumberFloatType.
|
|
// On platforms where long double is wider than double (e.g. GCC/Clang on
|
|
// Linux/macOS x86_64), dump() goes through the snprintf path in
|
|
// serializer::dump_float(x, std::false_type). That branch must use the
|
|
// "%.*Lg" format specifier; using "%.*g" with a long double argument is
|
|
// undefined behavior and corrupts the output.
|
|
using long_double_json = nlohmann::basic_json<std::map, std::vector, std::string,
|
|
bool, std::int64_t, std::uint64_t, long double>;
|
|
|
|
SECTION("round-trip dump/parse")
|
|
{
|
|
constexpr std::array<long double, 13> values =
|
|
{
|
|
{
|
|
0.0L, -0.0L, 1.0L, -1.0L,
|
|
0.5L, -0.5L, 1.5L, -2.25L,
|
|
1.23e45L, 1.23e-45L,
|
|
(std::numeric_limits<long double>::min)(),
|
|
std::numeric_limits<long double>::lowest(),
|
|
(std::numeric_limits<long double>::max)()
|
|
}
|
|
};
|
|
|
|
for (long double v : values)
|
|
{
|
|
const long_double_json j = v;
|
|
const auto s = j.dump();
|
|
const auto j2 = long_double_json::parse(s);
|
|
CHECK(j2.template get<long double>() == v);
|
|
}
|
|
}
|
|
|
|
SECTION("exact dump string for simple values")
|
|
{
|
|
CHECK(long_double_json(0.5L).dump() == "0.5");
|
|
CHECK(long_double_json(-0.5L).dump() == "-0.5");
|
|
CHECK(long_double_json(1.5L).dump() == "1.5");
|
|
CHECK(long_double_json(-2.25L).dump() == "-2.25");
|
|
CHECK(long_double_json(0.0L).dump() == "0.0");
|
|
CHECK(long_double_json(1.0L).dump() == "1.0");
|
|
CHECK(long_double_json(-1.0L).dump() == "-1.0");
|
|
CHECK(long_double_json(100.0L).dump() == "100.0");
|
|
}
|
|
|
|
SECTION("NaN and infinity dump as null")
|
|
{
|
|
CHECK(long_double_json(std::numeric_limits<long double>::quiet_NaN()).dump() == "null");
|
|
|
|
// Probe the platform's runtime behavior — `volatile` forces a runtime
|
|
// call rather than constexpr-folding to a known answer at compile time.
|
|
// Skip the infinity assertions if std::isfinite() doesn't actually
|
|
// recognize long double infinity on this platform (notably, Valgrind
|
|
// 3.22's x87 80-bit emulation reports +/-inf as a large finite value).
|
|
// TODO(rusloker): remove this guard once Valgrind's 80-bit long double
|
|
// support ships (Valgrind bug https://bugs.kde.org/show_bug.cgi?id=197915,
|
|
// ASSIGNED since 2009 — the Valgrind project tracks its bugs on
|
|
// bugs.kde.org) and the minimum supported Valgrind version contains it.
|
|
const volatile long double inf_probe = std::numeric_limits<long double>::infinity();
|
|
if (!std::isfinite(inf_probe))
|
|
{
|
|
CHECK(long_double_json(std::numeric_limits<long double>::infinity()).dump() == "null");
|
|
CHECK(long_double_json(-std::numeric_limits<long double>::infinity()).dump() == "null");
|
|
}
|
|
}
|
|
|
|
SECTION("dump output matches double for exactly-representable values")
|
|
{
|
|
auto check_same = [](long double v_ld, double v_d)
|
|
{
|
|
const long_double_json j_ld = v_ld;
|
|
const json j_d = v_d;
|
|
CHECK(j_ld.dump() == j_d.dump());
|
|
};
|
|
|
|
check_same(0.0L, 0.0);
|
|
check_same(0.5L, 0.5);
|
|
check_same(-0.5L, -0.5);
|
|
check_same(1.5L, 1.5);
|
|
check_same(-2.25L, -2.25);
|
|
check_same(1.0L, 1.0);
|
|
check_same(100.0L, 100.0);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("serialization of strings (bulk fast path)")
|
|
{
|
|
// These cases exercise the SWAR bulk-copy fast path in dump_escaped and the
|
|
// internal write buffer: long runs, escapes interrupting runs, 0x7F/DEL,
|
|
// multibyte UTF-8 under both ensure_ascii settings, and payloads larger than
|
|
// the write buffer.
|
|
|
|
SECTION("long unescaped ASCII exceeds the write buffer")
|
|
{
|
|
const std::string big(3000, 'a');
|
|
const json j = big;
|
|
CHECK(j.dump() == '"' + big + '"');
|
|
CHECK(j.dump(-1, ' ', true) == '"' + big + '"');
|
|
// round-trips
|
|
CHECK(json::parse(j.dump()) == j);
|
|
}
|
|
|
|
SECTION("runs interrupted by escapes")
|
|
{
|
|
const json j = std::string(500, 'x') + "\n\"\\" + std::string(500, 'y');
|
|
const std::string out = j.dump();
|
|
CHECK(out == '"' + std::string(500, 'x') + "\\n\\\"\\\\" + std::string(500, 'y') + '"');
|
|
CHECK(json::parse(out) == j);
|
|
}
|
|
|
|
SECTION("DEL (0x7F) depends on ensure_ascii")
|
|
{
|
|
const json j = std::string("a\x7f" "b");
|
|
CHECK(j.dump(-1, ' ', false) == "\"a\x7f" "b\""); // copied verbatim
|
|
CHECK(j.dump(-1, ' ', true) == "\"a\\u007fb\""); // escaped
|
|
}
|
|
|
|
SECTION("multibyte UTF-8 under both ensure_ascii settings")
|
|
{
|
|
const json j = std::string("A\xc3\xa9\xe4\xbd\xa0\xf0\x9f\x98\x80Z"); // A é 你 😀 Z
|
|
// not escaping non-ASCII: bytes are copied through the bulk validator
|
|
CHECK(j.dump(-1, ' ', false) == "\"A\xc3\xa9\xe4\xbd\xa0\xf0\x9f\x98\x80Z\"");
|
|
// ensure_ascii: escaped (with a surrogate pair for the emoji)
|
|
CHECK(j.dump(-1, ' ', true) == "\"A\\u00e9\\u4f60\\ud83d\\ude00Z\"");
|
|
CHECK(json::parse(j.dump(-1, ' ', true)) == j);
|
|
}
|
|
|
|
SECTION("many small structural writes exceed the write buffer")
|
|
{
|
|
json arr = json::array();
|
|
for (int i = 0; i < 2000; ++i)
|
|
{
|
|
arr.push_back(i);
|
|
}
|
|
const std::string out = arr.dump();
|
|
CHECK(out.front() == '[');
|
|
CHECK(out.back() == ']');
|
|
CHECK(json::parse(out) == arr);
|
|
|
|
json obj = json::object();
|
|
for (int i = 0; i < 500; ++i)
|
|
{
|
|
obj["key" + std::to_string(i)] = i;
|
|
}
|
|
CHECK(json::parse(obj.dump()) == obj);
|
|
CHECK(json::parse(obj.dump(2)) == obj);
|
|
|
|
// an array of many empty strings emits a long run of single-character
|
|
// writes ('"', '"', ',') at shallow nesting depth, so the write buffer
|
|
// fills and flushes mid-run without the deep recursion that would
|
|
// overflow the stack on some debug builds
|
|
json many_empty = json::array();
|
|
for (int i = 0; i < 500; ++i)
|
|
{
|
|
many_empty.push_back("");
|
|
}
|
|
const std::string out2 = many_empty.dump();
|
|
CHECK(out2.size() > 1024); // spans multiple write-buffer flushes
|
|
CHECK(out2.front() == '[');
|
|
CHECK(out2.back() == ']');
|
|
CHECK(json::parse(out2) == many_empty);
|
|
}
|
|
|
|
SECTION("invalid UTF-8 handling is unaffected by the fast path")
|
|
{
|
|
const json j = std::string("valid\xff" "more");
|
|
CHECK_THROWS_WITH_AS(utils::ignore_return_value(j.dump()), "[json.exception.type_error.316] invalid UTF-8 byte at index 5: 0xFF", json::type_error&);
|
|
CHECK(j.dump(-1, ' ', false, json::error_handler_t::replace) == "\"valid\xef\xbf\xbd" "more\"");
|
|
CHECK(j.dump(-1, ' ', true, json::error_handler_t::replace) == "\"valid\\ufffdmore\"");
|
|
CHECK(j.dump(-1, ' ', false, json::error_handler_t::ignore) == "\"validmore\"");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("indentation is written straight into the write buffer")
|
|
{
|
|
// put_indent() memsets the indentation into the write buffer instead of
|
|
// copying it out of a pre-grown indentation string. These cases cover an
|
|
// indentation wider than the buffer, a non-space indentation character, and
|
|
// nesting deep enough that the accumulated indentation spans several
|
|
// buffer-fulls - the situations the old grow-a-string approach got wrong.
|
|
|
|
SECTION("indent_step wider than the write buffer")
|
|
{
|
|
const json j = {{"a", 1}};
|
|
// 2000 > the 1024-byte write buffer, and > the 512 the indentation
|
|
// string used to start at
|
|
CHECK(j.dump(2000) == "{\n" + std::string(2000, ' ') + "\"a\": 1\n}");
|
|
// several whole buffer-fulls, so the buffer is refilled once and then
|
|
// flushed repeatedly
|
|
CHECK(j.dump(5000) == "{\n" + std::string(5000, ' ') + "\"a\": 1\n}");
|
|
CHECK(j.dump(5000, '\t') == "{\n" + std::string(5000, '\t') + "\"a\": 1\n}");
|
|
// an exact multiple of the buffer size
|
|
CHECK(j.dump(4096) == "{\n" + std::string(4096, ' ') + "\"a\": 1\n}");
|
|
}
|
|
|
|
SECTION("a non-space indentation character is used throughout")
|
|
{
|
|
const json j = {{"a", 1}};
|
|
// 600 is past the point where the indentation used to be grown, which
|
|
// is where a hard-coded space would have shown up
|
|
CHECK(j.dump(600, '\t') == "{\n" + std::string(600, '\t') + "\"a\": 1\n}");
|
|
CHECK(j.dump(3, '.') == "{\n...\"a\": 1\n}");
|
|
}
|
|
|
|
SECTION("accumulated indentation spans several buffer-fulls")
|
|
{
|
|
// five levels deep at 400 per level: the innermost value is indented by
|
|
// 2000 characters, reached in steps that each straddle the buffer end
|
|
json j = json::array({1});
|
|
for (int i = 0; i < 4; ++i)
|
|
{
|
|
j = json::array({j});
|
|
}
|
|
|
|
const std::string out = j.dump(400);
|
|
CHECK(out.find(std::string("\n") + std::string(2000, ' ') + "1\n") != std::string::npos);
|
|
CHECK(json::parse(out) == j);
|
|
}
|
|
|
|
SECTION("binary values are indented the same way")
|
|
{
|
|
// a binary value is serialized as an object with "bytes" and
|
|
// "subtype" keys; the byte array itself is always written compactly
|
|
// (see dump_byte()), so only the surrounding object's indentation
|
|
// goes through put_indent()
|
|
const json j = json::binary({1, 2, 3}, 128);
|
|
CHECK(j.dump(2000) == "{\n" + std::string(2000, ' ') + "\"bytes\": [1, 2, 3],\n"
|
|
+ std::string(2000, ' ') + "\"subtype\": 128\n}");
|
|
CHECK(j.dump(2000, '\t') == "{\n" + std::string(2000, '\t') + "\"bytes\": [1, 2, 3],\n"
|
|
+ std::string(2000, '\t') + "\"subtype\": 128\n}");
|
|
}
|
|
|
|
SECTION("indentation is unchanged for ordinary widths")
|
|
{
|
|
const json j = {{"a", {1, 2}}, {"b", nullptr}};
|
|
CHECK(j.dump(2) == "{\n \"a\": [\n 1,\n 2\n ],\n \"b\": null\n}");
|
|
CHECK(j.dump(0) == "{\n\"a\": [\n1,\n2\n],\n\"b\": null\n}");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("serialization of deeply nested values")
|
|
{
|
|
// dump() descends into a bounded number of levels and writes out whatever
|
|
// is nested deeper than that without the call stack; see
|
|
// https://github.com/nlohmann/json/issues/5387
|
|
|
|
SECTION("nested deeper than the call stack could follow")
|
|
{
|
|
// parsing is iterative, so building these costs little
|
|
const std::size_t depth = 100000;
|
|
|
|
const std::string array_text = std::string(depth, '[') + '0' + std::string(depth, ']');
|
|
CHECK(json::parse(array_text).dump() == array_text);
|
|
|
|
std::string object_text;
|
|
object_text.reserve((6 * depth) + 1);
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
object_text += "{\"a\":";
|
|
}
|
|
object_text += '1';
|
|
object_text.append(depth, '}');
|
|
CHECK(json::parse(object_text).dump() == object_text);
|
|
}
|
|
|
|
SECTION("depths around the bound of the descent")
|
|
{
|
|
// Cover every depth around the bound, so that the two ways of writing a
|
|
// value are known to meet cleanly - wherever the bound is set.
|
|
for (std::size_t d = 1; d <= 300; ++d)
|
|
{
|
|
CAPTURE(d)
|
|
|
|
const std::string array_text = std::string(d, '[') + '7' + std::string(d, ']');
|
|
CHECK(json::parse(array_text).dump() == array_text);
|
|
|
|
std::string object_text;
|
|
for (std::size_t i = 0; i < d; ++i)
|
|
{
|
|
object_text += "{\"k\":";
|
|
}
|
|
object_text += '7';
|
|
object_text.append(d, '}');
|
|
CHECK(json::parse(object_text).dump() == object_text);
|
|
}
|
|
}
|
|
|
|
SECTION("pretty-printing across the bound")
|
|
{
|
|
for (std::size_t d = 120; d <= 140; ++d)
|
|
{
|
|
CAPTURE(d)
|
|
|
|
const json j = json::parse(std::string(d, '[') + '7' + std::string(d, ']'));
|
|
|
|
std::string expected;
|
|
for (std::size_t i = 0; i < d; ++i)
|
|
{
|
|
expected += std::string(2 * i, ' ') + "[\n";
|
|
}
|
|
expected += std::string(2 * d, ' ') + '7';
|
|
for (std::size_t i = d; i > 0; --i)
|
|
{
|
|
expected += '\n' + std::string(2 * (i - 1), ' ') + ']';
|
|
}
|
|
|
|
CHECK(j.dump(2) == expected);
|
|
}
|
|
}
|
|
|
|
SECTION("an empty container below the bound")
|
|
{
|
|
// an empty container is written out in full and never descended into,
|
|
// so it must not gain a newline when it is reached iteratively
|
|
for (std::size_t d = 125; d <= 135; ++d)
|
|
{
|
|
CAPTURE(d)
|
|
|
|
const std::string compact = std::string(d, '[') + "[]" + std::string(d, ']');
|
|
CHECK(json::parse(compact).dump() == compact);
|
|
|
|
const std::string with_object = std::string(d, '[') + "{}" + std::string(d, ']');
|
|
CHECK(json::parse(with_object).dump() == with_object);
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
// wraps @a inner into @a depth single-element arrays
|
|
json wrap_in_arrays(const json& inner, const std::size_t depth)
|
|
{
|
|
json j = inner;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
j = json::array({std::move(j)});
|
|
}
|
|
return j;
|
|
}
|
|
|
|
// what wrap_in_arrays(inner, depth).dump(2) is expected to be: the arrays
|
|
// around inner.dump(2), with inner's own lines indented by the depth
|
|
std::string expected_pretty_in_arrays(const json& inner, const std::size_t depth)
|
|
{
|
|
std::string expected;
|
|
for (std::size_t i = 0; i < depth; ++i)
|
|
{
|
|
expected += std::string(2 * i, ' ') + "[\n";
|
|
}
|
|
|
|
const std::string indent(2 * depth, ' ');
|
|
expected += indent;
|
|
for (const char c : inner.dump(2))
|
|
{
|
|
expected += c;
|
|
if (c == '\n')
|
|
{
|
|
expected += indent;
|
|
}
|
|
}
|
|
|
|
for (std::size_t i = depth; i > 0; --i)
|
|
{
|
|
expected += '\n' + std::string(2 * (i - 1), ' ') + ']';
|
|
}
|
|
return expected;
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("serialization of every kind of value below the bound of the descent")
|
|
{
|
|
// Values nested deeper than the bound are written without the call stack,
|
|
// by code of their own; each kind of value must come out the same there as
|
|
// it does at the top level, compact and pretty-printed.
|
|
std::vector<json> values =
|
|
{
|
|
json::parse(R"({"a": 1, "b": [1, 2, {"c": "x"}], "d": {}, "e": []})"),
|
|
json::parse(R"([1, [2, 3], {"k": null}, "s"])"),
|
|
json::object(),
|
|
json::array(),
|
|
json::binary({1, 2, 3}, 42),
|
|
json::binary({1, 2, 3}),
|
|
json::binary({}, 7),
|
|
json::binary({}),
|
|
"a string with \"escapes\"\n",
|
|
true,
|
|
false,
|
|
-42,
|
|
42u,
|
|
1.5,
|
|
nullptr,
|
|
json(json::value_t::discarded),
|
|
};
|
|
// a pretty-printed object whose members are themselves deep
|
|
values.push_back({{"x", wrap_in_arrays(1, 5)}, {"y", {{"z", 2}}}});
|
|
|
|
for (const std::size_t depth : std::vector<std::size_t> {1, 200})
|
|
{
|
|
CAPTURE(depth)
|
|
for (const auto& inner : values)
|
|
{
|
|
CAPTURE(inner.dump())
|
|
const json j = wrap_in_arrays(inner, depth);
|
|
CHECK(j.dump() == std::string(depth, '[') + inner.dump() + std::string(depth, ']'));
|
|
CHECK(j.dump(2) == expected_pretty_in_arrays(inner, depth));
|
|
}
|
|
}
|
|
|
|
SECTION("pretty-printed objects across the bound")
|
|
{
|
|
for (std::size_t d = 120; d <= 140; ++d)
|
|
{
|
|
CAPTURE(d)
|
|
|
|
// built from the inside out: {"k": <level below>, "n": <level>}
|
|
json j = 7;
|
|
std::string expected = "7";
|
|
for (std::size_t i = d; i > 0; --i)
|
|
{
|
|
j = json({{"k", std::move(j)}, {"n", i}});
|
|
|
|
const std::string indent(2 * i, ' ');
|
|
const std::string outer_indent(2 * (i - 1), ' ');
|
|
std::string next = "{\n";
|
|
next += indent;
|
|
next += "\"k\": ";
|
|
next += expected;
|
|
next += ",\n";
|
|
next += indent;
|
|
next += "\"n\": ";
|
|
next += std::to_string(i);
|
|
next += '\n';
|
|
next += outer_indent;
|
|
next += '}';
|
|
expected = std::move(next);
|
|
}
|
|
|
|
CHECK(j.dump(2) == expected);
|
|
CHECK(json::parse(j.dump(2)) == j);
|
|
CHECK(json::parse(j.dump()) == j);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE("serializer buffers are flushed mid-string and mid-binary")
|
|
{
|
|
SECTION("a long run of escaped characters")
|
|
{
|
|
// each character is escaped on its own, so the escape buffer fills up
|
|
const json newlines = std::string(600, '\n');
|
|
std::string expected = "\"";
|
|
for (int i = 0; i < 600; ++i)
|
|
{
|
|
expected += "\\n";
|
|
}
|
|
expected += '"';
|
|
CHECK(newlines.dump() == expected);
|
|
|
|
// every character is \u-escaped under ensure_ascii
|
|
std::string umlauts;
|
|
std::string escaped_umlauts = "\"";
|
|
for (int i = 0; i < 300; ++i)
|
|
{
|
|
umlauts += "\xC3\xA4";
|
|
escaped_umlauts += "\\u00e4";
|
|
}
|
|
escaped_umlauts += '"';
|
|
CHECK(json(umlauts).dump(-1, ' ', true) == escaped_umlauts);
|
|
}
|
|
|
|
SECTION("a large binary value")
|
|
{
|
|
std::vector<std::uint8_t> bytes(3000);
|
|
std::string expected_bytes;
|
|
std::string expected_pretty_bytes;
|
|
for (std::size_t i = 0; i < bytes.size(); ++i)
|
|
{
|
|
bytes[i] = static_cast<std::uint8_t>(i % 256);
|
|
expected_bytes += (i == 0 ? "" : ",") + std::to_string(i % 256);
|
|
expected_pretty_bytes += (i == 0 ? "" : ", ") + std::to_string(i % 256);
|
|
}
|
|
const json j = json::binary(bytes);
|
|
CHECK(j.dump() == "{\"bytes\":[" + expected_bytes + "],\"subtype\":null}");
|
|
CHECK(j.dump(2) == "{\n \"bytes\": [" + expected_pretty_bytes + "],\n \"subtype\": null\n}");
|
|
}
|
|
}
|