nlohmann / json

JSON for Modern C++
https://json.nlohmann.me
MIT License
42.01k stars 6.63k forks source link

NLOHMANN_JSON_SERIALIZE_ENUM defaulting to the first enum value is surprising behavior #3992

Open bear24rw opened 1 year ago

bear24rw commented 1 year ago

Description

I had a json document with an invalid enum string. I would like an exception to be raised if the string is not a known conversion back to the enum. I don't want to have to add and check an Unknown or Invalid enum value which would only be used for json serialization.

This is well documented: "undefined JSON values will default to the first specified conversion." but is IMO is surprising behavior since we throw json.exception.type_error's for other type conversions if they fail (like converting an integer in the json document to a std::string)

This is maybe only a good idea for strongly typed enum class's.

Reproduction steps

namespace ns
{
enum class Color
{
    red, green, blue // I don't want to define an unknown item just used for json deserialization
};

NLOHMANN_JSON_SERIALIZE_ENUM(Color,
{
    { Color::red, "red" },
    { Color::green, "green" },
    { Color::blue, "blue" }
})
}

json some_color_json = "pink";
try {
    auto color = some_color_json.get<ns::Color>();
} catch (json::type_error& e) {
    printf("Invalid color!\n");
}

Expected vs. actual results

Expected: It prints 'invalid color' Actual: color gets set to Color::red

I believe the behavior I am after can be achieved by with this patch

diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp
index 6248bea..9916aac 100644
--- a/include/nlohmann/detail/macro_scope.hpp
+++ b/include/nlohmann/detail/macro_scope.hpp
@@ -215,7 +215,9 @@
         {                                                                                       \
             return ej_pair.first == e;                                                          \
         });                                                                                     \
-        j = ((it != std::end(m)) ? it : std::begin(m))->second;                                 \
+        if (it == std::end(m))
+            throw ...
+        j = it;
     }                                                                                           \
     template<typename BasicJsonType>                                                            \
     inline void from_json(const BasicJsonType& j, ENUM_TYPE& e)                                 \
@@ -227,7 +229,9 @@
         {                                                                                       \
             return ej_pair.second == j;                                                         \
         });                                                                                     \
-        e = ((it != std::end(m)) ? it : std::begin(m))->first;                                  \
+        if (it == std::end(m))
+            throw ...
+        e = it;
     }

 // Ugly macros to avoid uglier copy-paste when specializing basic_json. They

Minimal code example

No response

Error messages

No response

Compiler and operating system

clang 15.0.7

Library version

v3.11.2

Validation

nlohmann commented 1 year ago

I removed the bug label as this is documented behavior. We cannot change this behavior as it would be a breaking change. I could think of a different macro to achieve this behavior though.

bear24rw commented 1 year ago

We cannot change this behavior as it would be a breaking change.

I agree. I think there is definitely a legitimate usecase for the current behavior for weakly typed enums which might be commonly used with implicit (un-named) enum values, for example:

enum State {
    INIT = 0
    SETUP = 1
    STEP = 2
    // bunch of implicit steps
    DONE = 10
};

if (step == State::STEP + 4) ...

It's less likely someone would be using an enum class in the same way but it's valid.

I could think of a different macro to achieve this behavior though.

It's almost like the current NLOHMANN_JSON_SERIALIZE_ENUM macro works like NLOHMANN_DEFINE_TYPE_*_WITH_DEFAULT. It's too bad we can't rename the exist macro to NLOHMANN_JSON_SERIALIZE_ENUM_WITH_DEFAULT.

Either way I'd be very eager to use a new more strict macro!

bear24rw commented 1 year ago

I could think of a different macro to achieve this behavior though.

Some potential naming options for a new enum courtesy of chatGPT

NLOHMANN_JSON_SERIALIZE_ENUM_STRICT
NLOHMANN_JSON_SERIALIZE_ENUM_CHECKED
NLOHMANN_JSON_SERIALIZE_ENUM_SAFE
NLOHMANN_JSON_SERIALIZE_ENUM_ENSURE
NLOHMANN_JSON_SERIALIZE_ENUM_VALIDATE
NLOHMANN_JSON_SERIALIZE_ENUM_STRICT_SERIALIZATION
NLOHMANN_JSON_SERIALIZE_ENUM_SAFE_DESERIALIZATION
NLOHMANN_JSON_SERIALIZE_ENUM_CHECKED_CONVERSION
NLOHMANN_JSON_SERIALIZE_ENUM_ENSURE_MAPPING
NLOHMANN_JSON_SERIALIZE_ENUM_VALIDATE_VALUES
gregmarr commented 1 year ago

I like NLOHMANN_JSON_SERIALIZE_ENUM_STRICT.

pabloariasal commented 1 year ago

just opened a MR for NLOHMANN_JSON_SERIALIZE_ENUM_STRICT

bear24rw commented 1 year ago

@pabloariasal why was https://github.com/nlohmann/json/pull/3996 closed? I just ran into this issue again and am looking forward to getting your PR merged.

nlohmann commented 11 months ago

Same here.

zagoli commented 6 months ago

Same

nlohmann commented 5 months ago

Any update on this? @pabloariasal