Skip to content

Commit d23291b

Browse files
authored
use diagnostic positions in exceptions (nlohmann#4585)
* add a ci step for Json_Diagnostic_Positions Signed-off-by: Harinath Nampally <[email protected]> * Update ci.cmake to address review comments Signed-off-by: Harinath Nampally <[email protected]> * address review comment Signed-off-by: Harinath Nampally <[email protected]> * fix typo in the comment Signed-off-by: Harinath Nampally <[email protected]> * fix typos in ci.cmake Signed-off-by: Harinath Nampally <[email protected]> * invoke the new ci step from ubuntu.yml Signed-off-by: Harinath Nampally <[email protected]> * issue4561 - use diagnostic positions for exceptions Signed-off-by: Harinath Nampally <[email protected]> * fix ci_test_documentation check Signed-off-by: Harinath Nampally <[email protected]> * address review comments Signed-off-by: Harinath Nampally <[email protected]> * fix ci check failures for unit-diagnostic-postions.cpp Signed-off-by: Harinath Nampally <[email protected]> * improvements based on review comments Signed-off-by: Harinath Nampally <[email protected]> * fix const correctness string Signed-off-by: Harinath Nampally <[email protected]> * further refinements based on reviews Signed-off-by: Harinath Nampally <[email protected]> * add one more test case for full coverage Signed-off-by: Harinath Nampally <[email protected]> * ci check fix - add const Signed-off-by: Harinath Nampally <[email protected]> * add unit tests for json_diagnostic_postions only Signed-off-by: Harinath Nampally <[email protected]> * fix ci_test_diagnostics Signed-off-by: Harinath Nampally <[email protected]> * fix ci_test_build_documentation check Signed-off-by: Harinath Nampally <[email protected]> --------- Signed-off-by: Harinath Nampally <[email protected]>
1 parent 0f9e6ae commit d23291b

13 files changed

+261
-9
lines changed

docs/mkdocs/docs/api/macros/json_diagnostic_positions.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,33 @@ When the macro is not defined, the library will define it to its default value.
8181

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

84+
??? example "Example 2: using only diagnostic positions in exceptions"
85+
86+
```cpp
87+
--8<-- "examples/diagnostic_positions_exception.cpp"
88+
```
89+
90+
Output:
91+
92+
```
93+
--8<-- "examples/diagnostic_positions_exception.output"
94+
```
95+
96+
The output shows the exception with start/end positions only.
97+
98+
??? example "Example 3: using extended diagnostics with positions enabled in exceptions"
99+
100+
```cpp
101+
--8<-- "examples/diagnostics_extended_positions.cpp"
102+
```
103+
104+
Output:
105+
106+
```
107+
--8<-- "examples/diagnostics_extended_positions.output"
108+
```
109+
110+
The output shows the exception with diagnostic path info and start/end positions.
84111
## Version history
85112

86113
- Added in version 3.12.0.

docs/mkdocs/docs/api/macros/json_diagnostics.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ When the macro is not defined, the library will define it to its default value.
7070

7171
Now the exception message contains a JSON Pointer `/address/housenumber` that indicates which value has the wrong type.
7272

73+
??? example "Example 3: using only diagnostic positions in exceptions"
74+
75+
```cpp
76+
--8<-- "examples/diagnostic_positions_exception.cpp"
77+
```
78+
79+
Output:
80+
81+
```
82+
--8<-- "examples/diagnostic_positions_exception.output"
83+
```
84+
The output shows the exception with start/end positions only.
85+
7386
## Version history
7487

7588
- Added in version 3.10.0.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#include <iostream>
2+
3+
#define JSON_DIAGNOSTIC_POSITIONS 1
4+
#include <nlohmann/json.hpp>
5+
6+
using json = nlohmann::json;
7+
8+
/* Demonstration of type error exception with diagnostic postions support enabled */
9+
int main()
10+
{
11+
//Invalid json string - housenumber type must be int instead of string
12+
const std::string json_invalid_string = R"(
13+
{
14+
"address": {
15+
"street": "Fake Street",
16+
"housenumber": "1"
17+
}
18+
}
19+
)";
20+
json j = json::parse(json_invalid_string);
21+
try
22+
{
23+
int housenumber = j["address"]["housenumber"];
24+
std::cout << housenumber;
25+
}
26+
catch (const json::exception& e)
27+
{
28+
std::cout << e.what() << '\n';
29+
}
30+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[json.exception.type_error.302] (bytes 92-95) type must be number, but is string
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#include <iostream>
2+
3+
#define JSON_DIAGNOSTICS 1
4+
#define JSON_DIAGNOSTIC_POSITIONS 1
5+
#include <nlohmann/json.hpp>
6+
7+
using json = nlohmann::json;
8+
9+
/* Demonstration of type error exception with diagnostic postions support enabled */
10+
int main()
11+
{
12+
//Invalid json string - housenumber type must be int instead of string
13+
const std::string json_invalid_string = R"(
14+
{
15+
"address": {
16+
"street": "Fake Street",
17+
"housenumber": "1"
18+
}
19+
}
20+
)";
21+
json j = json::parse(json_invalid_string);
22+
try
23+
{
24+
int housenumber = j["address"]["housenumber"];
25+
std::cout << housenumber;
26+
}
27+
catch (const json::exception& e)
28+
{
29+
std::cout << e.what() << '\n';
30+
}
31+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[json.exception.type_error.302] (/address/housenumber) (bytes 92-95) type must be number, but is string

include/nlohmann/detail/exceptions.hpp

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,16 +131,34 @@ class exception : public std::exception
131131
{
132132
return concat(a, '/', detail::escape(b));
133133
});
134-
return concat('(', str, ") ");
134+
135+
return concat('(', str, ") ", get_byte_positions(leaf_element));
135136
#else
136-
static_cast<void>(leaf_element);
137-
return "";
137+
return get_byte_positions(leaf_element);
138138
#endif
139139
}
140140

141141
private:
142142
/// an exception object as storage for error messages
143143
std::runtime_error m;
144+
#if JSON_DIAGNOSTIC_POSITIONS
145+
template<typename BasicJsonType>
146+
static std::string get_byte_positions(const BasicJsonType* leaf_element)
147+
{
148+
if ((leaf_element->start_pos() != std::string::npos) && (leaf_element->end_pos() != std::string::npos))
149+
{
150+
return concat("(bytes ", std::to_string(leaf_element->start_pos()), "-", std::to_string(leaf_element->end_pos()), ") ");
151+
}
152+
return "";
153+
}
154+
#else
155+
template<typename BasicJsonType>
156+
static std::string get_byte_positions(const BasicJsonType* leaf_element)
157+
{
158+
static_cast<void>(leaf_element);
159+
return "";
160+
}
161+
#endif
144162
};
145163

146164
/// @brief exception indicating a parse error

single_include/nlohmann/json.hpp

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4586,16 +4586,34 @@ class exception : public std::exception
45864586
{
45874587
return concat(a, '/', detail::escape(b));
45884588
});
4589-
return concat('(', str, ") ");
4589+
4590+
return concat('(', str, ") ", get_byte_positions(leaf_element));
45904591
#else
4591-
static_cast<void>(leaf_element);
4592-
return "";
4592+
return get_byte_positions(leaf_element);
45934593
#endif
45944594
}
45954595

45964596
private:
45974597
/// an exception object as storage for error messages
45984598
std::runtime_error m;
4599+
#if JSON_DIAGNOSTIC_POSITIONS
4600+
template<typename BasicJsonType>
4601+
static std::string get_byte_positions(const BasicJsonType* leaf_element)
4602+
{
4603+
if ((leaf_element->start_pos() != std::string::npos) && (leaf_element->end_pos() != std::string::npos))
4604+
{
4605+
return concat("(bytes ", std::to_string(leaf_element->start_pos()), "-", std::to_string(leaf_element->end_pos()), ") ");
4606+
}
4607+
return "";
4608+
}
4609+
#else
4610+
template<typename BasicJsonType>
4611+
static std::string get_byte_positions(const BasicJsonType* leaf_element)
4612+
{
4613+
static_cast<void>(leaf_element);
4614+
return "";
4615+
}
4616+
#endif
45994617
};
46004618

46014619
/// @brief exception indicating a parse error
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// __ _____ _____ _____
2+
// __| | __| | | | JSON for Modern C++ (supporting code)
3+
// | | |__ | | | | | | version 3.11.3
4+
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
5+
//
6+
// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann <https://nlohmann.me>
7+
// SPDX-License-Identifier: MIT
8+
9+
#include "doctest_compatibility.h"
10+
11+
#ifdef JSON_DIAGNOSTICS
12+
#undef JSON_DIAGNOSTICS
13+
#endif
14+
15+
#define JSON_DIAGNOSTICS 0
16+
#define JSON_DIAGNOSTIC_POSITIONS 1
17+
#include <nlohmann/json.hpp>
18+
19+
using json = nlohmann::json;
20+
21+
TEST_CASE("Better diagnostics with positions only")
22+
{
23+
SECTION("invalid type")
24+
{
25+
const std::string json_invalid_string = R"(
26+
{
27+
"address": {
28+
"street": "Fake Street",
29+
"housenumber": "1"
30+
}
31+
}
32+
)";
33+
json j = json::parse(json_invalid_string);
34+
CHECK_THROWS_WITH_AS(j.at("address").at("housenumber").get<int>(),
35+
"[json.exception.type_error.302] (bytes 108-111) type must be number, but is string", json::type_error);
36+
}
37+
38+
SECTION("invalid type without positions")
39+
{
40+
const json j = "foo";
41+
CHECK_THROWS_WITH_AS(j.get<int>(),
42+
"[json.exception.type_error.302] type must be number, but is string", json::type_error);
43+
}
44+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// __ _____ _____ _____
2+
// __| | __| | | | JSON for Modern C++ (supporting code)
3+
// | | |__ | | | | | | version 3.11.3
4+
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
5+
//
6+
// SPDX-FileCopyrightText: 2013 - 2024 Niels Lohmann <https://nlohmann.me>
7+
// SPDX-License-Identifier: MIT
8+
9+
#include "doctest_compatibility.h"
10+
11+
#define JSON_DIAGNOSTICS 1
12+
#define JSON_DIAGNOSTIC_POSITIONS 1
13+
#include <nlohmann/json.hpp>
14+
15+
using json = nlohmann::json;
16+
17+
TEST_CASE("Better diagnostics with positions")
18+
{
19+
SECTION("invalid type")
20+
{
21+
const std::string json_invalid_string = R"(
22+
{
23+
"address": {
24+
"street": "Fake Street",
25+
"housenumber": "1"
26+
}
27+
}
28+
)";
29+
json j = json::parse(json_invalid_string);
30+
CHECK_THROWS_WITH_AS(j.at("address").at("housenumber").get<int>(),
31+
"[json.exception.type_error.302] (/address/housenumber) (bytes 108-111) type must be number, but is string", json::type_error);
32+
}
33+
34+
SECTION("invalid type without positions")
35+
{
36+
const json j = "foo";
37+
CHECK_THROWS_WITH_AS(j.get<int>(),
38+
"[json.exception.type_error.302] type must be number, but is string", json::type_error);
39+
}
40+
}

tests/src/unit-json_patch.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,20 @@ TEST_CASE("JSON patch")
6060
json const doc2 = R"({ "q": { "bar": 2 } })"_json;
6161

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

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

6872
// should cause an error because "b" does not exist in doc3
6973
#if JSON_DIAGNOSTICS
7074
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] (/a) key 'b' not found", json::out_of_range&);
75+
#elif JSON_DIAGNOSTIC_POSITIONS
76+
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] (bytes 7-9) key 'b' not found", json::out_of_range&);
7177
#else
7278
CHECK_THROWS_WITH_AS(doc3.patch(patch2), "[json.exception.out_of_range.403] key 'b' not found", json::out_of_range&);
7379
#endif
@@ -333,6 +339,8 @@ TEST_CASE("JSON patch")
333339
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
334340
#if JSON_DIAGNOSTICS
335341
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
342+
#elif JSON_DIAGNOSTIC_POSITIONS
343+
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-95) unsuccessful: " + patch[0].dump());
336344
#else
337345
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
338346
#endif
@@ -417,8 +425,11 @@ TEST_CASE("JSON patch")
417425
// applied), because the "add" operation's target location that
418426
// references neither the root of the document, nor a member of
419427
// an existing object, nor a member of an existing array.
420-
428+
#if JSON_DIAGNOSTIC_POSITIONS
429+
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.403] (bytes 21-37) key 'baz' not found", json::out_of_range&);
430+
#else
421431
CHECK_THROWS_WITH_AS(doc.patch(patch), "[json.exception.out_of_range.403] key 'baz' not found", json::out_of_range&);
432+
#endif
422433
}
423434

424435
// A.13. Invalid JSON Patch Document
@@ -476,6 +487,8 @@ TEST_CASE("JSON patch")
476487
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
477488
#if JSON_DIAGNOSTICS
478489
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
490+
#elif JSON_DIAGNOSTIC_POSITIONS
491+
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-92) unsuccessful: " + patch[0].dump());
479492
#else
480493
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
481494
#endif
@@ -1205,6 +1218,8 @@ TEST_CASE("JSON patch")
12051218
CHECK_THROWS_AS(doc.patch(patch), json::other_error&);
12061219
#if JSON_DIAGNOSTICS
12071220
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (/0) unsuccessful: " + patch[0].dump());
1221+
#elif JSON_DIAGNOSTIC_POSITIONS
1222+
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] (bytes 47-117) unsuccessful: " + patch[0].dump());
12081223
#else
12091224
CHECK_THROWS_WITH_STD_STR(doc.patch(patch), "[json.exception.other_error.501] unsuccessful: " + patch[0].dump());
12101225
#endif

tests/src/unit-json_pointer.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,15 @@ TEST_CASE("JSON pointers")
203203
// escaped access
204204
CHECK(j[json::json_pointer("/a~1b")] == j["a/b"]);
205205
CHECK(j[json::json_pointer("/m~0n")] == j["m~n"]);
206-
206+
#if JSON_DIAGNOSTIC_POSITIONS
207+
// unescaped access
208+
CHECK_THROWS_WITH_AS(j.at(json::json_pointer("/a/b")),
209+
"[json.exception.out_of_range.403] (bytes 13-297) key 'a' not found", json::out_of_range&);
210+
#else
207211
// unescaped access
208212
CHECK_THROWS_WITH_AS(j.at(json::json_pointer("/a/b")),
209213
"[json.exception.out_of_range.403] key 'a' not found", json::out_of_range&);
210-
214+
#endif
211215
// unresolved access
212216
const json j_primitive = 1;
213217
CHECK_THROWS_WITH_AS(j_primitive["/foo"_json_pointer],

tests/src/unit-regression1.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,14 +1400,24 @@ TEST_CASE("regression tests 1")
14001400
auto p1 = R"([{"op": "move",
14011401
"from": "/one/two/three",
14021402
"path": "/a/b/c"}])"_json;
1403+
#if JSON_DIAGNOSTIC_POSITIONS
1404+
CHECK_THROWS_WITH_AS(model.patch(p1),
1405+
"[json.exception.out_of_range.403] (bytes 0-158) key 'a' not found", json::out_of_range&);
1406+
#else
14031407
CHECK_THROWS_WITH_AS(model.patch(p1),
14041408
"[json.exception.out_of_range.403] key 'a' not found", json::out_of_range&);
1409+
#endif
14051410

14061411
auto p2 = R"([{"op": "copy",
14071412
"from": "/one/two/three",
14081413
"path": "/a/b/c"}])"_json;
1414+
#if JSON_DIAGNOSTIC_POSITIONS
1415+
CHECK_THROWS_WITH_AS(model.patch(p2),
1416+
"[json.exception.out_of_range.403] (bytes 0-158) key 'a' not found", json::out_of_range&);
1417+
#else
14091418
CHECK_THROWS_WITH_AS(model.patch(p2),
14101419
"[json.exception.out_of_range.403] key 'a' not found", json::out_of_range&);
1420+
#endif
14111421
}
14121422

14131423
SECTION("issue #961 - incorrect parsing of indefinite length CBOR strings")

0 commit comments

Comments
 (0)