add strict enum de/serialization macro

Signed-off-by: Harinath Nampally <harinath922@gmail.com>
This commit is contained in:
Harinath Nampally 2025-01-19 23:51:58 -05:00
parent f06604fce0
commit 06b667cbd5
8 changed files with 395 additions and 1 deletions

View File

@ -0,0 +1,90 @@
# NLOHMANN_JSON_SERIALIZE_ENUM_STRICT
```cpp
#define NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(type, conversion...)
```
The `NLOHMANN_JSON_SERIALIZE_ENUM_STRICT` allows to define 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 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<typename BasicJsonType>
inline void to_json(BasicJsonType& j, const type& e);
template<typename BasicJsonType>
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"
- If an enum value appears more than once in the mapping, only the first occurrence will be used for serialization, subsequent mappings for the same enum value will be ignored.
- If a JSON value appears more than once in the mapping, only the first occurrence will be used for deserialization, subsequent mappings for the same JSON value will be ignored.
- 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:
```
[json.exception.type_error.302] can't serialize - enum value 3 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:
```
[json.exception.type_error.302] can't deserialize - 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.3.

View File

@ -0,0 +1,57 @@
#include <iostream>
#include <nlohmann/json.hpp>
#ifdef __cpp_exceptions
#undef __cpp_exceptions
#define __cpp_exceptions 1
#endif
#ifdef JSON_NOEXCEPTION
#define JSON_NOEXCEPTION 0
#endif
#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION)
#define JSON_THROW(exception) throw exception
#define JSON_TRY try
#define JSON_CATCH(exception) catch(exception)
#define JSON_INTERNAL_CATCH(exception) catch(exception)
#else
#include <cstdlib>
#define JSON_THROW(exception) std::abort()
#define JSON_TRY if(true)
#define JSON_CATCH(exception) if(false)
#define JSON_INTERNAL_CATCH(exception) if(false)
#endif
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()
{
// deserialization
json j_yellow = "yellow";
try
{
auto yellow = j_yellow.template get<ns::Color>();
std::cout << j_yellow << " -> " << static_cast<int>(yellow) << std::endl;
}
catch (const nlohmann::json::exception& e)
{
std::cout << e.what() << std::endl;
}
}

View File

@ -0,0 +1,56 @@
#include <iostream>
#include <nlohmann/json.hpp>
#ifdef __cpp_exceptions
#undef __cpp_exceptions
#define __cpp_exceptions 1
#endif
#ifdef JSON_NOEXCEPTION
#define JSON_NOEXCEPTION 0
#endif
#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION)
#define JSON_THROW(exception) throw exception
#define JSON_TRY try
#define JSON_CATCH(exception) catch(exception)
#define JSON_INTERNAL_CATCH(exception) catch(exception)
#else
#include <cstdlib>
#define JSON_THROW(exception) std::abort()
#define JSON_TRY if(true)
#define JSON_CATCH(exception) if(false)
#define JSON_INTERNAL_CATCH(exception) if(false)
#endif
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<ns::Color>();
std::cout << static_cast<int>(color) << " -> " << j_red << std::endl;
}
catch (const nlohmann::json::exception& e)
{
std::cout << e.what() << std::endl;
}
}

View File

@ -0,0 +1 @@
[json.exception.type_error.302] can't serialize - enum value 3 out of range

View File

@ -27,6 +27,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM( TaskState, {
The [`NLOHMANN_JSON_SERIALIZE_ENUM()` macro](../api/macros/nlohmann_json_serialize_enum.md) declares a set of
`to_json()` / `from_json()` functions for type `TaskState` while avoiding repetition and boilerplate serialization code.
## Usage
```cpp
@ -59,3 +60,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 preffered, 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"},
})
```

View File

@ -242,6 +242,47 @@
e = ((it != std::end(m)) ? it : std::begin(m))->first; \
}
/*!
@brief macro to briefly define a mapping between an enum and JSON
@def NLOHMANN_JSON_SERIALIZE_ENUM
@since version 3.4.0
*/
#define NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(ENUM_TYPE, ...) \
template<typename BasicJsonType> \
inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \
{ \
/* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \
static_assert(std::is_enum<ENUM_TYPE>::value, #ENUM_TYPE " must be an enum!"); \
/* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on <array> */ \
static const std::pair<ENUM_TYPE, BasicJsonType> m[] = __VA_ARGS__; \
auto it = std::find_if(std::begin(m), std::end(m), \
[e](const std::pair<ENUM_TYPE, BasicJsonType>& ej_pair) -> bool \
{ \
return ej_pair.first == e; \
}); \
if (it == std::end(m)) { \
auto value = static_cast<typename std::underlying_type<ENUM_TYPE>::type>(e); \
JSON_THROW(nlohmann::detail::type_error::create(302, nlohmann::detail::concat("can't serialize - enum value ", std::to_string(value), " out of range"), &j)); \
} \
j = it->second; \
} \
template<typename BasicJsonType> \
inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \
{ \
/* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \
static_assert(std::is_enum<ENUM_TYPE>::value, #ENUM_TYPE " must be an enum!"); \
/* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on <array> */ \
static const std::pair<ENUM_TYPE, BasicJsonType> m[] = __VA_ARGS__; \
auto it = std::find_if(std::begin(m), std::end(m), \
[&j](const std::pair<ENUM_TYPE, BasicJsonType>& ej_pair) -> bool \
{ \
return ej_pair.second == j; \
}); \
if (it == std::end(m)) \
JSON_THROW(nlohmann::detail::type_error::create(302, nlohmann::detail::concat("can't deserialize - 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.

View File

@ -2608,6 +2608,47 @@ JSON_HEDLEY_DIAGNOSTIC_POP
e = ((it != std::end(m)) ? it : std::begin(m))->first; \
}
/*!
@brief macro to briefly define a mapping between an enum and JSON
@def NLOHMANN_JSON_SERIALIZE_ENUM
@since version 3.4.0
*/
#define NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(ENUM_TYPE, ...) \
template<typename BasicJsonType> \
inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \
{ \
/* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \
static_assert(std::is_enum<ENUM_TYPE>::value, #ENUM_TYPE " must be an enum!"); \
/* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on <array> */ \
static const std::pair<ENUM_TYPE, BasicJsonType> m[] = __VA_ARGS__; \
auto it = std::find_if(std::begin(m), std::end(m), \
[e](const std::pair<ENUM_TYPE, BasicJsonType>& ej_pair) -> bool \
{ \
return ej_pair.first == e; \
}); \
if (it == std::end(m)) { \
auto value = static_cast<typename std::underlying_type<ENUM_TYPE>::type>(e); \
JSON_THROW(nlohmann::detail::type_error::create(302, nlohmann::detail::concat("can't serialize - enum value ", std::to_string(value), " out of range"), &j)); \
} \
j = it->second; \
} \
template<typename BasicJsonType> \
inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \
{ \
/* NOLINTNEXTLINE(modernize-type-traits) we use C++11 */ \
static_assert(std::is_enum<ENUM_TYPE>::value, #ENUM_TYPE " must be an enum!"); \
/* NOLINTNEXTLINE(modernize-avoid-c-arrays) we don't want to depend on <array> */ \
static const std::pair<ENUM_TYPE, BasicJsonType> m[] = __VA_ARGS__; \
auto it = std::find_if(std::begin(m), std::end(m), \
[&j](const std::pair<ENUM_TYPE, BasicJsonType>& ej_pair) -> bool \
{ \
return ej_pair.second == j; \
}); \
if (it == std::end(m)) \
JSON_THROW(nlohmann::detail::type_error::create(302, nlohmann::detail::concat("can't deserialize - 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.

View File

@ -1657,6 +1657,94 @@ TEST_CASE("JSON to enum mapping")
}
}
#ifdef __cpp_exceptions
#undef __cpp_exceptions
#define __cpp_exceptions 1
#endif
#ifdef JSON_NOEXCEPTION
#define JSON_NOEXCEPTION 0
#endif
#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION)
#define JSON_THROW(exception) throw exception
#define JSON_TRY try
#define JSON_CATCH(exception) catch(exception)
#define JSON_INTERNAL_CATCH(exception) catch(exception)
#else
#include <cstdlib>
#define JSON_THROW(exception) std::abort()
#define JSON_TRY if(true)
#define JSON_CATCH(exception) if(false)
#define JSON_INTERNAL_CATCH(exception) if(false)
#endif
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<cards_strict>(), "[json.exception.type_error.302] can't deserialize - 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<TaskStateStrict>(), "[json.exception.type_error.302] can't deserialize - invalid json value : \"foo\"", json::type_error);
}
}
#ifdef JSON_HAS_CPP_17
#ifndef JSON_USE_IMPLICIT_CONVERSIONS
TEST_CASE("std::optional")
@ -1725,4 +1813,4 @@ TEST_CASE("std::optional")
#ifdef JSON_HAS_CPP_14
#undef JSON_HAS_CPP_14
#endif
DOCTEST_CLANG_SUPPRESS_WARNING_POP
DOCTEST_CLANG_SUPPRESS_WARNING_POP