| // __ _____ _____ _____ |
| // __| | __| | | | 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 |
| |
| // cmake/test.cmake selects the C++ standard versions with which to build a |
| // unit test based on the presence of JSON_HAS_CPP_<VERSION> macros. |
| // When using macros that are only defined for particular versions of the standard |
| // (e.g., JSON_HAS_FILESYSTEM for C++17 and up), please mention the corresponding |
| // version macro in a comment close by, like this: |
| // JSON_HAS_CPP_<VERSION> (do not remove; see note at top of file) |
| |
| #include "doctest_compatibility.h" |
| |
| #include <nlohmann/json.hpp> |
| using nlohmann::json; |
| |
| #include <array> // array |
| #include <cstddef> // size_t |
| #include <cstdint> // uint8_t |
| #include <list> |
| #include <string> // string |
| #include <vector> // vector |
| |
| #if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20) |
| #include <iterator> |
| #endif |
| |
| namespace |
| { |
| TEST_CASE("Use arbitrary stdlib container") |
| { |
| std::string raw_data = "[1,2,3,4]"; |
| std::list<char> data(raw_data.begin(), raw_data.end()); |
| |
| json as_json = json::parse(data.begin(), data.end()); |
| CHECK(as_json.at(0) == 1); |
| CHECK(as_json.at(1) == 2); |
| CHECK(as_json.at(2) == 3); |
| CHECK(as_json.at(3) == 4); |
| } |
| |
| struct MyContainer |
| { |
| const char* data; |
| }; |
| |
| const char* begin(const MyContainer& c) |
| { |
| return c.data; |
| } |
| |
| const char* end(const MyContainer& c) |
| { |
| return c.data + strlen(c.data); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) |
| } |
| |
| TEST_CASE("Custom container non-member begin/end") |
| { |
| |
| const MyContainer data{"[1,2,3,4]"}; |
| json as_json = json::parse(data); |
| CHECK(as_json.at(0) == 1); |
| CHECK(as_json.at(1) == 2); |
| CHECK(as_json.at(2) == 3); |
| CHECK(as_json.at(3) == 4); |
| |
| } |
| |
| struct MyContainerNonConstADL |
| { |
| char* data; |
| std::size_t size; |
| }; |
| |
| char* begin(MyContainerNonConstADL& c) |
| { |
| return c.data; |
| } |
| |
| char* end(MyContainerNonConstADL& c) |
| { |
| return c.data + c.size; // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) |
| } |
| |
| TEST_CASE("Custom container non-member non-const begin/end") |
| { |
| // Container with lvalue-only non-const ADL begin/end (bug reproduction) |
| std::string raw_data = "[1,2,3,4]"; |
| MyContainerNonConstADL data{&raw_data[0], raw_data.size()}; // NOLINT(readability-container-data-pointer) |
| const json as_json = json::parse(data); |
| CHECK(as_json.at(0) == 1); |
| CHECK(as_json.at(1) == 2); |
| CHECK(as_json.at(2) == 3); |
| CHECK(as_json.at(3) == 4); |
| |
| // Same container with accept() |
| CHECK(json::accept(data)); |
| } |
| |
| TEST_CASE("Custom container non-member begin/end, rvalue") |
| { |
| // Regression check: rvalue container parsing should still work |
| const json as_json = json::parse(MyContainer{"[1,2,3,4]"}); |
| CHECK(as_json.at(0) == 1); |
| CHECK(as_json.at(1) == 2); |
| CHECK(as_json.at(2) == 3); |
| CHECK(as_json.at(3) == 4); |
| } |
| |
| TEST_CASE("Custom container member begin/end") |
| { |
| struct MyContainer2 |
| { |
| const char* data; |
| |
| const char* begin() const noexcept |
| { |
| return data; |
| } |
| |
| const char* end() const noexcept |
| { |
| return data + strlen(data); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) |
| } |
| }; |
| |
| const MyContainer2 data{"[1,2,3,4]"}; |
| json as_json = json::parse(data); |
| CHECK(as_json.at(0) == 1); |
| CHECK(as_json.at(1) == 2); |
| CHECK(as_json.at(2) == 3); |
| CHECK(as_json.at(3) == 4); |
| } |
| |
| TEST_CASE("Custom iterator") |
| { |
| const char* raw_data = "[1,2,3,4]"; |
| |
| struct MyIterator |
| { |
| using difference_type = std::size_t; |
| using value_type = char; |
| using pointer = const char*; |
| using reference = const char&; |
| using iterator_category = std::input_iterator_tag; |
| |
| MyIterator& operator++() |
| { |
| ++ptr; // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) |
| return *this; |
| } |
| |
| reference operator*() const |
| { |
| return *ptr; |
| } |
| |
| bool operator!=(const MyIterator& rhs) const |
| { |
| return ptr != rhs.ptr; |
| } |
| |
| const char* ptr; |
| }; |
| |
| // avoid -Wunused-local-typedefs |
| CHECK(std::is_same<MyIterator::difference_type, std::size_t>::value); |
| CHECK(std::is_same<MyIterator::value_type, char>::value); |
| CHECK(std::is_same<MyIterator::pointer, const char*>::value); |
| CHECK(std::is_same<MyIterator::reference, const char&>::value); |
| CHECK(std::is_same<MyIterator::iterator_category, std::input_iterator_tag>::value); |
| |
| const MyIterator begin{raw_data}; |
| const MyIterator end{raw_data + strlen(raw_data)}; // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) |
| |
| json as_json = json::parse(begin, end); |
| CHECK(as_json.at(0) == 1); |
| CHECK(as_json.at(1) == 2); |
| CHECK(as_json.at(2) == 3); |
| CHECK(as_json.at(3) == 4); |
| } |
| |
| // Custom sentinel type for testing heterogeneous iterator+sentinel support |
| struct CustomSentinel |
| { |
| const char* end_ptr; |
| |
| // only the iterator-first direction (it != sentinel) is ever evaluated by |
| // the library's parse loop; a reversed-order overload would go unused and |
| // trip -Wunneeded-internal-declaration under -Weverything |
| friend bool operator!=(const char* it, const CustomSentinel& sentinel) |
| { |
| return it != sentinel.end_ptr; |
| } |
| }; |
| |
| TEST_CASE("Parse with heterogeneous iterator and sentinel types") |
| { |
| const std::string json_str = R"({"key":"value"})"; |
| const char* end_ptr = json_str.data() + json_str.size(); |
| |
| // Parse using pointer and sentinel (different types) |
| json j = json::parse(json_str.data(), CustomSentinel{end_ptr}); |
| CHECK(j["key"] == "value"); |
| |
| // Accept using pointer and sentinel |
| CHECK(json::accept(json_str.data(), CustomSentinel{end_ptr})); |
| |
| // Test that the same-type case still works |
| std::string raw_data = R"([1,2,3])"; |
| std::list<char> data(raw_data.begin(), raw_data.end()); |
| json j2 = json::parse(data.begin(), data.end()); |
| CHECK(j2.at(0) == 1); |
| } |
| |
| // A type whose data() hands out raw bytes but whose size() counts something |
| // else - here fixed-size records. Reading [data(), data() + size()) as bytes |
| // would silently truncate the input, so data() and size() alone must not be |
| // taken as evidence of contiguous byte storage. |
| struct record_buffer |
| { |
| using value_type = std::array<char, 4>; |
| |
| std::string bytes; |
| |
| const char* data() const noexcept |
| { |
| return bytes.data(); |
| } |
| std::size_t size() const noexcept |
| { |
| return bytes.size() / sizeof(value_type); |
| } |
| const char* begin() const noexcept |
| { |
| return bytes.data(); |
| } |
| const char* end() const noexcept |
| { |
| return bytes.data() + bytes.size(); |
| } |
| }; |
| |
| TEST_CASE("Contiguous byte containers take the pointer adapter") |
| { |
| // Containers with contiguous single-byte storage are routed through the |
| // pointer-based adapter so the bulk fast paths apply in every standard, not |
| // only in C++20 where the library iterators model std::contiguous_iterator. |
| CHECK(nlohmann::detail::is_contiguous_byte_container<std::string>::value); |
| CHECK(nlohmann::detail::is_contiguous_byte_container<std::vector<char>>::value); |
| CHECK(nlohmann::detail::is_contiguous_byte_container<std::vector<std::uint8_t>>::value); |
| CHECK(nlohmann::detail::is_contiguous_byte_container<std::array<char, 4>>::value); |
| |
| // input_adapter() takes its container by forwarding reference, so the trait |
| // is also asked about reference types |
| CHECK(nlohmann::detail::is_contiguous_byte_container<std::string&>::value); |
| CHECK(nlohmann::detail::is_contiguous_byte_container<const std::string&>::value); |
| |
| // everything else keeps the iterator-based adapter |
| CHECK_FALSE(nlohmann::detail::is_contiguous_byte_container<std::list<char>>::value); |
| CHECK_FALSE(nlohmann::detail::is_contiguous_byte_container<std::vector<int>>::value); |
| CHECK_FALSE(nlohmann::detail::is_contiguous_byte_container<const char*>::value); |
| |
| // including a type that has data() and size() but whose size() does not |
| // count the units data() points at: its value_type says so |
| CHECK_FALSE(nlohmann::detail::is_contiguous_byte_container<record_buffer>::value); |
| |
| // and such a container still parses through its iterators, in full - taking |
| // it for a byte container would stop after data() + size() bytes |
| const record_buffer buffer{"[1,2,3,4,5]"}; |
| CHECK(buffer.data() == buffer.bytes.data()); |
| CHECK(buffer.size() * sizeof(record_buffer::value_type) < buffer.bytes.size()); |
| CHECK(json::parse(buffer) == json({1, 2, 3, 4, 5})); |
| } |
| |
| #if defined(__cpp_lib_concepts) && defined(JSON_HAS_CPP_20) |
| // JSON_HAS_CPP_20 (do not remove; see note at top of file) |
| TEST_CASE("Parse with std::counted_iterator and std::default_sentinel_t") |
| { |
| using iterator_type = std::string::const_iterator; |
| const std::string json_str = R"({"key":"value","array":[1,2,3]})"; |
| const auto len = static_cast<std::iter_difference_t<iterator_type>>(json_str.size()); |
| |
| const std::counted_iterator<iterator_type> first(json_str.begin(), len); |
| const json j = json::parse(first, std::default_sentinel); |
| CHECK(j["key"] == "value"); |
| CHECK(j["array"].size() == 3); |
| |
| const std::counted_iterator<iterator_type> first2(json_str.begin(), len); |
| CHECK(json::accept(first2, std::default_sentinel)); |
| } |
| |
| TEST_CASE("std::counted_iterator reaches the contiguous fast paths") |
| { |
| // A sized sentinel makes the remaining element count computable in O(1), so |
| // std::counted_iterator over a contiguous iterator must reach the same bulk |
| // string/number scanners as a plain pointer - not just the byte-at-a-time |
| // fallback (see #5268 for the equivalent memcpy fast path). |
| #if JSON_HAS_RANGES |
| // JSON_HAS_RANGES is 0 on standard libraries with an incomplete <ranges> |
| // (libstdc++ < 11, libc++ < 16), where the adapter deliberately falls back |
| // to the byte-at-a-time scanner; everything below still has to work there. |
| using adapter_type = nlohmann::detail::iterator_input_adapter<std::counted_iterator<const char*>, std::default_sentinel_t>; |
| CHECK(adapter_type::supports_bulk_scan); |
| CHECK(adapter_type::supports_seek); |
| #endif |
| |
| // exercise every fast path: long ASCII run, multibyte UTF-8, escapes, and |
| // integer/floating-point numbers |
| const std::string json_str = |
| R"({"ascii":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",)" |
| "\"utf8\":\"\xe4\xb8\xad\xe6\x96\x87\xf0\x9f\x98\x80\xc3\xa9\"," |
| R"("escaped":"aéb\n\\","ints":[0,-1,18446744073709551615,-9223372036854775808],)" |
| R"("floats":[1.5,-2.25e3,0.30000000000000004]})"; |
| const auto len = static_cast<std::iter_difference_t<const char*>>(json_str.size()); |
| |
| const std::counted_iterator<const char*> first(json_str.data(), len); |
| const json j = json::parse(first, std::default_sentinel); |
| |
| // parsing through the pointer adapter must give exactly the same result |
| CHECK(j == json::parse(json_str)); |
| |
| #if !defined(JSON_NOEXCEPTION) |
| // Diagnostics that quote the offending token are reconstructed from the |
| // already-consumed input (supports_seek), a path a sized sentinel only |
| // reaches now; check a few that include the "last read" text. Parsing |
| // invalid input aborts when exceptions are off, hence the guard. |
| // Raw strings and explicit bytes: an escaped literal and two literals |
| // written next to each other both read as mistakes to static analysis. |
| const auto byte = [](int value) |
| { |
| return std::string(1, static_cast<char>(value)); |
| }; |
| const std::vector<std::string> diagnostic_docs = |
| { |
| "1\nx", |
| "truX", |
| "[tru]", |
| R"("abc)", |
| R"(["\ud834"])", |
| R"(["a)" + byte(0x01) + R"(b"])", |
| R"([")" + byte(0xC3) + byte(0x28) + R"("])", |
| "[1e]", |
| R"(["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX)" |
| }; |
| |
| for (const auto& text : diagnostic_docs) |
| { |
| CAPTURE(text); |
| const std::counted_iterator<const char*> it(text.data(), static_cast<std::iter_difference_t<const char*>>(text.size())); |
| std::string counted_message; |
| std::string string_message; |
| try |
| { |
| const json counted_result = json::parse(it, std::default_sentinel); |
| static_cast<void>(counted_result); |
| } |
| catch (const json::parse_error& e) |
| { |
| counted_message = e.what(); |
| } |
| try |
| { |
| const json string_result = json::parse(text); |
| static_cast<void>(string_result); |
| } |
| catch (const json::parse_error& e) |
| { |
| string_message = e.what(); |
| } |
| CHECK_FALSE(counted_message.empty()); |
| CHECK(counted_message == string_message); |
| } |
| |
| // and errors must still be reported identically |
| const std::string bad = "[01\n]"; |
| const std::counted_iterator<const char*> bad_first(bad.data(), static_cast<std::iter_difference_t<const char*>>(bad.size())); |
| std::string counted_what; |
| std::string string_what; |
| try |
| { |
| const json counted_result = json::parse(bad_first, std::default_sentinel); |
| static_cast<void>(counted_result); |
| } |
| catch (const json::parse_error& e) |
| { |
| counted_what = e.what(); |
| } |
| try |
| { |
| const json string_result = json::parse(bad); |
| static_cast<void>(string_result); |
| } |
| catch (const json::parse_error& e) |
| { |
| string_what = e.what(); |
| } |
| CHECK_FALSE(counted_what.empty()); |
| CHECK(counted_what == string_what); |
| #endif |
| } |
| |
| #if !defined(JSON_NOEXCEPTION) |
| // several cases below are truncated on purpose, and parsing invalid input |
| // aborts when exceptions are off |
| TEST_CASE("std::counted_iterator bulk scanning stops at the counted end") |
| { |
| // The count, not the size of the underlying buffer, is the end of the |
| // input: the bulk scanners must never look at the bytes behind it, even |
| // though they are readable. Each case is compared against parsing the |
| // equivalent prefix as a std::string. |
| const auto via_counted = [](const std::string & buf, std::size_t n) -> std::string |
| { |
| const std::counted_iterator<const char*> first(buf.data(), static_cast<std::iter_difference_t<const char*>>(n)); |
| try |
| { |
| const json j = json::parse(first, std::default_sentinel); |
| return "OK|" + j.dump(); |
| } |
| catch (const json::parse_error& e) |
| { |
| return {e.what()}; |
| } |
| }; |
| const auto via_prefix = [](const std::string & buf, std::size_t n) -> std::string |
| { |
| try |
| { |
| const json j = json::parse(buf.substr(0, n)); |
| return "OK|" + j.dump(); |
| } |
| catch (const json::parse_error& e) |
| { |
| return {e.what()}; |
| } |
| }; |
| |
| struct testcase // NOLINT(cppcoreguidelines-pro-type-member-init,hicpp-member-init) |
| { |
| const char* buffer; |
| std::size_t count; |
| }; |
| const std::vector<testcase> cases = |
| { |
| {"[\"abc\"]____TRAILING____", 7}, // exact fit, tail hidden |
| {"[\"abcdefghijklmnop\"]____", 8}, // cut inside a string |
| {"[\"abc\"]____", 6}, // cut just before the closing quote |
| {"[12345]xxxxx", 4}, // cut inside a number |
| {"[123]999999", 5}, // number ends exactly at the count |
| {"[\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"]", 12}, // closing quote only behind the count |
| {"[\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"]", 19}, // cut inside an 8-byte SWAR stride |
| {"[\"\xe4\xb8\xad\xe6\x96\x87\"]", 5}, // cut inside a UTF-8 sequence |
| {"[\"\xe4\xb8\xad\xe6\x96\x87\"]____", 10}, // complete UTF-8, tail hidden |
| {"[1.25e3]TRAILINGDIGITS999", 7}, // number token reaches the count |
| }; |
| |
| for (const auto& tc : cases) |
| { |
| CAPTURE(tc.buffer); |
| CAPTURE(tc.count); |
| const std::string buffer = tc.buffer; |
| CHECK(via_counted(buffer, tc.count) == via_prefix(buffer, tc.count)); |
| } |
| } |
| #endif |
| #endif |
| |
| } // namespace |