diff --git a/docs/mkdocs/docs/api/basic_json/object_t.md b/docs/mkdocs/docs/api/basic_json/object_t.md index cf0ffc87b..de41b86e4 100644 --- a/docs/mkdocs/docs/api/basic_json/object_t.md +++ b/docs/mkdocs/docs/api/basic_json/object_t.md @@ -63,7 +63,8 @@ behavior: object will agree on the name-value mappings. - When the names within an object are not unique, it is unspecified which one of the values for a given key will be chosen. For instance, `#!json {"key": 2, "key": 1}` could be equal to either `#!json {"key": 1}` or - `#!json {"key": 2}`. + `#!json {"key": 2}`. To reject duplicate keys instead of silently resolving them one way or another, see + [this parsing recipe](../../features/parsing/parser_callbacks.md#recipe-rejecting-duplicate-object-keys). - Internally, name/value pairs are stored in lexicographical order of the names. Objects will also be serialized (see [`dump`](dump.md)) in this order. For instance, `#!json {"b": 1, "a": 2}` and `#!json {"a": 2, "b": 1}` will be stored and serialized as `#!json {"a": 2, "b": 1}`. diff --git a/docs/mkdocs/docs/examples/reject_duplicate_keys.cpp b/docs/mkdocs/docs/examples/reject_duplicate_keys.cpp new file mode 100644 index 000000000..0e5e78925 --- /dev/null +++ b/docs/mkdocs/docs/examples/reject_duplicate_keys.cpp @@ -0,0 +1,61 @@ +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +json parse_strict(const std::string& input) +{ + // one key set per nesting depth, reused across sibling objects + std::vector> keys; + + auto reject_duplicate_keys = [&](int depth, json::parse_event_t event, json & parsed) + { + if (event == json::parse_event_t::object_start) + { + // keys of this object are reported at depth+1 (see the event table above) + const auto child_depth = static_cast(depth) + 1; + if (keys.size() <= child_depth) + { + keys.resize(child_depth + 1); + } + keys[child_depth].clear(); + return true; + } + + if (event == json::parse_event_t::key) + { + auto& seen = keys[static_cast(depth)]; + const auto& key = parsed.get_ref(); + if (!seen.insert(key).second) + { + throw std::runtime_error("duplicate JSON object key: " + key); + } + return true; + } + + return true; + }; + + return json::parse(input, reject_duplicate_keys); +} + +int main() +{ + // parsing succeeds when all keys are unique + json j = parse_strict(R"({"one": 1, "two": 2})"); + std::cout << j << '\n'; + + // parsing throws when a key is repeated + try + { + parse_strict(R"({"one": 1, "one": 2})"); + } + catch (const std::exception& e) + { + std::cout << e.what() << '\n'; + } +} diff --git a/docs/mkdocs/docs/examples/reject_duplicate_keys.output b/docs/mkdocs/docs/examples/reject_duplicate_keys.output new file mode 100644 index 000000000..2366b4a43 --- /dev/null +++ b/docs/mkdocs/docs/examples/reject_duplicate_keys.output @@ -0,0 +1,2 @@ +{"one":1,"two":2} +duplicate JSON object key: one diff --git a/docs/mkdocs/docs/features/parsing/parser_callbacks.md b/docs/mkdocs/docs/features/parsing/parser_callbacks.md index ef076d126..d398be325 100644 --- a/docs/mkdocs/docs/features/parsing/parser_callbacks.md +++ b/docs/mkdocs/docs/features/parsing/parser_callbacks.md @@ -81,3 +81,34 @@ was called: ```json --8<-- "examples/parse__string__parser_callback_t.output" ``` + +## Recipe: rejecting duplicate object keys + +The JSON specification leaves the handling of objects with repeated keys up to the implementation. As described in +[`object_t`](../../api/basic_json/object_t.md#behavior), it is unspecified which value for a repeated key ends up in +the resulting `#!c json` value -- once parsing has produced that value, the duplicate is already gone, because object +storage maps each key to a single value. If duplicate keys should instead be treated as an error, a parser callback +can detect them while the object is still being read, before that ambiguity ever applies. + +??? example + + ```cpp + --8<-- "examples/reject_duplicate_keys.cpp" + ``` + + Output: + + ```json + --8<-- "examples/reject_duplicate_keys.output" + ``` + +This approach has two limitations: + +- The depth-indexed bookkeeping must account for the fact that `object_start` reports the depth of the *parent* of + the object, while the `key` events inside that object are reported one depth deeper (see the event table above); + it is easy to get this off by one for nested objects. +- The thrown exception cannot carry a `parse_error`-style byte offset, because position tracking only exists inside + the parser and lexer, not at the callback layer. + +For strict validation with precise error positions, implementing a [SAX interface](sax_interface.md) instead gives +access to the parser's position information directly. diff --git a/docs/mkdocs/docs/features/types/index.md b/docs/mkdocs/docs/features/types/index.md index 78e332936..f24da12a6 100644 --- a/docs/mkdocs/docs/features/types/index.md +++ b/docs/mkdocs/docs/features/types/index.md @@ -131,7 +131,7 @@ std::map< The choice of `object_t` influences the behavior of the JSON class. With the default type, objects have the following behavior: - When all names are unique, objects will be interoperable in the sense that all software implementations receiving that object will agree on the name-value mappings. -- When the names within an object are not unique, it is unspecified which one of the values for a given key will be chosen. For instance, `#!json {"key": 2, "key": 1}` could be equal to either `#!json {"key": 1}` or `#!json {"key": 2}`. +- When the names within an object are not unique, it is unspecified which one of the values for a given key will be chosen. For instance, `#!json {"key": 2, "key": 1}` could be equal to either `#!json {"key": 1}` or `#!json {"key": 2}`. To reject duplicate keys instead of silently resolving them one way or another, see [this parsing recipe](../parsing/parser_callbacks.md#recipe-rejecting-duplicate-object-keys). - Internally, name/value pairs are stored in lexicographical order of the names. Objects will also be serialized (see `dump`) in this order. For instance, both `#!json {"b": 1, "a": 2}` and `#!json {"a": 2, "b": 1}` will be stored and serialized as `#!json {"a": 2, "b": 1}`. - When comparing objects, the order of the name/value pairs is irrelevant. This makes objects interoperable in the sense that they will not be affected by these differences. For instance, `#!json {"b": 1, "a": 2}` and `#!json {"a": 2, "b": 1}` will be treated as equal.