Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add strict enum de/serialization macro #4612

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing some words.

The ... macro allows defining a ...
or
The ... macro allows one to define a ...

I can see this is basically the same text as in the original, it should probably be updated there too.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say "pairs" rather than "a pair", and "a JSON serialization" is odd, maybe "the string to be used in the JSON serialization. The "arbitrary pairs can be given as a comma-separated list" sounds weird to me.
Maybe

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<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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth putting in something here like "will currently be ignored but may result in an error in the future"?

- If a JSON value appears more than once in the mapping, only the first occurrence will be used for deserialization,
nlohmann marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"If a string value" since it's not an arbitrary json value but a string....

Oh, OH, it doesn't require that it be a string, does it?

😮

There's nothing in the code that requires that this be a string.

NLOHMANN_JSON_SERIALIZE_ENUM_STRICT(Color, {
  { Red, "red" },
  { Green, nlohmann::json(1) },
  { Blue, nlohmann::json({{{"purple", "people eater"}}})}
})


int main()
{
  nlohmann::json j;
  j = { Red, Green, Blue };
  std::cout << j.dump() << "\n";
  auto j2 = nlohmann::json::parse(j.dump());
  std::vector<Color> vec = j2;
  std::cout << vec[0] << vec[1] << vec[2];
}
["red",1,[{"purple":"people eater"}]]
012

@hnampally Is this intended behavior, or did this just accidentally fall out of the second parameter being BasicJsonType instead of std::string?

If this is intended behavior, it should definitely be documented before someone trips upon it accidentally, or someone relies on it and then someone else "fixes" it.

If it's not intended behavior, then it should be fixed.

Copy link
Contributor Author

@hnampally hnampally Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gregmarr thanks for bringing this up but I am not really sure about the requirements for this feature but I am open for either, @nlohmann please weigh in!

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.4
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include <iostream>
#include <nlohmann/json.hpp>

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()
{

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the empty line.

Also: move line

json j_yellow = "yellow";

above the comment

// deserialization

// 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[json.exception.type_error.302] can't deserialize - invalid json value : "yellow"
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include <iostream>
#include <nlohmann/json.hpp>

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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[json.exception.type_error.302] can't serialize - enum value 3 out of range
19 changes: 19 additions & 0 deletions docs/mkdocs/docs/features/enum_conversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
})
```
57 changes: 57 additions & 0 deletions include/nlohmann/detail/macro_scope.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,63 @@
e = ((it != std::end(m)) ? it : std::begin(m))->first; \
}

NLOHMANN_JSON_NAMESPACE_BEGIN
namespace detail
{
template<typename T>
[[noreturn]] inline void json_throw_from_serialize_macro(T&& exception)
{
#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) || defined(EXCEPTIONS)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When defining JSON_THROW in the same file, we use the following code to detect exceptions. Please use the same here to avoid issues.

#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh okay, thanks!

throw std::forward<T>(exception);
#else
// Forward the exception (even if unused) and abort
std::forward<T>(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<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); \
nlohmann::detail::json_throw_from_serialize_macro(nlohmann::detail::type_error::create(302, nlohmann::detail::concat("can't serialize - enum value ", std::to_string(value), " out of range"), &j)); \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the exception message. What about

serialization failed: enum value ... is out of range

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that sounds better, thanks!

} \
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)) \
nlohmann::detail::json_throw_from_serialize_macro(nlohmann::detail::type_error::create(302, nlohmann::detail::concat("can't deserialize - invalid json value : ", j.dump()), &j)); \
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above:

deserialization failed: invalid JSON value '...'

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.

Expand Down
57 changes: 57 additions & 0 deletions single_include/nlohmann/json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename T>
[[noreturn]] inline void json_throw_from_serialize_macro(T&& exception)
{
#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) || defined(EXCEPTIONS)
throw std::forward<T>(exception);
#else
// Forward the exception (even if unused) and abort
std::forward<T>(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<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); \
nlohmann::detail::json_throw_from_serialize_macro(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)) \
nlohmann::detail::json_throw_from_serialize_macro(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.

Expand Down
66 changes: 66 additions & 0 deletions tests/src/unit-conversions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<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")
Expand Down
Loading