json icon indicating copy to clipboard operation
json copied to clipboard

NLOHMANN_JSON_SERIALIZE_ENUM defaulting to the first enum value is surprising behavior

Open bear24rw opened this issue 2 years ago • 10 comments

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

bear24rw avatar Mar 23 '23 17:03 bear24rw

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.

nlohmann avatar Mar 23 '23 18:03 nlohmann

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 avatar Mar 23 '23 18:03 bear24rw

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

bear24rw avatar Mar 23 '23 21:03 bear24rw

I like NLOHMANN_JSON_SERIALIZE_ENUM_STRICT.

gregmarr avatar Mar 24 '23 00:03 gregmarr

just opened a MR for NLOHMANN_JSON_SERIALIZE_ENUM_STRICT

pabloariasal avatar Mar 25 '23 21:03 pabloariasal

@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.

bear24rw avatar Aug 07 '23 19:08 bear24rw

Same here.

nlohmann avatar Sep 23 '23 16:09 nlohmann

Same

zagoli avatar Feb 23 '24 09:02 zagoli

Any update on this? @pabloariasal

nlohmann avatar Mar 26 '24 16:03 nlohmann