json: Add option to parse numbers as strings
authorBartek Ryniec <bryniec@fb.com>
Fri, 4 Dec 2015 21:07:15 +0000 (13:07 -0800)
committerfacebook-github-bot-4 <folly-bot@fb.com>
Sun, 6 Dec 2015 03:20:31 +0000 (19:20 -0800)
Summary: Adding a flag in `folly::json::serialization_opts` that allows you to request all numbers to be parsed into strings. This saves you from exceptions being thrown by `folly::parseJson` if it encounters numbers that wouldn't fit in int64.

Reviewed By: yfeldblum

Differential Revision: D2713039

fb-gh-sync-id: 4f3d0d20f3012efd6229adf895200a3901bb4493

folly/json.cpp
folly/json.h
folly/test/JsonTest.cpp

index 4c856598e123bc61a1a8ea8760fdf69ddbf14fdc..63fa2ba08dc7236577d73de4e781f04be132ce3c 100644 (file)
@@ -470,8 +470,10 @@ dynamic parseArray(Input& in) {
 
 dynamic parseNumber(Input& in) {
   bool const negative = (*in == '-');
-  if (negative) {
-    if (in.consume("-Infinity")) {
+  if (negative && in.consume("-Infinity")) {
+    if (in.getOpts().parse_numbers_as_strings) {
+      return "-Infinity";
+    } else {
       return -std::numeric_limits<double>::infinity();
     }
   }
@@ -487,6 +489,11 @@ dynamic parseNumber(Input& in) {
   constexpr const char* minInt = "9223372036854775808";
   constexpr auto maxIntLen = __builtin_strlen(maxInt);
 
+
+  if (*in != '.' && !wasE && in.getOpts().parse_numbers_as_strings) {
+    return integral;
+  }
+
   if (*in != '.' && !wasE) {
     if (LIKELY(!in.getOpts().double_fallback || integral.size() < maxIntLen) ||
          (integral.size() == maxIntLen &&
@@ -511,7 +518,9 @@ dynamic parseNumber(Input& in) {
     end = expPart.end();
   }
   auto fullNum = range(integral.begin(), end);
-
+  if (in.getOpts().parse_numbers_as_strings) {
+    return fullNum;
+  }
   auto val = to<double>(fullNum);
   return val;
 }
@@ -626,8 +635,12 @@ dynamic parseValue(Input& in) {
          in.consume("true") ? true :
          in.consume("false") ? false :
          in.consume("null") ? nullptr :
-         in.consume("Infinity") ? std::numeric_limits<double>::infinity() :
-         in.consume("NaN") ? std::numeric_limits<double>::quiet_NaN() :
+         in.consume("Infinity") ?
+          (in.getOpts().parse_numbers_as_strings ? (dynamic)"Infinity" :
+            (dynamic)std::numeric_limits<double>::infinity()) :
+         in.consume("NaN") ?
+           (in.getOpts().parse_numbers_as_strings ? (dynamic)"NaN" :
+             (dynamic)std::numeric_limits<double>::quiet_NaN()) :
          in.error("expected json value");
 }
 
index 8352abbf969371fd1ba167cfc77310f92c6d35b2..ead3afd01bec45a6618fdb6da217bb06d1269a4d 100644 (file)
@@ -65,6 +65,7 @@ namespace json {
       , double_mode(double_conversion::DoubleToStringConverter::SHORTEST)
       , double_num_digits(0) // ignored when mode is SHORTEST
       , double_fallback(false)
+      , parse_numbers_as_strings(false)
     {}
 
     // If true, keys in an object can be non-strings.  (In strict
@@ -109,6 +110,10 @@ namespace json {
     // Fallback to double when a value that looks like integer is too big to
     // fit in an int64_t. Can result in loss a of precision.
     bool double_fallback;
+
+    // Do not parse numbers. Instead, store them as strings and leave the
+    // conversion up to the user.
+    bool parse_numbers_as_strings;
   };
 
   /*
index 02976cf3887203beff3c58e475b374c1a368b226..d514e8f3af84630844b48ba89f740353060ad371 100644 (file)
@@ -401,6 +401,39 @@ TEST(Json, ParseDoubleFallback) {
         opts).items().begin()->second.asDouble());
 }
 
+TEST(Json, ParseNumbersAsStrings) {
+  folly::json::serialization_opts opts;
+  opts.parse_numbers_as_strings = true;
+  auto parse = [&](folly::fbstring number) {
+    return parseJson(number, opts).asString();
+  };
+
+  EXPECT_EQ("0", parse("0"));
+  EXPECT_EQ("1234", parse("1234"));
+  EXPECT_EQ("3.00", parse("3.00"));
+  EXPECT_EQ("3.14", parse("3.14"));
+  EXPECT_EQ("0.1234", parse("0.1234"));
+  EXPECT_EQ("0.0", parse("0.0"));
+  EXPECT_EQ("46845131213548676854213265486468451312135486768542132",
+      parse("46845131213548676854213265486468451312135486768542132"));
+  EXPECT_EQ("-468451312135486768542132654864684513121354867685.5e4",
+      parse("-468451312135486768542132654864684513121354867685.5e4"));
+  EXPECT_EQ("6.62607004e-34", parse("6.62607004e-34"));
+  EXPECT_EQ("6.62607004E+34", parse("6.62607004E+34"));
+  EXPECT_EQ("Infinity", parse("Infinity"));
+  EXPECT_EQ("-Infinity", parse("-Infinity"));
+  EXPECT_EQ("NaN", parse("NaN"));
+
+  EXPECT_THROW(parse("ThisIsWrong"), std::runtime_error);
+  EXPECT_THROW(parse("34-2"), std::runtime_error);
+  EXPECT_THROW(parse(""), std::runtime_error);
+  EXPECT_THROW(parse("-"), std::runtime_error);
+  EXPECT_THROW(parse("34-e2"), std::runtime_error);
+  EXPECT_THROW(parse("34e2.4"), std::runtime_error);
+  EXPECT_THROW(parse("infinity"), std::runtime_error);
+  EXPECT_THROW(parse("nan"), std::runtime_error);
+}
+
 TEST(Json, SortKeys) {
   folly::json::serialization_opts opts_on, opts_off;
   opts_on.sort_keys = true;