diff --git a/README.md b/README.md index eab5fe847..97ac8f451 100644 --- a/README.md +++ b/README.md @@ -1105,7 +1105,7 @@ Just as in [Arbitrary Type Conversions](#arbitrary-types-conversions) above, Other Important points: -- When using `get()`, undefined JSON values will default to the first pair specified in your map. Select this default pair carefully. +- When using `get()`, undefined JSON values will default to the first pair specified in your map. Select this default pair carefully. If you desire an exception in this circumstance use `NLOHMANN_JSON_SERIALIZE_ENUM_STRICT()` which behaves identically except for throwing an exception on unrecognized values. - If an enum or JSON value is specified more than once in your map, the first matching occurrence from the top of the map will be returned when converting to or from JSON. ### Binary formats (BSON, CBOR, MessagePack, UBJSON, and BJData) diff --git a/docs/mkdocs/docs/api/macros/index.md b/docs/mkdocs/docs/api/macros/index.md index 2773bfc93..2397bc702 100644 --- a/docs/mkdocs/docs/api/macros/index.md +++ b/docs/mkdocs/docs/api/macros/index.md @@ -54,6 +54,7 @@ header. See also the [macro overview page](../../features/macros.md). ### Enums - [**NLOHMANN_JSON_SERIALIZE_ENUM**](nlohmann_json_serialize_enum.md) - serialize/deserialize an enum +- [**NLOHMANN_JSON_SERIALIZE_ENUM_STRICT**](nlohmann_json_serialize_enum_strict.md) - serialize/deserialize an enum with exceptions ### Classes and structs diff --git a/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum.md b/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum.md index fba6b217a..915a20a5f 100644 --- a/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum.md +++ b/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum.md @@ -78,6 +78,7 @@ inline void from_json(const BasicJsonType& j, type& e); ## See also - [Specializing enum conversion](../../features/enum_conversion.md) +- [`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT`](./nlohmann_json_serialize_enum_strict.md) - [`JSON_DISABLE_ENUM_SERIALIZATION`](json_disable_enum_serialization.md) ## Version history diff --git a/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum_strict.md b/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum_strict.md new file mode 100644 index 000000000..f4c52e070 --- /dev/null +++ b/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum_strict.md @@ -0,0 +1,102 @@ +# NLOHMANN_JSON_SERIALIZE_ENUM_STRICT + +```cpp +#define NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(type, conversion...) +``` + +By default, enum values are serialized to JSON as integers. In some cases, this could result in undesired behavior. If +an enum is modified or re-ordered after data has been serialized to JSON, the later deserialized JSON data may be +undefined or a different enum value than was originally intended. + +`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT` allows to define a user-defined serialization for every enumerator that +throws an exception on undefined input. + +## Parameters + +`type` (in) +: name of the enum to serialize/deserialize + +`conversion` (in) +: a pair of an enumerator and a JSON serialization; arbitrary pairs can be given as a comma-separated list + +## Default definition + +The macro adds two functions to the namespace which take care of the serialization and deserialization: + +```cpp +template +inline void to_json(BasicJsonType& j, const type& e); +template +inline void from_json(const BasicJsonType& j, type& e); +``` + +## Notes + +!!! info "Prerequisites" + + The macro must be used inside the namespace of the enum. + +!!! important "Important notes" + + - When using [`get()`](../basic_json/get.md), undefined JSON values will throw an exception. + - If an enum or JSON value is specified in multiple conversions, the first matching conversion from the top of the + list will be returned when converting to or from JSON. See example 2 below. + +## Examples + +??? example "Example 1: Basic usage" + + The example shows how `NLOHMANN_JSON_SERIALIZE_ENUM_STRICT` can be used to serialize/deserialize both classical enums and + C++11 enum classes: + + ```cpp hl_lines="16 17 18 19 20 21 22 29 30 31 32 33" + --8<-- "examples/nlohmann_json_serialize_enum_strict.cpp" + ``` + + Output: + + ```json + --8<-- "examples/nlohmann_json_serialize_enum_strict.output" + ``` + +??? example "Example 2: Multiple conversions for one enumerator" + + The example shows how to use multiple conversions for a single enumerator. In the example, `Color::red` will always + be *serialized* to `"red"`, because the first occurring conversion. The second conversion, however, offers an + alternative *deserialization* from `"rot"` to `Color::red`. + + ```cpp hl_lines="17" + --8<-- "examples/nlohmann_json_serialize_enum_strict_2.cpp" + ``` + + Output: + + ```json + --8<-- "examples/nlohmann_json_serialize_enum_strict_2.output" + ``` + +??? example "Example 3: exceptions on invalid serialization" + + The example shows how an invalid serialization causes an exception to be thrown. In the example, + Color::unknown is not defined in the mapping used to call `NLOHMANN_JSON_SERIALIZE_ENUM_STRICT` + so causes an exception when used to serialize. Similarly, "what" does not refer to an enum + value so also causes an exception when deserialization is attempted. + + ```cpp hl_lines="14 32 33 43 44 45" + --8<-- "examples/nlohmann_json_serialize_enum_strict_err.cpp" + ``` + + Output: + ```json + --8<-- "examples/nlohmann_json_serialize_enum_strict_err.output" + ``` + +## See also + +- [Specializing enum conversion](../../features/enum_conversion.md) +- [`NLOHMANN_JSON_SERIALIZE_ENUM`](./nlohmann_json_serialize_enum.md) +- [`JSON_DISABLE_ENUM_SERIALIZATION`](json_disable_enum_serialization.md) + +## Version history + +Added in version 3.12.0. diff --git a/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict.cpp b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict.cpp new file mode 100644 index 000000000..fa2572dec --- /dev/null +++ b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict.cpp @@ -0,0 +1,52 @@ +#include +#include + +using json = nlohmann::json; + +namespace ns +{ +enum TaskState +{ + TS_STOPPED, + TS_RUNNING, + TS_COMPLETED, + TS_INVALID = -1 +}; + +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(TaskState, +{ + { TS_INVALID, nullptr }, + { TS_STOPPED, "stopped" }, + { TS_RUNNING, "running" }, + { TS_COMPLETED, "completed" } +}) + +enum class Color +{ + red, green, blue, unknown +}; + +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(Color, +{ + { Color::unknown, "unknown" }, { Color::red, "red" }, + { Color::green, "green" }, { Color::blue, "blue" } +}) +} // namespace ns + +int main() +{ + // serialization + json j_stopped = ns::TS_STOPPED; + json j_red = ns::Color::red; + std::cout << "ns::TS_STOPPED -> " << j_stopped + << ", ns::Color::red -> " << j_red << std::endl; + + // deserialization + json j_running = "running"; + json j_blue = "blue"; + auto running = j_running.get(); + auto blue = j_blue.get(); + std::cout << j_running << " -> " << running + << ", " << j_blue << " -> " << static_cast(blue) << std::endl; + +} diff --git a/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict.output b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict.output new file mode 100644 index 000000000..59c4ff70a --- /dev/null +++ b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict.output @@ -0,0 +1,2 @@ +ns::TS_STOPPED -> "stopped", ns::Color::red -> "red" +"running" -> 1, "blue" -> 2 diff --git a/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_2.cpp b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_2.cpp new file mode 100644 index 000000000..7f766b7a9 --- /dev/null +++ b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_2.cpp @@ -0,0 +1,33 @@ +#include +#include + +using json = nlohmann::json; + +namespace ns +{ +enum class Color +{ + red, green, blue, unknown +}; + +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(Color, +{ + { Color::unknown, "unknown" }, { Color::red, "red" }, + { Color::green, "green" }, { Color::blue, "blue" }, + { Color::red, "rot" } // a second conversion for Color::red +}) +} + +int main() +{ + // serialization + json j_red = ns::Color::red; + std::cout << static_cast(ns::Color::red) << " -> " << j_red << std::endl; + + // deserialization + json j_rot = "rot"; + auto rot = j_rot.get(); + auto red = j_red.get(); + std::cout << j_rot << " -> " << static_cast(rot) << std::endl; + std::cout << j_red << " -> " << static_cast(red) << std::endl; +} diff --git a/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_2.output b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_2.output new file mode 100644 index 000000000..5dec31b4a --- /dev/null +++ b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_2.output @@ -0,0 +1,3 @@ +0 -> "red" +"rot" -> 0 +"red" -> 0 diff --git a/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_err.cpp b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_err.cpp new file mode 100644 index 000000000..aeb901122 --- /dev/null +++ b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_err.cpp @@ -0,0 +1,53 @@ +#include +#include + +using json = nlohmann::json; + +namespace ns +{ + +enum class Color +{ + red, + green, + blue, + unknown // not mapped in JSON_SERIALIZE_ENUM_STRICT +}; + +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(Color, +{ + {Color::red, "red"}, + {Color::green, "green"}, + {Color::blue, "blue"} +}) + +} // namespace ns + + +int main() +{ + // invalid serialization + try + { + // ns::color::unknown was not mapped in macro + json invalid_serialization = ns::Color::unknown; + } + catch (const json::exception e) + { + std::cout << "deserialization failed: " << e.what() << std::endl; + } + + // invalid deserialization + try + { + // what does not map to an enum + json invalid_deserialization("what"); + ns::Color color = invalid_deserialization.get(); + } + catch (const json::exception e) + { + std::cout << "deserialization failed: " << e.what() << std::endl; + } + + return 0; +} diff --git a/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_err.output b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_err.output new file mode 100644 index 000000000..4894b4c7d --- /dev/null +++ b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict_err.output @@ -0,0 +1,2 @@ +deserialization failed: [json.exception.out_of_range.410] enum value out of range for Color +deserialization failed: [json.exception.out_of_range.410] enum value out of range for Color: "what" diff --git a/docs/mkdocs/docs/features/enum_conversion.md b/docs/mkdocs/docs/features/enum_conversion.md index bd3977d91..d75d6e112 100644 --- a/docs/mkdocs/docs/features/enum_conversion.md +++ b/docs/mkdocs/docs/features/enum_conversion.md @@ -55,7 +55,8 @@ Just as in [Arbitrary Type Conversions](arbitrary_types.md) above, Other Important points: - When using `get()`, undefined JSON values will default to the first pair specified in your map. Select this - default pair carefully. + default pair carefully. If you desire an exception in this circumstance use [`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT()`](../api/macros/nlohmann_json_serialize_enum_strict.md) + which behaves identically except for throwing an exception on unrecognized values. - If an enum or JSON value is specified more than once in your map, the first matching occurrence from the top of the map will be returned when converting to or from JSON. - To disable the default serialization of enumerators as integers and force a compiler error instead, see [`JSON_DISABLE_ENUM_SERIALIZATION`](../api/macros/json_disable_enum_serialization.md). diff --git a/docs/mkdocs/docs/home/exceptions.md b/docs/mkdocs/docs/home/exceptions.md index cfa4f57a2..c949bdbf2 100644 --- a/docs/mkdocs/docs/home/exceptions.md +++ b/docs/mkdocs/docs/home/exceptions.md @@ -868,6 +868,16 @@ Key identifiers to be serialized to BSON cannot contain code point U+0000, since BSON key cannot contain code point U+0000 (at byte 2) ``` +### json.exception.out_of_range.410 + +Undefined json fields cannot be used with [`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT`](../api/macros/nlohmann_json_serialize_enum_strict.md) + +!!! failure "Example message" + + ``` + enum value out of range + ``` + ## Further exceptions This exception is thrown in case of errors that cannot be classified with the diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 5794ecd05..c374926d2 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -294,6 +294,7 @@ nav: - 'NLOHMANN_JSON_NAMESPACE_BEGIN, NLOHMANN_JSON_NAMESPACE_END': api/macros/nlohmann_json_namespace_begin.md - 'NLOHMANN_JSON_NAMESPACE_NO_VERSION': api/macros/nlohmann_json_namespace_no_version.md - 'NLOHMANN_JSON_SERIALIZE_ENUM': api/macros/nlohmann_json_serialize_enum.md + - 'NLOHMANN_JSON_SERIALIZE_ENUM_STRICT': api/macros/nlohmann_json_serialize_enum_strict.md - 'NLOHMANN_JSON_VERSION_MAJOR, NLOHMANN_JSON_VERSION_MINOR, NLOHMANN_JSON_VERSION_PATCH': api/macros/nlohmann_json_version_major.md - Community: - community/index.md diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index 8a7608ec3..450ba943f 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -253,6 +253,61 @@ e = ((it != std::end(m)) ? it : std::begin(m))->first; \ } + + +/*! +@brief function to wrap JSON_THROW_MACRO - there can be compilation errors about + there being no arguments to JSON_THROW that depend on template arguments + if this is not used to call JSON_THROW +*/ +template +void templated_json_throw(ExceptionType exception) +{ + JSON_THROW(exception); + + /* JSON_THROW(exception) discards exception and aborts - void cast needed to supress + compilation error if compiled with -Werror and Wunused-parameter */ + (void)exception; +} + +/*! +@brief macro to briefly define a mapping between an enum and JSON with exception + on invalid input +@def NLOHMANN_JSON_SERIALIZE_ENUM_STRICT +@since version 3.12.0 +*/ +#define NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(ENUM_TYPE, ...) \ + template \ + inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ + { \ + /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on */ \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [e](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.first == e; \ + }); \ + if (it != std::end(m)) j = it->second; \ + else templated_json_throw(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE, nullptr)); \ + } \ + template \ + inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ + { \ + /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on */ \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [&j](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.second == j; \ + }); \ + if (it != std::end(m)) e = it->first; \ + else templated_json_throw(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE ": " + j.dump(), &j)); \ + } + // Ugly macros to avoid uglier copy-paste when specializing basic_json. They // may be removed in the future once the class is split. diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 795d60cfe..2e16ad5b9 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -2627,6 +2627,61 @@ JSON_HEDLEY_DIAGNOSTIC_POP e = ((it != std::end(m)) ? it : std::begin(m))->first; \ } + + +/*! +@brief function to wrap JSON_THROW_MACRO - there can be compilation errors about + there being no arguments to JSON_THROW that depend on template arguments + if this is not used to call JSON_THROW +*/ +template +void templated_json_throw(ExceptionType exception) +{ + JSON_THROW(exception); + + /* JSON_THROW(exception) discards exception and aborts - void cast needed to supress + compilation error if compiled with -Werror and Wunused-parameter */ + (void)exception; +} + +/*! +@brief macro to briefly define a mapping between an enum and JSON with exception + on invalid input +@def NLOHMANN_JSON_SERIALIZE_ENUM_STRICT +@since version 3.12.0 +*/ +#define NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(ENUM_TYPE, ...) \ + template \ + inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ + { \ + /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on */ \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [e](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.first == e; \ + }); \ + if (it != std::end(m)) j = it->second; \ + else templated_json_throw(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE, nullptr)); \ + } \ + template \ + inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ + { \ + /* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + /* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on */ \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [&j](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.second == j; \ + }); \ + if (it != std::end(m)) e = it->first; \ + else templated_json_throw(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE ": " + j.dump(), &j)); \ + } + // Ugly macros to avoid uglier copy-paste when specializing basic_json. They // may be removed in the future once the class is split. diff --git a/tests/src/unit-conversions.cpp b/tests/src/unit-conversions.cpp index 81b8608fb..4581fa660 100644 --- a/tests/src/unit-conversions.cpp +++ b/tests/src/unit-conversions.cpp @@ -1657,6 +1657,84 @@ TEST_CASE("JSON to enum mapping") } } +enum class strict_cards {kreuz, pik, herz, karo, andere}; // andere not included in mapping + +// NOLINTNEXTLINE(misc-use-internal-linkage,misc-const-correctness,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) - false positive +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(strict_cards, +{ + {strict_cards::kreuz, "kreuz"}, + {strict_cards::pik, "pik"}, + {strict_cards::pik, "puk"}, // second entry for cards::pik; will not be used + {strict_cards::herz, "herz"}, + {strict_cards::karo, "karo"} +}) + +enum StrictTaskState // NOLINT(cert-int09-c,readability-enum-initial-value,cppcoreguidelines-use-enum-class) +{ + STRICT_TS_STOPPED, + STRICT_TS_RUNNING, + STRICT_TS_COMPLETED, + STRICT_TS_OTHER, // STRICT_TS_OTHER not in mapping + STRICT_TS_INVALID = -1, +}; + +// NOLINTNEXTLINE(misc-const-correctness,misc-use-internal-linkage,cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) - false positive +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(StrictTaskState, +{ + {STRICT_TS_INVALID, nullptr}, + {STRICT_TS_STOPPED, "stopped"}, + {STRICT_TS_RUNNING, "running"}, + {STRICT_TS_COMPLETED, "completed"}, +}) + +TEST_CASE("Strict JSON to enum mapping") +{ + SECTION("enum class") + { + // enum -> json + CHECK(json(strict_cards::kreuz) == "kreuz"); + CHECK(json(strict_cards::pik) == "pik"); + CHECK(json(strict_cards::herz) == "herz"); + CHECK(json(strict_cards::karo) == "karo"); + + // json -> enum + CHECK(strict_cards::kreuz == json("kreuz")); + CHECK(strict_cards::pik == json("pik")); + CHECK(strict_cards::herz == json("herz")); + CHECK(strict_cards::karo == json("karo")); + + // invalid json -> exception thrown + json _; + CHECK_THROWS_WITH_AS(_ = json("what?").get(), "[json.exception.out_of_range.410] enum value out of range for strict_cards: \"what?\"", json::out_of_range&); + + // conversion of unmapped enum -> exception thrown + CHECK_THROWS_WITH_AS(json(strict_cards::andere), "[json.exception.out_of_range.410] enum value out of range for strict_cards", json::out_of_range&); + } + + SECTION("traditional enum") + { + // enum -> json + CHECK(json(STRICT_TS_STOPPED) == "stopped"); + CHECK(json(STRICT_TS_RUNNING) == "running"); + CHECK(json(STRICT_TS_COMPLETED) == "completed"); + CHECK(json(STRICT_TS_INVALID) == json()); + + // json -> enum + CHECK(STRICT_TS_STOPPED == json("stopped")); + CHECK(STRICT_TS_RUNNING == json("running")); + CHECK(STRICT_TS_COMPLETED == json("completed")); + CHECK(STRICT_TS_INVALID == json()); + + // invalid json -> exception thrown + json _; + CHECK_THROWS_WITH_AS(_ = json("what?").get(), "[json.exception.out_of_range.410] enum value out of range for StrictTaskState: \"what?\"", json::out_of_range&); + + // conversion of unmapped enum -> exception thrown + CHECK_THROWS_WITH_AS(json(STRICT_TS_OTHER), "[json.exception.out_of_range.410] enum value out of range for StrictTaskState", json::out_of_range&); + } +} + + #ifdef JSON_HAS_CPP_17 #if JSON_HAS_FILESYSTEM || JSON_HAS_EXPERIMENTAL_FILESYSTEM TEST_CASE("std::filesystem::path")