Skip to content

use diagnostic positions in exceptions #4585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/examples/diagnostic_positions_exception.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include <iostream>

#define JSON_DIAGNOSTIC_POSITIONS 1
#include <nlohmann/json.hpp>

using json = nlohmann::json;

/* Demonstration of type error exception with diagnostic postions support enabled */
int main()
{
//Invalid json string - housenumber type must be int instead of string
std::string json_invalid_string = R"(
{
"address": {
"street": "Fake Street",
"housenumber": "1"
}
}
)";
json j = json::parse(json_invalid_string);
try
{
int housenumber = j["address"]["housenumber"];

Check notice on line 23 in docs/examples/diagnostic_positions_exception.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/examples/diagnostic_positions_exception.cpp#L23

Variable 'housenumber' is assigned a value that is never used.
}
catch (const json::exception& e)
{
std::cout << e.what() << '\n';
}
}
1 change: 1 addition & 0 deletions docs/examples/diagnostic_positions_exception.output
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[json.exception.type_error.302] (bytes 92-95) type must be number, but is string
30 changes: 30 additions & 0 deletions docs/examples/diagnostics_extended_positions.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#include <iostream>

#define JSON_DIAGNOSTICS 1
#define JSON_DIAGNOSTIC_POSITIONS 1
#include <nlohmann/json.hpp>

using json = nlohmann::json;

/* Demonstration of type error exception with diagnostic postions support enabled */
int main()
{
//Invalid json string - housenumber type must be int instead of string
std::string json_invalid_string = R"(
{
"address": {
"street": "Fake Street",
"housenumber": "1"
}
}
)";
json j = json::parse(json_invalid_string);
try
{
int housenumber = j["address"]["housenumber"];

Check notice on line 24 in docs/examples/diagnostics_extended_positions.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

docs/examples/diagnostics_extended_positions.cpp#L24

Variable 'housenumber' is assigned a value that is never used.
}
catch (const json::exception& e)
{
std::cout << e.what() << '\n';
}
}
1 change: 1 addition & 0 deletions docs/examples/diagnostics_extended_positions.output
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[json.exception.type_error.302] (/address/housenumber) (bytes 92-95) type must be number, but is string
27 changes: 26 additions & 1 deletion docs/mkdocs/docs/api/macros/json_diagnostic_positions.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,33 @@ When the macro is not defined, the library will define it to its default value.
```
--8<-- "examples/diagnostic_positions.output"
```

The output shows the start/end positions of all the objects and fields in the JSON string.

??? example "Example 2: using extended diagnostics with positions enabled in exceptions"

```cpp
--8<-- "examples/diagnostics_extended_positions.cpp"
```

Output:

```
--8<-- "examples/diagnostics_extended_positions.output"
```
The output shows the exception with diagnostic path info and start/end positions.

??? example "Example 3: using only diagnostic positions in exceptions"

```cpp
--8<-- "examples/diagnostic_positions_exception.cpp"
```

Output:

```
--8<-- "examples/diagnostic_positions_exception.output"
```
The output shows the exception with start/end positions only.

## Version history

20 changes: 17 additions & 3 deletions include/nlohmann/detail/exceptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,30 @@ class exception : public std::exception
{
return concat(a, '/', detail::escape(b));
});
return concat('(', str, ") ");

return concat('(', str, ") ") + get_byte_positions(leaf_element);
#else
static_cast<void>(leaf_element);
return "";
return get_byte_positions(leaf_element);
#endif
}

private:
/// an exception object as storage for error messages
std::runtime_error m;
template<typename BasicJsonType>
static std::string get_byte_positions(const BasicJsonType* leaf_element)
{
#if JSON_DIAGNOSTIC_POSITIONS
if ((leaf_element->start_pos() != std::string::npos) && (leaf_element->end_pos() != std::string::npos))
{
return concat('(', "bytes ", std::to_string(leaf_element->start_pos()), "-", std::to_string(leaf_element->end_pos()), ") ");
}
return "";
#else
static_cast<void>(leaf_element);
return "";
#endif
}
};

/// @brief exception indicating a parse error
Expand Down
20 changes: 17 additions & 3 deletions single_include/nlohmann/json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4534,16 +4534,30 @@ class exception : public std::exception
{
return concat(a, '/', detail::escape(b));
});
return concat('(', str, ") ");

return concat('(', str, ") ") + get_byte_positions(leaf_element);
#else
static_cast<void>(leaf_element);
return "";
return get_byte_positions(leaf_element);
#endif
}

private:
/// an exception object as storage for error messages
std::runtime_error m;
template<typename BasicJsonType>
static std::string get_byte_positions(const BasicJsonType* leaf_element)
{
#if JSON_DIAGNOSTIC_POSITIONS
if ((leaf_element->start_pos() != std::string::npos) && (leaf_element->end_pos() != std::string::npos))
{
return concat('(', "bytes ", std::to_string(leaf_element->start_pos()), "-", std::to_string(leaf_element->end_pos()), ") ");
}
return "";
#else
static_cast<void>(leaf_element);
return "";
#endif
}
};

/// @brief exception indicating a parse error
Expand Down
17 changes: 16 additions & 1 deletion tests/src/unit-json_patch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,20 @@ TEST_CASE("JSON patch")
json const doc2 = R"({ "q": { "bar": 2 } })"_json;

// because "a" does not exist.
#if JSON_DIAGNOSTIC_POSITIONS
CHECK_THROWS_WITH_AS(doc2.patch(patch1), "[json.exception.out_of_range.403] (bytes 0-21) key 'a' not found", json::out_of_range&);
#else
CHECK_THROWS_WITH_AS(doc2.patch(patch1), "[json.exception.out_of_range.403] key 'a' not found", json::out_of_range&);
#endif

json const doc3 = R"({ "a": {} })"_json;
json const patch2 = R"([{ "op": "add", "path": "/a/b/c", "value": 1 }])"_json;

// should cause an error because "b" does not exist in doc3
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] (/a) key 'b' not found", json::out_of_range&);
#elif JSON_DIAGNOSTIC_POSITIONS
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] (bytes 7-9) key 'b' not found", json::out_of_range&);
#else
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] key 'b' not found", json::out_of_range&);
#endif
Expand Down Expand Up @@ -333,6 +339,8 @@ TEST_CASE("JSON patch")
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
#elif JSON_DIAGNOSTIC_POSITIONS
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-95) unsuccessful: " + patch[0].dump());
#else
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
#endif
Expand Down Expand Up @@ -417,8 +425,11 @@ TEST_CASE("JSON patch")
// applied), because the "add" operation's target location that
// references neither the root of the document, nor a member of
// an existing object, nor a member of an existing array.

#if JSON_DIAGNOSTIC_POSITIONS
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.403] (bytes 21-37) key 'baz' not found", json::out_of_range&);
#else
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
#endif
}

// A.13. Invalid JSON Patch Document
Expand Down Expand Up @@ -476,6 +487,8 @@ TEST_CASE("JSON patch")
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
#elif JSON_DIAGNOSTIC_POSITIONS
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-92) unsuccessful: " + patch[0].dump());
#else
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
#endif
Expand Down Expand Up @@ -1205,6 +1218,8 @@ TEST_CASE("JSON patch")
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
#if JSON_DIAGNOSTICS
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
#elif JSON_DIAGNOSTIC_POSITIONS
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-117) unsuccessful: " + patch[0].dump());
#else
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
#endif
Expand Down
8 changes: 6 additions & 2 deletions tests/src/unit-json_pointer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,15 @@ TEST_CASE("JSON pointers")
// escaped access
CHECK(j[json::json_pointer("/a~1b")] == j["a/b"]);
CHECK(j[json::json_pointer("/m~0n")] == j["m~n"]);

#if JSON_DIAGNOSTIC_POSITIONS
// unescaped access
CHECK_THROWS_WITH_AS(j.at(json::json_pointer("/a/b")),
"[json.exception.out_of_range.403] (bytes 13-297) key 'a' not found", json::out_of_range&);
#else
// unescaped access
CHECK_THROWS_WITH_AS(j.at(json::json_pointer("/a/b")),
"[json.exception.out_of_range.403] key 'a' not found", json::out_of_range&);

#endif
// unresolved access
const json j_primitive = 1;
CHECK_THROWS_WITH_AS(j_primitive["/foo"_json_pointer],
Expand Down
10 changes: 10 additions & 0 deletions tests/src/unit-regression1.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1400,14 +1400,24 @@ TEST_CASE("regression tests 1")
auto p1 = R"([{"op": "move",
"from": "/one/two/three",
"path": "/a/b/c"}])"_json;
#if JSON_DIAGNOSTIC_POSITIONS
CHECK_THROWS_WITH_AS(model.patch(p1),
"[json.exception.out_of_range.403] (bytes 0-158) key 'a' not found", json::out_of_range&);
#else
CHECK_THROWS_WITH_AS(model.patch(p1),
"[json.exception.out_of_range.403] key 'a' not found", json::out_of_range&);
#endif

auto p2 = R"([{"op": "copy",
"from": "/one/two/three",
"path": "/a/b/c"}])"_json;
#if JSON_DIAGNOSTIC_POSITIONS
CHECK_THROWS_WITH_AS(model.patch(p2),
"[json.exception.out_of_range.403] (bytes 0-158) key 'a' not found", json::out_of_range&);
#else
CHECK_THROWS_WITH_AS(model.patch(p2),
"[json.exception.out_of_range.403] key 'a' not found", json::out_of_range&);
#endif
}

SECTION("issue #961 - incorrect parsing of indefinite length CBOR strings")
Expand Down
Loading