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 0000000000..3d026b3c97 --- /dev/null +++ b/docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum_strict.md @@ -0,0 +1,94 @@ +# NLOHMANN_JSON_SERIALIZE_ENUM_STRICT + +```cpp +#define NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(type, conversion...) +``` + +The `NLOHMANN_JSON_SERIALIZE_ENUM_STRICT` macro allows defining a user-defined serialization for every enumerator. + +This macro declares strict serialization and deserialization functions (`to_json` and `from_json`) for an enum type. +Unlike [`NLOHMANN_JSON_SERIALIZE_ENUM`](nlohmann_json_serialize_enum.md), this macro enforces strict validation and +throws errors for unmapped values instead of defaulting to the first enum value. + +## Parameters + +`type` (in) +: name of the enum to serialize/deserialize + +`conversion` (in) +: A list of parameters alternating between an enumerator value and a string to use in the JSON serialization. + +## 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" + + - Duplicate enum or JSON values in the mapping are not supported. Only the first occurrence will be used, + and subsequent mappings will be ignored. + This behavior is currently tolerated but may become an error in future versions. + - Unlike `NLOHMANN_JSON_SERIALIZE_ENUM`, this macro enforces strict validation: + - Attempting to serialize an unmapped enum value will throw a `type_error.302` exception + - Attempting to deserialize an unmapped JSON value will throw a `type_error.302` exception + - There is no default value behavior - all values must be explicitly mapped + +## Examples + +??? example "Example 1: Strict serialization" + + The example shows how `NLOHMANN_JSON_SERIALIZE_ENUM_STRICT` enforces strict validation when serializing an enum value that is not in the mapping: + + ```cpp + --8<-- "examples/nlohmann_json_serialize_enum_strict.cpp" + ``` + + Expected output: + + ``` + --8<-- "examples/nlohmann_json_serialize_enum_strict.output" + [json.exception.type_error.302] serialization failed: enum value 3 is out of range + ``` + +??? example "Example 2: Strict deserialization" + + The example shows how `NLOHMANN_JSON_SERIALIZE_ENUM_STRICT` enforces strict validation when deserializing a JSON value that is not in the mapping: + + ```cpp + --8<-- "examples/nlohmann_json_deserialize_enum_strict.cpp" + ``` + + Expected output: + + ``` + --8<-- "examples/nlohmann_json_deserialize_enum_strict.output" + [json.exception.type_error.302] deserialization failed: invalid JSON value "yellow" + ``` + +Both examples demonstrate: + +- Proper error handling using try-catch blocks +- Clear error messages indicating the cause of failure +- No default value behavior - all values must be explicitly mapped +- Exception throwing for unmapped values + +## See also + +- [Specializing enum conversion](../../features/enum_conversion.md) +- [`JSON_DISABLE_ENUM_SERIALIZATION`](json_disable_enum_serialization.md) + +## Version history + +- Added in version 3.11.4 diff --git a/docs/mkdocs/docs/examples/nlohmann_json_deserialize_enum_strict.cpp b/docs/mkdocs/docs/examples/nlohmann_json_deserialize_enum_strict.cpp new file mode 100644 index 0000000000..beb73e7e71 --- /dev/null +++ b/docs/mkdocs/docs/examples/nlohmann_json_deserialize_enum_strict.cpp @@ -0,0 +1,34 @@ +#include +#include + +using json = nlohmann::json; + +namespace ns +{ +enum class Color +{ + red, green, blue +}; + +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(Color, +{ + { Color::red, "red" }, + { Color::green, "green" }, + { Color::blue, "blue" }, +}) +} + +int main() +{ + json j_yellow = "yellow"; + // deserialization + try + { + auto yellow = j_yellow.template get(); + std::cout << j_yellow << " -> " << static_cast(yellow) << std::endl; + } + catch (const nlohmann::json::exception& e) + { + std::cout << e.what() << std::endl; + } +} diff --git a/docs/mkdocs/docs/examples/nlohmann_json_deserialize_enum_strict.output b/docs/mkdocs/docs/examples/nlohmann_json_deserialize_enum_strict.output new file mode 100644 index 0000000000..5b1d3f0623 --- /dev/null +++ b/docs/mkdocs/docs/examples/nlohmann_json_deserialize_enum_strict.output @@ -0,0 +1 @@ +[json.exception.type_error.302] deserialization failed: invalid JSON value "yellow" 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 0000000000..caf2408b61 --- /dev/null +++ b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict.cpp @@ -0,0 +1,34 @@ +#include +#include + +using json = nlohmann::json; + +namespace ns +{ +enum class Color +{ + red, green, blue, pink +}; + +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(Color, +{ + { Color::red, "red" }, + { Color::green, "green" }, + { Color::blue, "blue" }, +}) +} + +int main() +{ + // serialization + try + { + json j_red = ns::Color::pink; + auto color = j_red.get(); + std::cout << static_cast(color) << " -> " << j_red << std::endl; + } + catch (const nlohmann::json::exception& e) + { + std::cout << e.what() << 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 0000000000..cdd1ff85cc --- /dev/null +++ b/docs/mkdocs/docs/examples/nlohmann_json_serialize_enum_strict.output @@ -0,0 +1 @@ +[json.exception.type_error.302] serialization failed: enum value 3 is out of range diff --git a/docs/mkdocs/docs/features/enum_conversion.md b/docs/mkdocs/docs/features/enum_conversion.md index 1755bca2ad..709abc2eef 100644 --- a/docs/mkdocs/docs/features/enum_conversion.md +++ b/docs/mkdocs/docs/features/enum_conversion.md @@ -59,3 +59,22 @@ Other Important points: - 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). + +An alternative macro [`NLOHMANN_JSON_SERIALIZE_ENUM_STRICT()` macro](../api/macros/nlohmann_json_serialize_enum.md) can be used when a more strict error handling is preferred, throwing in case of serialization errors instead of defaulting to the first enum value defined in the macro. + +## Usage +```cpp +// example enum type declaration +enum TaskState { + TS_STOPPED, + TS_RUNNING, + TS_COMPLETED, +}; + +// map TaskState values to JSON as strings +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT( TaskState, { + {TS_STOPPED, "stopped"}, + {TS_RUNNING, "running"}, + {TS_COMPLETED, "completed"}, +}) +``` \ No newline at end of file diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index fe825b4431..1ed9dd1142 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -242,6 +242,63 @@ e = ((it != std::end(m)) ? it : std::begin(m))->first; \ } +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +template +[[noreturn]] inline void json_throw_from_serialize_macro(T&& exception) +{ +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + throw std::forward(exception); +#else + // Forward the exception (even if unused) and abort + std::forward(exception); + std::abort(); +#endif +} +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END +/*! +@brief macro to briefly define a mapping between an enum and JSON +@def NLOHMANN_JSON_SERIALIZE_ENUM_STRICT +@since version 3.11.4 +*/ +#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)) { \ + auto value = static_cast::type>(e); \ + nlohmann::detail::json_throw_from_serialize_macro(nlohmann::detail::type_error::create(302, nlohmann::detail::concat("serialization failed: enum value ", std::to_string(value), " is out of range"), &j)); \ + } \ + j = it->second; \ + } \ + 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)) \ + nlohmann::detail::json_throw_from_serialize_macro(nlohmann::detail::type_error::create(302, nlohmann::detail::concat("deserialization failed: invalid JSON value ", j.dump()), &j)); \ + e = it->first; \ + } + // 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 cda42a6e34..d1d3443f77 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -2608,6 +2608,63 @@ JSON_HEDLEY_DIAGNOSTIC_POP e = ((it != std::end(m)) ? it : std::begin(m))->first; \ } +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ +template +[[noreturn]] inline void json_throw_from_serialize_macro(T&& exception) +{ +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + throw std::forward(exception); +#else + // Forward the exception (even if unused) and abort + std::forward(exception); + std::abort(); +#endif +} +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END +/*! +@brief macro to briefly define a mapping between an enum and JSON +@def NLOHMANN_JSON_SERIALIZE_ENUM_STRICT +@since version 3.11.4 +*/ +#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)) { \ + auto value = static_cast::type>(e); \ + nlohmann::detail::json_throw_from_serialize_macro(nlohmann::detail::type_error::create(302, nlohmann::detail::concat("serialization failed: enum value ", std::to_string(value), " is out of range"), &j)); \ + } \ + j = it->second; \ + } \ + 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)) \ + nlohmann::detail::json_throw_from_serialize_macro(nlohmann::detail::type_error::create(302, nlohmann::detail::concat("deserialization failed: invalid JSON value ", j.dump()), &j)); \ + e = it->first; \ + } + // 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 7aa4e2a468..6818506165 100644 --- a/tests/src/unit-conversions.cpp +++ b/tests/src/unit-conversions.cpp @@ -1657,6 +1657,72 @@ TEST_CASE("JSON to enum mapping") } } +enum class cards_strict {kreuz, pik, herz, karo}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) - false positive +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(cards_strict, +{ + {cards_strict::kreuz, "kreuz"}, + {cards_strict::pik, "pik"}, + {cards_strict::pik, "puk"}, // second entry for cards::puk; will not be used + {cards_strict::herz, "herz"}, + {cards_strict::karo, "karo"} +}) + +enum TaskStateStrict // NOLINT(cert-int09-c,readability-enum-initial-value) +{ + TSS_STOPPED, + TSS_RUNNING, + TSS_COMPLETED, +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) - false positive +NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(TaskStateStrict, +{ + {TSS_STOPPED, "stopped"}, + {TSS_RUNNING, "running"}, + {TSS_COMPLETED, "completed"}, +}) + +TEST_CASE("JSON to enum mapping") +{ + SECTION("enum class") + { + // enum -> json + CHECK(json(cards_strict::kreuz) == "kreuz"); + CHECK(json(cards_strict::pik) == "pik"); + CHECK(json(cards_strict::herz) == "herz"); + CHECK(json(cards_strict::karo) == "karo"); + + // json -> enum + CHECK(cards_strict::kreuz == json("kreuz")); + CHECK(cards_strict::pik == json("pik")); + CHECK(cards_strict::herz == json("herz")); + CHECK(cards_strict::karo == json("karo")); + + // invalid json + const json j = "foo"; + CHECK_THROWS_WITH_AS(j.template get(), "[json.exception.type_error.302] deserialization failed: invalid JSON value \"foo\"", json::type_error); + } + + SECTION("traditional enum") + { + // enum -> json + CHECK(json(TSS_STOPPED) == "stopped"); + CHECK(json(TSS_RUNNING) == "running"); + CHECK(json(TSS_COMPLETED) == "completed"); + + // json -> enum + CHECK(TSS_STOPPED == json("stopped")); + CHECK(TSS_RUNNING == json("running")); + CHECK(TSS_COMPLETED == json("completed")); + + // invalid json + const json j = "foo"; + CHECK_THROWS_WITH_AS(j.template get(), "[json.exception.type_error.302] deserialization failed: invalid JSON value \"foo\"", json::type_error); + } +} + #ifdef JSON_HAS_CPP_17 #ifndef JSON_USE_IMPLICIT_CONVERSIONS TEST_CASE("std::optional")