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

[Question] How do I parse JSON into custom types? #1669

Closed
rcdailey opened this issue Jul 12, 2019 · 7 comments
Closed

[Question] How do I parse JSON into custom types? #1669

rcdailey opened this issue Jul 12, 2019 · 7 comments
Labels
kind: question state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated

Comments

@rcdailey
Copy link

Reviewing the documentation, I don't see a lot of helpful examples on more complex parsing scenarios. Really it seems like most of the documentation describes going from types to JSON, but I need to completely deserialize JSON data into C++ objects.

Here is the JSON data I'm working with:

{
  "campaigns": [{
      "name": "patt_coupon",
      "start_date": "2019-01-01",
      "end_date": "2019-12-31",
      "push": "pass.com.company.coupon.5dollar",
      "pull": [
        "pass.com.company.coupon",
        "pass.com.company.coupon_revised"
      ]
    },
    {
      "name": "loyalty",
      "start_date": "2019-01-01",
      "end_date": "2019-12-31",
      "push": "pass.com.company.loyalty.program1",
      "pull": [
        "pass.com.company.loyalty.program1"
      ]
    }
  ]
}

For the campaigns array, I want to parse each element into an array of custom objects:

#include <fstream>
#include <stdexcept>
#include <boost/date_time/gregorian/gregorian.hpp>
#include <fmt/format.h>
#include <nlohmann/json.hpp>

using json = nlohmann::json;
namespace boost
{
   namespace gregorian
   {
      void from_json(json& j, boost::gregorian::date& d)
      {
         std::string dateValue;
         j.get_to(dateValue);
         d = boost::gregorian::from_string(dateValue);
      }
   }
}

class WalletCampaignConfiguration::WalletCampaign
{
public:
   WalletCampaign(json& config)
   {
      config.at("start_date").get_to(startDate);
   }

   boost::gregorian::date startDate, endDate;
   std::string pushPass;
   std::vector<std::string> pullPasses;
};

int main() {
   std::ifstream file{jsonPath}; // this points to the file containing the JSON above
   if (!file)
   {
      throw std::runtime_error{"Unable to open wallet config json: " + jsonPath.string()};
   }

   json config;
   file >> config;

   for (auto const& campaignJson : config.at("campaigns"))
   {
      m_campaigns.push_back(std::make_unique<WalletCampaign>(campaignJson));
   }

   auto const& c = *m_campaigns.at(0);
   std::cout << "start: " << c.startDate << "\n\n";
   std::cout << "end: " << c.endDate << "\n\n";
   std::cout << "push: " << c.pushPass << "\n\n";

   for (auto const& p : c.pullPasses)
   {
      std::cout << "pull: " << p << "\n\n";
   }
}

I'm compiling this using Visual Studio 2019 with C++17 enabled. The code above doesn't compile, with these errors:

1>E:\code\frontend\source\Core\Wallet\Source\Wallet\WalletCampaignConfiguration.cpp(47,30): error C2672:  'nlohmann::basic_json<std::map,std::vector,std::string,bool,int64_t,uint64_t,double,std::allocator,nlohmann::adl_serializer>::get_to': no matching overloaded function found
1>E:\code\frontend\source\Core\Wallet\Source\Wallet\WalletCampaignConfiguration.cpp(47,1): error C2783:  'ValueType &nlohmann::basic_json<std::map,std::vector,std::string,bool,int64_t,uint64_t,double,std::allocator,nlohmann::adl_serializer>::get_to(ValueType &) noexcept(<expr>) const': could not deduce template argument for '__formal'
1>E:\code\frontend\source\Core\External\json\include\nlohmann/json.hpp(2680): message :  see declaration of 'nlohmann::basic_json<std::map,std::vector,std::string,bool,int64_t,uint64_t,double,std::allocator,nlohmann::adl_serializer>::get_to'

I'm not sure why it doesn't compile. But I'm sure it has to do with something about how I'm trying to use the library. Can you give me an example of how to parse this JSON document in an object oriented way as I'm trying to do?

@nlohmann
Copy link
Owner

I don't have time to compile the code in the moment. But one note: The signature of the from_json function must be

void from_json(const json& j, boost::gregorian::date& d)

instead of

void from_json(json& j, boost::gregorian::date& d)

(see https://github.com/nlohmann/json#basic-usage).

@rcdailey
Copy link
Author

Thanks for the quick response. To help you out, I put this code on Wandbox along with your single header file and the above JSON. You can easily compile it that way:

https://wandbox.org/permlink/rgECm599lctiFXkA

Note with your fix it still has problems. The for loop where I construct the unique_ptr to WalletCampaign doesn't work. I guess the elements I'm getting aren't json types. A big part of this is intended to be a code review. If you have the time, I want to know the idiomatic way of doing what I'm trying to do. I feel like I'm doing this the hard way. Your library seems like it should simplify a lot of my boilerplate.

Again I appreciate your time. Let me know if you get a spare moment to code review this. Thanks!

@nlohmann
Copy link
Owner

Your constructor should accept a const reference:

WalletCampaign(const json& config)

That makes the code compile at Wandbox.

You may want to replace the constructor taking a json to a from_json function, but this is just style.

What makes you feel you write boilerplate code?

@rcdailey
Copy link
Author

rcdailey commented Jul 12, 2019

Thanks, your fix was the missing piece. The compiler diagnostic did not make the const issue obvious. Very cryptic output. Maybe you can add a static_assert to detect these issues for easier learning experience? Just a thought...

In terms of boilerplate, I'm still learning things, but so far making JSON elements "optional" is pretty tedious. There's no 1 line solution to this that I've seen so far. It would be nice to see built-in support for std::optional<T>. I have seen some examples where you implement a specialization of adl_serializer, but this should be built-in IMHO. And also adl_serializer can't be specialized with just from_json (I only do one-way conversion, I don't support saving to JSON, only reading from JSON).

Right now I do this:

      boost::gregorian::date startDate;
      auto element = config.find("start_date");
      if (element != config.end())
      {
         element->get_to(startDate);
      }

Would be nicer instead to have something like this:

std::optional<boost::gregorian::date> startDate;
config.try_get_to("start_date", startDate);

at() will throw so I can't use that.

EDIT: Actually my example of find() above doesn't even compile... diagnostic output again is very difficult to comprehend. Doesn't explain the problem.

EDIT2: Looks like it doesn't compile because I was wrapping the date object with std::optional<>. If I remove optional it works.

@rcdailey
Copy link
Author

Any objection to making these part of your library?

namespace nlohmann
{
   template<typename T>
   struct adl_serializer<std::optional<T>>
   {
      static void to_json(json& j, const std::optional<T>& value)
      {
         if (!value)
         {
            j = nullptr;
         }
         else
         {
            // this will call adl_serializer::to_json which will
            // find the free function to_json in T's namespace!
            j = *value; 
         }
      }

      static void from_json(json const& j, std::optional<T>& value)
      {
         if (j.is_null())
         {
            value.reset();
         }
         else
         {
            // same as above, but with adl_serializer<T>::from_json
            value = j.get<T>();
         }
      }
   };

   template<typename T>
   struct adl_serializer<std::unique_ptr<T>>
   {
      static void to_json(json& j, const std::unique_ptr<T>& value)
      {
         if (!value)
         {
            j = nullptr;
         }
         else
         {
            j = *value;
         }
      }

      static void from_json(json const& j, std::unique_ptr<T>& value)
      {
         if (j.is_null())
         {
            value.reset();
         }
         else
         {
            value = std::make_unique<T>();
            *value = j.get<T>();
         }
      }
   };
}

Probably need to add one for shared_ptr too.

@nlohmann
Copy link
Owner

(There is a function contains to check if a key is present in an object. This may clean up code in case you don't want to compare iterators.)

I'm hesitating to add the conversions for optionals and smart pointers right now. I need to understand what this may break first.

@stale
Copy link

stale bot commented Aug 12, 2019

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 Aug 12, 2019
@stale stale bot closed this as completed Aug 19, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind: question state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated
Projects
None yet
Development

No branches or pull requests

2 participants