nlohmann / json

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

Have a json::type_error exception because of JSON object #1540

Closed DragonOsman closed 5 years ago

DragonOsman commented 5 years ago

@nlohmann

Hi again.

What I want to achieve is pretty much the same as I said in the other issue I opened here, except now I just have a different kind of json::exception being thrown that I'm catching. The files I want to read from each have an actual JSON object in them, which looks like this:

{
    "success": "true",
    "currencies": {
        "AED": "United Arab Emirates dirham - \u062f.\u0625",
        "AFN": "Afghan afghani - \u060b",
        "ALL": "Albanian lek - Lek",
        "AMD": "Armenian dram - \u058f",
        "ANG": "Netherlands Antillean guilder - \u0192",
        "AOA": "Angolan kwanza - Kz",
        "ARS": "Argentine peso - N$",
        "AUD": "Australian dollar - AU$",
        "AWG": "Aruban florin - \u0192",
        "AZN": "Azerbaijani manat - \u20bc",
        "BAM": "Bosnia and Herzegovina convertible mark - KM",
        "BBD": "Barbadian dollar - Bds$",
        "BDT": "Bangladeshi taka - \u09f3",
        "BGN": "Bulgarian lev - \u043b\u0432",
        "BHD": "Bahraini dinar - \u062f.\u0628",
        "BIF": "Burundian franc - Fr",
        "BMD": "Bermudian dollar - BD$",
        "BND": "Brunei dollar - B$",
        "BOB": "Bolivian boliviano - $b",
        "BRL": "Brazilian real - R$",
        "BSD": "Bahamian dollar - B$",
        "BTN": "Bhutanese ngultrum - Nu.",
        "BWP": "Botswana pula - P",
        "BYN": "Belarusian ruble - Br",
        "BYR": "Belarusian ruble - Br",
        "BZD": "Belize dollar - BZ$",
        "CAD": "Canadian dollar - C$",
        "CDF": "Congolese franc - Fr",
        "CHF": "Swiss franc - CHF",
        "CLF": "Chilean Unit of Account - UF",
        "CLP": "Chilean peso - CLP$",
        "CNY": "Chinese yuan - \u5143",
        "COP": "Colombian peso - COL$",
        "CRC": "Costa Rican col\u00f3n - \u20a1",
        "CRYPTO_ADA": "Cardano",
        "CRYPTO_AE": "Aeternity",
        "CRYPTO_AION": "Aion",
        "CRYPTO_AOA": "Aurora",
        "CRYPTO_ARDR": "Ardor",
        "CRYPTO_ARK": "Ark",
        "CRYPTO_BAT": "Basic Attention Token",
        "CRYPTO_BCD": "Bitcoin Diamond",
        "CRYPTO_BCH": "Bitcoin Cash",
        "CRYPTO_BCN": "Bytecoin",
        "CRYPTO_BNB": "Binance Coin",
        "CRYPTO_BNT": "Bancor",
        "CRYPTO_BTC": "Bitcoin",
        "CRYPTO_BTG": "Bitcoin Gold",
        "CRYPTO_BTM": "Bytom",
        "CRYPTO_BTS": "BitShares",
        "CRYPTO_CMT": "CyberMiles",
        "CRYPTO_CNX": "Cryptonex",
        "CRYPTO_DAI": "Dai",
        "CRYPTO_DASH": "Dash",
        "CRYPTO_DCN": "Dentacoin",
        "CRYPTO_DCR": "Decred",
        "CRYPTO_DGB": "DigiByte",
        "CRYPTO_DGD": "DigixDAO",
        "CRYPTO_DGTX": "Digitex Futures",
        "CRYPTO_DOGE": "Dogecoin",
        "CRYPTO_DROP": "Dropil",
        "CRYPTO_ELF": "aelf",
        "CRYPTO_EOS": "EOS",
        "CRYPTO_ETC": "Ethereum Classic",
        "CRYPTO_ETH": "Ethereum",
        "CRYPTO_ETN": "Electroneum",
        "CRYPTO_ETP": "Metaverse ETP",
        "CRYPTO_FUN": "FunFair",
        "CRYPTO_GNT": "Golem",
        "CRYPTO_GXS": "GXChain",
        "CRYPTO_HC": "HyperCash",
        "CRYPTO_HOT": "Holo",
        "CRYPTO_HT": "Huobi Token",
        "CRYPTO_ICX": "ICON",
        "CRYPTO_IOST": "IOST",
        "CRYPTO_KCS": "KuCoin Shares",
        "CRYPTO_KMD": "Komodo",
        "CRYPTO_LINK": "Chainlink",
        "CRYPTO_LOOM": "Loom Network",
        "CRYPTO_LRC": "Loopring",
        "CRYPTO_LSK": "Lisk",
        "CRYPTO_LTC": "Litecoin",
        "CRYPTO_MAID": "MaidSafeCoin",
        "CRYPTO_MANA": "Decentraland",
        "CRYPTO_MCO": "Crypto.com",
        "CRYPTO_MGO": "MobileGo",
        "CRYPTO_MIOTA": "IOTA",
        "CRYPTO_MITH": "Mithril",
        "CRYPTO_MKR": "Maker",
        "CRYPTO_MONA": "MonaCoin",
        "CRYPTO_NANO": "Nano",
        "CRYPTO_NAS": "Nebulas",
        "CRYPTO_NEO": "NEO",
        "CRYPTO_NEXO": "Nexo",
        "CRYPTO_NPXS": "Pundi X",
        "CRYPTO_NXT": "Nxt",
        "CRYPTO_OMG": "OmiseGO",
        "CRYPTO_ONT": "Ontology",
        "CRYPTO_PAX": "Paxos Standard",
        "CRYPTO_PAY": "TenX",
        "CRYPTO_PIVX": "PIVX",
        "CRYPTO_POLY": "Polymath",
        "CRYPTO_PPT": "Populous",
        "CRYPTO_QASH": "QASH",
        "CRYPTO_QTUM": "Qtum",
        "CRYPTO_R": "Revain",
        "CRYPTO_RDD": "ReddCoin",
        "CRYPTO_REP": "Augur",
        "CRYPTO_RVN": "Ravencoin",
        "CRYPTO_SC": "Siacoin",
        "CRYPTO_SNT": "Status",
        "CRYPTO_STEEM": "Steem",
        "CRYPTO_STRAT": "Stratis",
        "CRYPTO_TRX": "TRON",
        "CRYPTO_TUSD": "TrueUSD",
        "CRYPTO_USDC": "USD Coin",
        "CRYPTO_USDT": "Tether",
        "CRYPTO_VERI": "Veritaseum",
        "CRYPTO_VET": "VeChain",
        "CRYPTO_WAN": "Wanchain",
        "CRYPTO_WAVES": "Waves",
        "CRYPTO_WAX": "WAX",
        "CRYPTO_WTC": "Waltonchain",
        "CRYPTO_XEM": "NEM",
        "CRYPTO_XET": "ETERNAL TOKEN",
        "CRYPTO_XLM": "Stellar",
        "CRYPTO_XMR": "Monero",
        "CRYPTO_XRP": "XRP",
        "CRYPTO_XTZ": "Tezos",
        "CRYPTO_XVG": "Verge",
        "CRYPTO_ZEC": "Zcash",
        "CRYPTO_ZEN": "Horizen",
        "CRYPTO_ZIL": "Zilliqa",
        "CRYPTO_ZRX": "0x",
        "CUC": "Cuban convertible peso - CUC$",
        "CUP": "Cuban peso - $MN",
        "CVE": "Cape Verdean escudo - Esc",
        "CZK": "Czech koruna - K\u010d",
        "DJF": "Djiboutian franc - Fr",
        "DKK": "Danish krone - kr",
        "DOP": "Dominican peso - RD$",
        "DZD": "Algerian dinar - \u062f.\u062c",
        "EGP": "Egyptian pound - \u00a3",
        "ERN": "Eritrean nakfa - Nfk",
        "ETB": "Ethiopian birr - Br",
        "EUR": "Euro - \u20ac",
        "FJD": "Fijian dollar - FJ$",
        "FKP": "Falkland Islands pound - \u00a3",
        "GBP": "British pound - \u00a3",
        "GEL": "Georgian lari - \u20be",
        "GGP": "Guernsey pound - \u00a3",
        "GHS": "Ghanaian cedi - \u20b5",
        "GIP": "Gibraltar pound - \u00a3",
        "GMD": "Gambian dalasi - D",
        "GNF": "Guinean franc - Fr",
        "GTQ": "Guatemalan quetzal - Q",
        "GYD": "Guyanese dollar - GY$",
        "HKD": "Hong Kong dollar - HK$",
        "HNL": "Honduran lempira - L",
        "HRK": "Croatian kuna - kn",
        "HTG": "Haitian gourde - G",
        "HUF": "Hungarian forint - Ft",
        "IDR": "Indonesian rupiah - Rp",
        "ILS": "Israeli new shekel - \u20aa",
        "IMP": "Manx pound - \u00a3",
        "INR": "Indian rupee - \u20b9",
        "IQD": "Iraqi dinar - \u0639.\u062f",
        "IRR": "Iranian rial - \ufdfc",
        "ISK": "Icelandic kr\u00f3na - kr",
        "JEP": "Jersey pound - \u00a3",
        "JMD": "Jamaican dollar - J$",
        "JOD": "Jordanian dinar - \u062f.\u0627",
        "JPY": "Japanese yen - \u00a5",
        "KES": "Kenyan shilling - Sh",
        "KGS": "Kyrgyzstani som - \u0441",
        "KHR": "Cambodian riel - \u17db",
        "KMF": "Comorian franc - Fr",
        "KPW": "North Korean won - \u20a9",
        "KRW": "South Korean won - \u20a9",
        "KWD": "Kuwaiti dinar - \u062f.\u0643",
        "KYD": "Cayman Islands dollar - CI$",
        "KZT": "Kazakhstani tenge - \u20b8",
        "LAK": "Lao kip - \u20ad",
        "LBP": "Lebanese pound - \u0644.\u0644",
        "LKR": "Sri Lankan rupee - Rs",
        "LRD": "Liberian dollar - LD$",
        "LSL": "Lesotho loti - L",
        "LTL": "Lithuania Litas - Lt",
        "LVL": "Latvia Lat - \u200eLs",
        "LYD": "Libyan dinar - \u0644.\u062f",
        "MAD": "Moroccan dirham - \u062f.\u0645.",
        "MDL": "Moldovan leu - L",
        "MGA": "Malagasy ariary - Ar",
        "MKD": "Macedonian denar - \u0434\u0435\u043d",
        "MMK": "Burmese kyat - Ks",
        "MNT": "Mongolian t\u00f6gr\u00f6g - \u20ae",
        "MOP": "Macanese pataca - P",
        "MRO": "Mauritanian ouguiya - UM",
        "MUR": "Mauritian rupee - \u20a8",
        "MVR": "Maldivian rufiyaa - .\u0783",
        "MWK": "Malawian kwacha - MK",
        "MXN": "Mexican peso - Mex$",
        "MYR": "Malaysian ringgit - RM",
        "MZN": "Mozambican metical - MT",
        "NAD": "Namibian dollar - N$",
        "NGN": "Nigerian naira - \u20a6",
        "NIO": "Nicaraguan c\u00f3rdoba - C",
        "NOK": "Norwegian krone - kr",
        "NPR": "Nepalese rupee - \u20a8",
        "NZD": "New Zealand dollar - NZ$",
        "OMR": "Omani rial - \ufdfc",
        "PAB": "Panamanian balboa - B\/.",
        "PEN": "Peruvian sol - S\/.",
        "PGK": "Papua New Guinean kina - K",
        "PHP": "Philippine piso - \u20b1",
        "PKR": "Pakistani rupee - \u20a8",
        "PLN": "Polish z\u0142oty - z\u0142",
        "PYG": "Paraguayan guaran\u00ed - \u20b2",
        "QAR": "Qatari riyal - \ufdfc",
        "RON": "Romanian leu - lei",
        "RSD": "Serbian dinar - \u0434\u0438\u043d.",
        "RUB": "Russian ruble - \u20bd",
        "RWF": "Rwandan franc - Fr",
        "SAR": "Saudi riyal - \u0631.\u0633",
        "SBD": "Solomon Islands dollar - SI$",
        "SCR": "Seychellois rupee - \u20a8",
        "SDG": "Sudanese pound - \u202b\u062c.\u0633.\u202c",
        "SEK": "Swedish krona - kr",
        "SGD": "Singapore dollar - S$",
        "SHP": "Saint Helena pound - \u00a3",
        "SLL": "Sierra Leonean leone - Le",
        "SOS": "Somali shilling - Sh",
        "SRD": "Surinamese dollar - Sr$",
        "STD": "South Sudanese pound - \u00a3",
        "SVC": "S\u00e3o Tom\u00e9 and Pr\u00edncipe dobra - Db",
        "SYP": "Syrian pound - \u00a3",
        "SZL": "Swazi lilangeni - L",
        "THB": "Thai baht - \u0e3f",
        "TJS": "Tajikistani somoni - \u0405\u041c",
        "TMT": "Turkmenistan manat - m",
        "TND": "Tunisian dinar - \u062f.\u062a",
        "TOP": "Tongan pa\u02bbanga - T",
        "TRY": "Turkish lira - \u20ba",
        "TTD": "Trinidad and Tobago dollar - TT$",
        "TWD": "New Taiwan dollar - NT$",
        "TZS": "Tanzanian shilling - Sh",
        "UAH": "Ukrainian hryvnia - \u20b4",
        "UGX": "Ugandan shilling - Sh",
        "USD": "United States dollar - US$",
        "UYU": "Uruguayan peso - $U",
        "UZS": "Uzbekistani so\u02bbm - \u0441\u045e\u043c",
        "VEF": "Venezuelan bol\u00edvar soberano - Bs.",
        "VND": "Vietnamese \u0111\u1ed3ng - \u20ab",
        "VUV": "Vanuatu vatu - Vt",
        "WST": "Samoan t\u0101l\u0101 - T",
        "XAF": "Central African CFA franc - Fr",
        "XAG": "Silver Ounce - oz",
        "XAU": "Gold Ounce - oz",
        "XCD": "Eastern Caribbean dollar - EC$",
        "XDR": "IMF Special Drawing Rights - SDR",
        "XOF": "West African CFA franc - Fr",
        "XPF": "CFP franc - Fr",
        "YER": "Yemeni rial - \ufdfc",
        "ZAR": "South African rand - R",
        "ZMK": "Zambian Kwacha - ZK",
        "ZMW": "Zambian kwacha - ZK",
        "ZWL": "Zimbabwean dollar - Z$"
    }
}

I'm working on a currency converter application, and the JSON object is a currency list from the currency API I'm using . What I want to do in this part of the app is basically to compare the objects. Right now they're the same, but the idea is to make a new request to the currency API to get the latest currency list and cache it if the contents of one object are ever different from those of the other.

As the title says, I have an exception of type json::type_error that says that it expected a string but got an object. Anyway, you can see what I've tried so far in my code that I just updated on GitHub here, where I have that exception being thrown in the function cache_storage::query_list on line 805, and also in the function handle_request on line 485 (I have to take out the function calls json::parse(ifs1) and json::parse(ifs2) in the query_list function to be able to see the one in handle_request, but they're both there). It's breaking on json::parse(ifs1), but it'd break on the other one if this one weren't there.

I'm using Windows 10 Home Single Language, x64. Version and build same as before. Compiler is MSVC in Visual Studio 2017, the latest release version.

I'm using the version of the library from the latest branch.

I want to know what I'm wrong and how to correctly pass a JSON object to json::parse such that it doesn't throw a type_error exception. And I also want to know why what I'm doing in handle_request that's causing that exception to be thrown. Any help would be appreciated.

Edit: Changed the code a bit, but it's basically still the same. Just that the line the exception is coming from in cache_storage::query_list is now 805, since I moved the initialization of the JSON objects.

nlohmann commented 5 years ago

What is the exact type and message of the type_error exception?

DragonOsman commented 5 years ago

I actually got it figured out. There was an exception here:

found = m_cache.insert_or_assign(found, mapkey, std::make_pair(std::chrono::steady_clock::now(), res.body()));

and also before where I was using json::parse explicitly. It said something like "json::type_error: type should be string, but is object".

There was also a weird problem where even though the JSON object looked fine when I printed it in the function I'm querying the API in, when I printed the response before sending it to the client-side code, it got explicit newline characters and quotes in it.

What I have now is this:

// This function queries the currency API for a list of currencies every hour
// Param mapkey: used to search the cache cache storage; if an entry with that key isn't already there
// an initial query will be made to the API with its results cached
const json &cache_storage::query_list(const char *currencykey, const std::string &mapkey, const json &sentry)
{
    boost::beast::error_code ec;
    auto found = m_cache.find(mapkey);
    try
    {
        if (found == m_cache.end() || (std::chrono::steady_clock::now() - found->second.first) > m_duration)
        {
            std::string host{ "bankersalgo.com" }, api_endpoint{ "/currencies_names" }, key{ currencykey };
            std::string target = api_endpoint + '/' + key;
            std::string port{ "443" };
            int version = 11;

            // The io_context is required for all IO
            boost::asio::io_context ioc;

            // The SSL context is required, and holds certificates
            ssl::context ctx{ ssl::context::tlsv12_client };

            // These objects perform our IO
            tcp::resolver resolver{ ioc };
            ssl::stream<tcp::socket> stream{ ioc, ctx };

            // Set SNI Hostname (many hosts need this to handshake successfully)
            if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str()))
            {
                boost::system::error_code ec{ static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category() };
                throw boost::system::system_error{ ec };
            }

            // Look up the domain name
            const auto results = resolver.resolve(host, port);

            // This holds the root certificate used for verification
            load_root_certificates(ctx);

            // Verify the remote server's certificate
            ctx.set_verify_mode(ssl::verify_peer);

            // Make the connection on the IP address we get from a lookup
            boost::asio::connect(stream.next_layer(), results.begin(), results.end());

            // Perform the SSL handshake
            stream.handshake(ssl::stream_base::client, ec);
            if (ec)
            {
                std::cerr << "Lines 827 and 828:\n";
                fail(ec, "handshake");
            }

            // Set up an HTTP GET request message
            http::request<http::string_body> req{ http::verb::get, target, version };
            req.set(http::field::host, host);
            req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
            req.set(http::field::content_type, "application/json");
            req.set(http::field::accept, "application/json");

            // Send the HTTP request to the remote host
            http::write(stream, req);

            // This buffer is used for reading and must be persisted
            boost::beast::flat_buffer buffer;

            // Declare a container to hold the response
            http::response<http::string_body> res;

            // Receive the HTTP response
            http::read(stream, buffer, res);
            found = m_cache.insert_or_assign(found, mapkey, std::make_pair(std::chrono::steady_clock::now(), res.body()));

            // Gracefully close the stream
            boost::system::error_code ec;
            stream.shutdown(ec);
            if (ec == boost::asio::error::eof)
            {
                // Rationale:
                // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error
                ec.assign(0, ec.category());
            }
            if (ec)
            {
                throw boost::system::system_error{ ec };
            }

            // If we get here then the connection is closed gracefully
        }
        return found->second.second;
    }
    catch (const std::exception& e)
    {
        std::cerr << "Line 867: Error: " << e.what() << '\n';
    }
    return sentry;
}

This function queries the currency API for a list of currencies and returns it to handle_request so that it can send the list to the client-side code to displayed on the dropdown menus on the HTML form that appears on the info window (the app GUI is a Google Map).

There's also a function that queries the API for conversion rates:

// This function queries the currency API after making sure
// that the stored result(s) is/are old enough
// It also makes a new query to the API if needed
const json &cache_storage::query_rate(const std::string &query_data, const char *currencykey, const json &sentry)
{
    auto found = m_cache.find(query_data);
    boost::beast::error_code ec;
    try
    {
        if (found == m_cache.end() || (std::chrono::steady_clock::now() - found->second.first) > m_duration)
        {
            std::string host{ "bankersalgo.com" }, api_endpoint{ "/apirates2" }, key{ currencykey };
            std::string target = api_endpoint + '/' + key + '/' + query_data;
            std::string port{ "443" };
            int version = 11;

            // The io_context is required for all IO
            boost::asio::io_context ioc;

            // The SSL context is required, and holds certificates
            ssl::context ctx{ ssl::context::tlsv12_client };

            // These objects perform our IO
            tcp::resolver resolver{ ioc };
            ssl::stream<tcp::socket> stream{ ioc, ctx };

            // Set SNI Hostname (many hosts need this to handshake successfully)
            if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str()))
            {
                boost::system::error_code ec{ static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category() };
                throw boost::system::system_error{ ec };
            }

            // Look up the domain name
            const auto results = resolver.resolve(host, port);

            // This holds the root certificate used for verification
            load_root_certificates(ctx);

            // Verify the remote server's certificate
            ctx.set_verify_mode(ssl::verify_peer);

            // Make the connection on the IP address we get from a lookup
            boost::asio::connect(stream.next_layer(), results.begin(), results.end());

            // Perform the SSL handshake
            stream.handshake(ssl::stream_base::client, ec);
            if (ec)
            {
                std::cerr << "Lines 725 and 726:\n";
                fail(ec, "handshake");
            }

            // Set up an HTTP GET request message
            http::request<http::string_body> req{ http::verb::get, target, version };
            req.set(http::field::host, host);
            req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
            req.set(http::field::content_type, "application/json");
            req.set(http::field::accept, "application/json");

            // Send the HTTP request to the remote host
            http::write(stream, req);

            // This buffer is used for reading and must be persisted
            boost::beast::flat_buffer buffer;

            // Declare a container to hold the response
            http::response<http::string_body> res;

            // Receive the HTTP response
            http::read(stream, buffer, res);
            found = m_cache.insert_or_assign(found, query_data, std::make_pair(std::chrono::steady_clock::now(), json::parse(res.body())));

            // Gracefully close the stream
            boost::system::error_code ec;
            stream.shutdown(ec);
            if (ec == boost::asio::error::eof)
            {
                // Rationale:
                // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error
                ec.assign(0, ec.category());
            }
            if (ec)
            {
                throw boost::system::system_error{ ec };
            }

            // If we get here then the connection is closed gracefully
        }
        return found->second.second;
    }
    catch (const std::exception &e)
    {
        std::cerr << "Line 769: Error: " << e.what() << '\n';
    }
    return sentry;
}

The updated app source code is here. It's a web server app I host on my own computer so I'm not always running it, but when it's running it's available on this address: https://dragonosman.dynu.net:5501/ . You can go to my portfolio site here to see if the server app is running or not (it'll tell you on the My Work page).

I want more work to build the portfolio, but right now that's all there is.

nlohmann commented 5 years ago

Thanks for checking back!