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 conversion from/to std::optional #2117

Closed
wants to merge 16 commits into from
Closed

Conversation

nlohmann
Copy link
Owner

Closes #1749.

@nlohmann nlohmann added this to the Release 3.8.0 milestone May 16, 2020
@nlohmann nlohmann self-assigned this May 16, 2020
@coveralls
Copy link

coveralls commented May 16, 2020

Coverage Status

Coverage remained the same at 100.0% when pulling 7ffd3ae on feature/optional into 68c3696 on develop.

@dota17
Copy link
Contributor

dota17 commented May 26, 2020

I am working for checking these CI tasks.

Restart the travis CI tasks, and then they would be fine.
This Appveyor CI task may be ok after we restart, it seems like a network error.

And about other failed Appveyor CI tasks, I find the difference is CXX_FLAGS=/permissive- /std:c++latest. Maybe it is the problem, and I would try to fix it.

@nlohmann
Copy link
Owner Author

I restarted all jobs.

@nlohmann
Copy link
Owner Author

The same AppVeyor jobs keep failing. Any ideas?

@dota17
Copy link
Contributor

dota17 commented May 27, 2020

More Info :
The std::optional code maybe not included. I had added logs in unit-conversions.cpp to check if the testcases of std::optional had been run:

    ....
    SECTION("traditional enum")
    {
        // check normal testcase had been run, result : print, run
        std::cout << "traditional enum" << std::endl; 
        ...
    }
#ifdef JSON_HAS_CPP_17
TEST_CASE("std::optional")
{
    SECTION("null")
    {
        // check the C++17 and testcase, reslut : not print, not run
        std::cout << "null" << std::endl; 
....

It seems that all successful jobs hadn't compile the std::optional code so that there are no compiler errors and the testcases hadn't been run. If compiled, maybe they would had same errors.

@nlohmann
Copy link
Owner Author

Yes, the <optional> tests are only executed in C++17 mode. Strangely, not all of them fail, but only a few of them.

@dota17
Copy link
Contributor

dota17 commented May 27, 2020

Yes, the tests are only executed in C++17 mode. Strangely, not all of them fail, but only a few of them.

I find that the <optional> tests are only executed in VS 2017 with CXX_FLAGS=/permissive- /std:c++latest. I think they should be also executed in VS2019, but didn't. Thus, some success may be fake because they didn't run the tests.

What's more, in VS 2017 with CXX_FLAGS=/permissive- /std:c++latest jobs, they didn't call the method from_json(const BasicJsonType& j, std::optional<T<& opt).

The actual behavior:

CHECK(std::optional<std::string>(j_string) == opt_string);  // call from_json(const BasicJsonType& j, ConstructibleStringType& s)
CHECK(std::optional<bool>(j_bool) == opt_bool); // call from_json(const BasicJsonType& j, typename BasicJsonType::boolean_t& b) 
CHECK(std::optional<int>(j_number) == opt_int); // call from_json(const BasicJsonType& j, ArithmeticType& val)

And the test SECTION("null") didn't call the expected method either.

  1. We should ensure which envs/jobs would execute the <optional> tests.
  2. It seems that the implement of from_json(const BasicJsonType& j, std::optional<T<& opt) doesn't work. We should fix it so that CHECK(std::optional<int>(j_number) == opt_int); calls the expected method. - I have no idea now.

@nlohmann
Copy link
Owner Author

Since I don't use MSVC myself, I cannot debug this further. I will drop this feature from 3.8.0 to proceed. Feel free to dig in, but I will not pursue this issue in the moment.

@stale
Copy link

stale bot commented Jul 11, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@nlohmann
Copy link
Owner Author

Wow, thanks for the detailed analysis! What do you propose to do?

@karzhenkov
Copy link
Contributor

karzhenkov commented Aug 20, 2020

I don't see any way to achieve desired conversion sequence for direct initialization when implicit conversions of json are enabled. We have no control over constructors of "external" target classes such as std::vector. Conversion operators defined in json are included in the candidate set only for copy initialization which uses a different syntax, e. g. std::vector<...> v = j instead of std::vector<...>(j).

The above only applies to "conforming" compilers, but each compiler vendor has their own approach to interpretation of standard. It will take time to reach a consensus on subtle topics (and on some trivial topics too). Anyway, it is dangerous to rely on the "new" overload resolution algorithm provided by GCC and Clang with -std=c++17. Language improvements should not change the meaning of the program. Meanwhile, the example discussed on Bugzilla compiles to executable that produces different output when compiled with -std=c++14 or -std=c++17. I suspect that there is a significant difference between GCC 8.3 and GCC 8.4 (the latter is used by Travis, but is not avaliaible at godbolt).

Maybe it is reasonable to support std::optional only with JSON_USE_IMPLICIT_CONVERSIONS defined as 0 for a while.

@nlohmann
Copy link
Owner Author

Maybe it is reasonable to support std::optional only with JSON_USE_IMPLICIT_CONVERSIONS defined as 0 for a while.

Yes, I think this is a reasonable approach.

@stale
Copy link

stale bot commented Oct 4, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Oct 4, 2020
@nlohmann nlohmann removed the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Oct 5, 2020
…re/optional

� Conflicts:
�	test/src/unit-conversions.cpp
…re/optional

� Conflicts:
�	test/src/unit-conversions.cpp
std::optional<std::string> opt_null;

CHECK(json(opt_null) == j_null);
CHECK(std::optional<std::string>(j_null) == std::nullopt);
Copy link
Contributor

Choose a reason for hiding this comment

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

Initialization of optional<T> from empty json should be considered as a type_error:

Suggested change
CHECK(std::optional<std::string>(j_null) == std::nullopt);
CHECK_THROWS_AS(std::optional<std::string>(j_null), json::type_error&);

The reason is as follows. If json is implicitly convertible to T, then initialization of optional<T> from json almost necessarily goes through the "forwarding" constructor:

template <class U> optional(U&& value); // unrelated details omitted

This constructor provides the best parameter/argument match by value category, constness and a type. Its parameter binds directly to json in any form. The only "weakness" of the forwarding constructor is that it is a template. A conversion function that would be considered as a better candidate for overload resolution must be non-templated, but this is obviously unacceptable.

The forwarding constructor converts json to T (initializes the latter from the former) and stores the result, so the constructed optional<T> cannot be empty. Such a conversion from empty json to T is a type error (even if T itself is optional<X>, by induction).

The non-standard overload resolution performed by GCC and Clang when enabling C++17 does not change the outcome. The only way to achieve the "natural" behavior is to tune constructors of std::optional<T>, which is beyond the scope of the project.

Copy link
Contributor

@karzhenkov karzhenkov Jan 1, 2021

Choose a reason for hiding this comment

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

There is also the usual way to explicitly convert json to optional<T>. The following will work:

Suggested change
CHECK(std::optional<std::string>(j_null) == std::nullopt);
CHECK(j_null.get<std::optional<std::string>>() == std::nullopt);

Copy link
Contributor

@karzhenkov karzhenkov Jan 2, 2021

Choose a reason for hiding this comment

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

Essentially, there is nothing new here. @dota17 proposed the same in #2213.
However, this approach (treat such initialization as type_error) introduces some inconsistency:

std::optional<int> opt1 = json().get<std::optional<int>>(); // ok: std::nullopt
std::optional<int> opt2 = std::optional<int>(json());       // throws type_error
std::optional<int> opt3 = json();                           // throws type_error

Perhaps it makes sense to throw type_error in the first case too?
The "natural" conversion could be explicitly implemented by dedicated member function:

template <typename T> std::optional<T> get_optional() const;

Copy link
Contributor

Choose a reason for hiding this comment

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

Moreover, as it turns out, this dedicated member function is the only meaningful thing nlohmann::basic_json can provide to support std::optional. Overloads of get<std::optional<T>> should probably be disabled to prevent confusion.

Copy link
Owner Author

Choose a reason for hiding this comment

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

I think the behavior is counter-intuitive. JSON's null is usually considered as "unset" value (in contrast to an empty value). Therefore, I would expect null to convert to std::nullopt of any optional type.

std::vector<std::optional<int>> opt_array = {{1, 2, std::nullopt}};

CHECK(json(opt_array) == j_array);
CHECK(std::vector<std::optional<int>>(j_array) == opt_array);
Copy link
Contributor

Choose a reason for hiding this comment

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

Direct initialization of vector<...> is not suitable here:

Suggested change
CHECK(std::vector<std::optional<int>>(j_array) == opt_array);
CHECK(j_array.get<std::vector<std::optional<int>>>() == opt_array);

Such kind of initialization of vector<...> has not been tested even with simple item types and actually leads to a compile error for the reason considered earlier. For example, the following will not compile unless using GCC or Clang with C++17 enabled (see https://godbolt.org/z/oeooPG):

json j({1, 2, 3});
std::vector<int> v1 = j;         // Copy initialization: ok
auto v2 = std::vector<int>(j);   // Direct initialization: compile error 

std::map<std::string, std::optional<int>> opt_object {{"one", 1}, {"two", 2}, {"zero", std::nullopt}};

CHECK(json(opt_object) == j_object);
CHECK(std::map<std::string, std::optional<int>>(j_object) == opt_object);
Copy link
Contributor

Choose a reason for hiding this comment

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

Direct initialization of map<...> has the same trouble as with vector<...> above:

Suggested change
CHECK(std::map<std::string, std::optional<int>>(j_object) == opt_object);
CHECK(j_object.get<std::map<std::string, std::optional<int>>>() == opt_object);

.travis.yml Outdated Show resolved Hide resolved
include/nlohmann/detail/macro_scope.hpp Outdated Show resolved Hide resolved
karzhenkov added a commit to karzhenkov/json that referenced this pull request Jan 1, 2021
@UndarkAido
Copy link

I don't know exactly how this is being implemented but my ideal implementation would be for a field not in the from_json to be interpreted as std::nullopt and for a std::nullopt to not appear in the to_json result.

(I'm trying to parse object from Discord's API and there's lots of optional fields. Right now I can't use the NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE because the fields just aren't there to parse.)

@nlohmann
Copy link
Owner Author

I'm not sure how to proceed with this PR. Any ideas?

@karzhenkov
Copy link
Contributor

The primary issue we have is the conversion of null json values to std::optional<T>. To fix the counter-intuitive behavior observed here (the cases that throw type_error) the "improvement" of std::optional is needed. Perhaps it is worth to consider a "better" version of optional as a part of this library (see #2229). Otherwise, we will have to adopt the existing counter-intuitive behavior, whether with or without this PR.

@fredemmott
Copy link

fredemmott commented Feb 15, 2021

I don't think this can be directly resolved without edge cases as 'may be null', and 'may be omitted' are distinct concepts in JSON, but people expect std::optional<> to work with both - and they have conflicting requirements.

I think these need to be separate types in C++ - perhaps a new nlohmann::nullable<T> type which acts essentially the same way as std::optional<> except for JSON encoding:

  • a nullable field is mandatory, but may be null
  • an optional field may be entirely missing
  • an optional field may not be null, unless it's an std::optional<nlohmann::nullable<T>>

This would be similar to:

  • foo ?: T (optional) vs foo: T|null (nullable) in TypeScript with strictNullChecks enabled
  • shape(?'foo' => T) (optional) vs shape('foo' => ?T) (nullable) in Hack

@stale
Copy link

stale bot commented Jan 9, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Jan 9, 2022
@fsandhei
Copy link
Contributor

Is there any updates on this?

@stale stale bot removed the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Apr 24, 2023
@nlohmann
Copy link
Owner Author

Unfortunately not - any help welcome!

@fsandhei
Copy link
Contributor

fsandhei commented May 7, 2023

If I may ask, what is the current blocker for this to go in to the library?

I see that the issues with overload resolutions taken up by @karzhenkov seem to still not be addressed by GCC/Clang.

From what I understand there is still also discussions about the case of initializing a std::optional from an empty JSON object.

Also the issues with implicit conversions hell with conversion operators / constructors seem avoidable (?) through JSON_USE_IMPLICIT_CONVERSIONS = 0

I would like to try to contribute to this to help, but I have to admit I'm unsure what I can help with.

@nlohmann
Copy link
Owner Author

nlohmann commented May 7, 2023

The branch first needs to be rebased to the current develop branch.

@nlohmann nlohmann added the please rebase Please rebase your branch to origin/develop label May 7, 2023
@cschreib-ibex
Copy link

cschreib-ibex commented Oct 14, 2024

I don't think this can be directly resolved without edge cases as 'may be null', and 'may be omitted' are distinct concepts in JSON, but people expect std::optional<> to work with both - and they have conflicting requirements.

My two cents:

  1. A variable of type std::optional<T> indicates a C++ variable that may or may not have a value. It is disjoint from any JSON concept. When used as a return type of a function, it means there may or may not be a return value for that function (again, no JSON here). The causes for the lack of return value are defined by the function, not by the return type. Therefore, I think "may be null" and "may be omitted" are cases that should be handled by different functions; they may both return std::optional<T>, but with different semantic.

  2. If we consider that calling j["some_field"].get<T>() where "some_field" doesn't exist is an error today, for all T, then j["some_field"].get<std::optional<T>>() should also be an error. This preserves the invariant that, no matter the template parameter, it's always an error to call get<T>() on a non-existent value. So, get<std::optional<T>>() should only support "may be null".

  3. We also want to support "may be omitted", therefore we can introduce another function try_get<T>() which returns std::optional<T>. This returns nullopt if the value is not present, and otherwise returns get<T>(). Then you can write try_get<std::optional<T>>() for a value that may not be present and may be null. Open question whether try_get<T>() should still throw in case of type mismatch, or return nullopt (in which case its semantic becomes "return T if possible, nullopt otherwise").

All choices are then covered:

j.get<std::string>();                    // must be a string
j.get<std::optional<std::string>>();     // must be a string or null
j.try_get<std::string>();                // may be a string, or may not exist
j.try_get<std::optional<std::string>>(); // may be a string or null, or may not exist

Granted, adding a nullable type would lift any ambiguity, especially with the 4th line above (std::optional<std::optional<std::string>> 👎, std::optional<nullable<std::string>> 👍).

@nlohmann
Copy link
Owner Author

Closed in favor of #4036.

@nlohmann nlohmann closed this Nov 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
please rebase Please rebase your branch to origin/develop release item: ✨ new feature state: help needed the issue needs help to proceed
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[C++17] Allow std::optional to convert to nlohmann::json
10 participants