beached / daw_json_link

Fast, convenient JSON serialization and parsing in C++
https://beached.github.io/daw_json_link/
Boost Software License 1.0
460 stars 30 forks source link

Error parsing empty string field when a data contract has custom nullable member: "An unexpected null value was encountered while serializing" #409

Closed THE-FYP closed 9 months ago

THE-FYP commented 10 months ago

Parsing an empty string of a class results in an error when json_data_contract specialization for that class has a string field and a custom nullable field in its json_member_list. This is only the case when such class has an user-defined constructor. It feels like an empty string field is somehow considered a null value under these conditions. Presence of that custom field in the data doesn't make a difference.

Example to reproduce the issue:

#include <daw/json/daw_json_link.h>
#include <string>
#include <optional>
#include <iostream>

enum class TestEnum : unsigned {
    A,
    B
};

constexpr std::string_view to_string(TestEnum v) {
    switch (v) {
    case TestEnum::A: return "a";
    case TestEnum::B: return "b";
    }
    DAW_UNREACHABLE();
}

constexpr TestEnum from_string(daw::tag_t<TestEnum>, std::string_view sv) {
    if (sv == "a") return TestEnum::A;
    if (sv == "b") return TestEnum::B;
    DAW_UNREACHABLE();
}

struct Test {
    std::string str;
    std::optional<TestEnum> opt;
};

struct TestUserCtor : public Test {
    TestUserCtor(std::string str, std::optional<TestEnum> opt) 
        : Test{std::move(str), std::move(opt)} {
    }
};

namespace daw::json {
    template<>
    struct json_data_contract<Test> {
        using type = json_member_list<
            json_string<"str">,
            json_custom_null<"opt", std::optional<TestEnum>>>;

        static constexpr auto to_json_data(const Test& v) {
            return std::forward_as_tuple(v.str, v.opt);
        }
    };

    template<>
    struct json_data_contract<TestUserCtor> {
        using type = json_member_list<
            json_string<"str">,
            json_custom_null<"opt", std::optional<TestEnum>>>;

        static constexpr auto to_json_data(const TestUserCtor& v) {
            return std::forward_as_tuple(v.str, v.opt);
        }
    };
} // namespace daw::json

int main() {
    static constexpr auto& json1 = R"({ "str": "wtf" })";
    static constexpr auto& json2 = R"({ "str": "" })";
    try {
        {
            const auto cls = daw::json::from_json<Test>(json1); // Ok
            std::cout << "Test 1: " << daw::json::to_json(cls) << '\n';
        }
        {
            const auto cls = daw::json::from_json<TestUserCtor>(json1); // Ok
            std::cout << "Test 2: " << daw::json::to_json(cls) << '\n';
        }
        {
            const auto cls = daw::json::from_json<Test>(json2); // Ok
            std::cout << "Test 3: " << daw::json::to_json(cls) << '\n';
        }
        {
            const auto cls = daw::json::from_json<TestUserCtor>(json2); // Error
            std::cout << "Test 4: " << daw::json::to_json(cls) << '\n';
        }
    }
    catch (const daw::json::json_exception& jex) {
        std::cerr << "Exception thrown by parser: " << jex.reason() << '\n';
    }
    return 0;
}

Output:

Test 1: {"str":"wtf"}
Test 2: {"str":"wtf"}
Test 3: {"str":""}
Exception thrown by parser: An unexpected null value was encountered while serializing
beached commented 9 months ago

Interesting, I tried locally(gcc/clang) and online and it does work. What system are you using, compiler(ver)/os ? https://jsonlink.godbolt.org/z/qM3ddGc89

THE-FYP commented 9 months ago

I'm using MSVC 19.37. I've also tested this with clang on windows and linux, gcc and mingw, no problem with these, so the issue appears to be MSVC-related.

beached commented 9 months ago

410 will close this. There was an issue with an empty range check and it only shows in MSVC because of other characteristics it has. Thanks for finding this bug, it did affect gcc/clang too