diff --git a/docs/mkdocs/docs/api/basic_json/unflatten.md b/docs/mkdocs/docs/api/basic_json/unflatten.md index 8b9f2cbc1..6bc9b66c0 100644 --- a/docs/mkdocs/docs/api/basic_json/unflatten.md +++ b/docs/mkdocs/docs/api/basic_json/unflatten.md @@ -26,7 +26,8 @@ The function can throw the following exceptions: - Throws [`type_error.314`](../../home/exceptions.md#jsonexceptiontype_error314) if value is not an object - Throws [`type_error.315`](../../home/exceptions.md#jsonexceptiontype_error315) if object values are not primitive - Throws [`type_error.313`](../../home/exceptions.md#jsonexceptiontype_error313) if a key (JSON pointer) leads to a - conflicting nesting; example: `"invalid value to unflatten"` + conflicting nesting, for example if a key with a value other than `#!json null` is a prefix of another key (as `/a` + and `/a/b`), in whichever order the keys come; example: `"invalid value to unflatten"` - Throws [`parse_error.106`](../../home/exceptions.md#jsonexceptionparse_error106) if an array index in a key begins with '0'; example: `"array index '01' must not begin with '0'"` - Throws [`parse_error.107`](../../home/exceptions.md#jsonexceptionparse_error107) if a key is not empty and does not @@ -90,3 +91,6 @@ Apart from these three cases, for a JSON value `j`, the following is always true - Throws [`parse_error.109`](../../home/exceptions.md#jsonexceptionparse_error109) instead of [`out_of_range.404`](../../home/exceptions.md#jsonexceptionout_of_range404) for a one-character array index that is not a digit (e.g., `/x`) in version 3.13.0, as it already did for longer ones. +- Throws [`type_error.313`](../../home/exceptions.md#jsonexceptiontype_error313) for a key that is a prefix of an + earlier key in version 3.13.0; before, an object type that keeps the insertion order (such as + [`ordered_json`](../ordered_json.md)) silently replaced the nested value. diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 784414a05..36f81482e 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -2455,6 +2455,19 @@ class json_pointer pos = end; } + if (JSON_HEDLEY_UNLIKELY(!current->is_null())) + { + // the key is a prefix of an earlier key (only possible if the + // object type does not sort its keys), so the value is the + // array or object built for that key: a null leaves it as it + // is (as when the shorter key comes first), any other value + // conflicts with it + if (element.second.is_null()) + { + continue; + } + JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &result)); + } *current = element.second; } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 4aeb64141..07ba20439 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -28742,6 +28742,19 @@ class json_pointer pos = end; } + if (JSON_HEDLEY_UNLIKELY(!current->is_null())) + { + // the key is a prefix of an earlier key (only possible if the + // object type does not sort its keys), so the value is the + // array or object built for that key: a null leaves it as it + // is (as when the shorter key comes first), any other value + // conflicts with it + if (element.second.is_null()) + { + continue; + } + JSON_THROW(detail::type_error::create(detail::exception_id::unflatten_invalid_value, "invalid value to unflatten", &result)); + } *current = element.second; } diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index 5029f91e8..9f0c9fbb8 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -1240,6 +1240,34 @@ TEST_CASE("flatten of structured values") CHECK(flat_object.unflatten() == nlohmann::ordered_json::parse(R"({"b": {"1": 2}})")); } +#if !defined(JSON_NOEXCEPTION) + SECTION("unflatten with a key that is a prefix of an earlier key") + { + // json visits the shorter key first; ordered_json may see it last + CHECK_THROWS_WITH_AS(nlohmann::ordered_json::parse(R"({"/a/b":2,"/a":1})").unflatten(), + "[json.exception.type_error.313] invalid value to unflatten", nlohmann::ordered_json::type_error&); + CHECK_THROWS_WITH_AS(nlohmann::ordered_json::parse(R"({"/a/0/b":1,"/a/0":2})").unflatten(), + "[json.exception.type_error.313] invalid value to unflatten", nlohmann::ordered_json::type_error&); + CHECK_THROWS_WITH_AS(nlohmann::ordered_json::parse(R"({"/x":1,"":2})").unflatten(), + "[json.exception.type_error.313] invalid value to unflatten", nlohmann::ordered_json::type_error&); + + // the same members in the other order + CHECK_THROWS_WITH_AS(nlohmann::ordered_json::parse(R"({"/a":1,"/a/b":2})").unflatten(), + "[json.exception.type_error.313] invalid value to unflatten", nlohmann::ordered_json::type_error&); + CHECK_THROWS_WITH_AS(json::parse(R"({"/a/b":2,"/a":1})").unflatten(), + "[json.exception.type_error.313] invalid value to unflatten", json::type_error&); + } +#endif + + SECTION("unflatten with a null at a key that is a prefix of another key") + { + // the null keeps the container, whichever key comes first + CHECK(nlohmann::ordered_json::parse(R"({"/a/b":2,"/a":null})").unflatten() == nlohmann::ordered_json::parse(R"({"a":{"b":2}})")); + CHECK(nlohmann::ordered_json::parse(R"({"/a":null,"/a/b":2})").unflatten() == nlohmann::ordered_json::parse(R"({"a":{"b":2}})")); + CHECK(nlohmann::ordered_json::parse(R"({"/a/0":1,"/a":null})").unflatten() == nlohmann::ordered_json::parse(R"({"a":[1]})")); + CHECK(json::parse(R"({"/a/b":2,"/a":null})").unflatten() == json::parse(R"({"a":{"b":2}})")); + } + #if !defined(JSON_NOEXCEPTION) SECTION("an object with the key \"0\" next to a non-index key does not round-trip") {