Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1074,7 +1074,7 @@ Just as in [Arbitrary Type Conversions](#arbitrary-types-conversions) above,

Other Important points:

- When using `get<ENUM_TYPE>()`, undefined JSON values will default to the first pair specified in your map. Select this default pair carefully.
- When using `get<ENUM_TYPE>()`, 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)
Expand Down
1 change: 1 addition & 0 deletions docs/mkdocs/docs/api/macros/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 102 additions & 0 deletions docs/mkdocs/docs/api/macros/nlohmann_json_serialize_enum_strict.md
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
nugentcaillin marked this conversation as resolved.

## 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"

- When using [`get<ENUM_TYPE>()`](../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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#include <iostream>
#include <nlohmann/json.hpp>

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<ns::TaskState>();
auto blue = j_blue.get<ns::Color>();
std::cout << j_running << " -> " << running
<< ", " << j_blue << " -> " << static_cast<int>(blue) << std::endl;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ns::TS_STOPPED -> "stopped", ns::Color::red -> "red"
"running" -> 1, "blue" -> 2
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#include <iostream>
#include <nlohmann/json.hpp>

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<int>(ns::Color::red) << " -> " << j_red << std::endl;

// deserialization
json j_rot = "rot";
auto rot = j_rot.get<ns::Color>();
auto red = j_red.get<ns::Color>();
std::cout << j_rot << " -> " << static_cast<int>(rot) << std::endl;
std::cout << j_red << " -> " << static_cast<int>(red) << std::endl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
0 -> "red"
"rot" -> 0
"red" -> 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include <iostream>
#include <nlohmann/json.hpp>

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<ns::Color>();
}
catch (const json::exception e)
{
std::cout << "deserialization failed: " << e.what() << std::endl;
}

return 0;
}
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 2 additions & 1 deletion docs/mkdocs/docs/features/enum_conversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ Just as in [Arbitrary Type Conversions](arbitrary_types.md) above,
Other Important points:

- When using `get<ENUM_TYPE>()`, 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).
10 changes: 10 additions & 0 deletions docs/mkdocs/docs/home/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/mkdocs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,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
Expand Down
55 changes: 55 additions & 0 deletions include/nlohmann/detail/macro_scope.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename ExceptionType>
void templated_json_throw(ExceptionType exception)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FYI, two alternative versions of this function from previous "strict enum" PRs:

https://github.com/nlohmann/json/pull/4612/changes#diff-ed4d9ac7996b56500f709b366672a844db294ffa884bbbcda8c690e6eb8a7711R245-R260
https://github.com/nlohmann/json/pull/4989/changes#diff-0af3903deeaf48f62fcb01acf3c6702a376ece2bb801a80d806c9757f826f41cR32-R41

The first one basically reimplements JSON_THROW, the second one uses JSON_THROW and is much more focused.

I'm not judging if any of them are better, just pointing out other versions of this same fix for not being able to use JSON_THROW in the macros.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for bringing these up!

I don't think the first would be a good fit as re-implementing JSON_THROW ignores the undef and redef of JSON_THROW if JSON_THROW_USER is defined, and would make the code less maintainable as JSON_THROW would have to be updated in an additional place if changes are needed, so I think it's important that JSON_THROW is called

https://github.com/nugentcaillin/json/blob/21fe58fa43745a846c6987f772094a6a0a4736c0/include/nlohmann/detail/macro_scope.hpp#L188-L192
https://github.com/Ash-Jose/json_issue/blob/12319044db42395e8709fd58a04e79dad51fceb1/include/nlohmann/detail/conversions/from_json.hpp#L32-L40

// override exception macros
#if defined(JSON_THROW_USER)
    #undef JSON_THROW
    #define JSON_THROW JSON_THROW_USER
#endif

As for the second, looking at them side by side they seem roughly equivalent with the exception of the previous one being inlined and templated on BasicJsonType instead of ExceptionType, and hardcoding the specific exception.
https://github.com/nugentcaillin/json/blob/21fe58fa43745a846c6987f772094a6a0a4736c0/include/nlohmann/detail/macro_scope.hpp#L258-L271

// current

/*!
@brief function to wrap JSON_THROW_MACRO - NLOHMANN_SERIALIZE_ENUM_STRICT has a
       compilation warning about there being no arguments to JSON_THROW that depend on
       template arguments otherwise
*/
template<typename ExceptionType>
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;
}

// from previous pull

/* helper for strict enum error reporting */
template<typename BasicJsonType>
inline void throw_enum_error(const BasicJsonType& j, const char* enum_type)
{
    JSON_THROW(::nlohmann::detail::type_error::create(
                   302,
                   std::string("invalid value for ") + enum_type + ": " + j.dump(),
                   &j));
}

I do worry that emulating this would make the change less extensible as hard-coding in the exception would mean that if another strict macro was implemented in the future it would need to implement its own throw function instead of using the current one.

I also don't know if it would make much sense to pass json object when throwing error in to_json as no object would have been created from VA_ARGS, and dump cannot be called on nullptr. It does make a lot of sense in from_json though - it seems in that PR they decided not to throw in to_json.

If that behavior is more desirable I could make a change like this and remove throwing in to_json - it does seem against the spirit of a strict serialization macro to not throw though:

/*!
@brief function to wrap JSON_THROW_MACRO - NLOHMANN_SERIALIZE_ENUM_STRICT has a
       compilation warning about there being no arguments to JSON_THROW that depend on
       template arguments otherwise
*/
template<typename BasicJsonType>
void nlohmann_serialize_enum_throw(const BasicJsonType& j, const char *enum_type)
{
    JSON_THROW(nlohmann::detail::out_of_range::create(
        410,
        std::string("invalid value for") + enum_type + ": " + j.dump(),
        &j));
}

I could also leave the templated function for throwing unchanged and change the call site to have a more descriptive error message in from_json like the function from the previous pull request and do something like this:

template<typename BasicJsonType>
inline void to_json(BasicJsonType& j, const ENUM_TYPE& e)
{
    ...
    // add stringification of enum type
    else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410,"enum value out of range for " + #ENUM_TYPE, nullptr));
}
template<typename BasicJsonType>
inline void from_json(BasicJsonType& j, const ENUM_TYPE& e)
{
    ...
    // add stringification of enum type, json dump and pointer
    else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410,"enum value out of range for " + #ENUM_TYPE + ": " + j.dump(), &j));
}

This would take the more descriptive error message from that PR whilst keeping the function available for use for any future strict macros. I've also left the inline out here since it's a templated function and I don't think inline will change how the compiler handles it at all - happy to add it in though.

Please advise which of these solutions, if any would be most appropriate. I'll start on the last one for now as that seems more appropriate but happy to switch.

{
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<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)) j = it->second; \
else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE, nullptr)); \
} \
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)) e = it->first; \
else templated_json_throw<nlohmann::detail::out_of_range>(nlohmann::detail::out_of_range::create(410,"enum value out of range for " #ENUM_TYPE ": " + j.dump(), &j)); \
}

Comment thread
nugentcaillin marked this conversation as resolved.
// 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
Loading
Loading