Skip to main content
Tweeted twitter.com/StackCodeReview/status/1176828833791643648
edited tags
Link
200_success
  • 145.6k
  • 22
  • 191
  • 481
Source Link
Saxpy
  • 73
  • 3

Minimal JSON Parser

Motivation: Partially for fun / learning, but also so I can roll out a custom JSON-like file format for a fighting game I am writing. There are two caveats: you cannot have repeated keys (requires std::unordered_multimap) and the keys are not in order (requires insertion order std::vector on the side, or some external boost lib probably). I am mainly looking for critique on my level of comments and ability to read my code, but everything else I am of course welcome to hear.

#pragma once

#include <list>
#include <map>
#include <string>
#include <variant>
#include <vector>

#include <fstream>
#include <iostream>
#include <sstream>

namespace json {
  class Value;

  // six json data types
  using null_t = std::nullptr_t;
  using bool_t = bool;
  using number_t = std::double_t;
  using string_t = std::string;
  using array_t = std::vector<Value>;
  using object_t = std::map<string_t, Value>;

  using aggregate_t = std::variant<
    null_t,   bool_t,  number_t,
    string_t, array_t, object_t>;

  class Value : protected aggregate_t {
  public:
    using aggregate_t::variant;
    // removes spurious E0291
    Value() = default;
    // converts int into double rather than bool
    Value(int integer) : aggregate_t(static_cast<double>(integer)) {}
    // converts c_string (pointer) into string rather than bool
    Value(const char* c_string) : aggregate_t(std::string(c_string)) {}

  public:
    auto operator[](const string_t& key) -> Value& {
      // transform into object if null
      if (std::get_if<null_t>(this))
        *this = object_t();
      return std::get<object_t>(*this)[key];
    }

    auto operator[](std::size_t key) -> Value& {
      // transform into array if null
      if (std::get_if<null_t>(this))
        *this = array_t();
      if (key >= std::get<array_t>(*this).size())
        std::get<array_t>(*this).resize(key + 1);
      return std::get<array_t>(*this)[key];
    }

    auto save(std::ostream& stream, std::string prefix = "") -> std::ostream& {
      static const std::string SPACING = "  "; // "\t"; // "    ";

      // depending on the type, write to correct value with format to stream
      std::visit([&stream, &prefix](auto&& value) {
        using namespace std;
        using T = decay_t<decltype(value)>;

        if constexpr (is_same_v<T, nullptr_t>)
          stream << "null";
        if constexpr (is_same_v<T, bool_t>)
          stream << (value ? "true" : "false");
        else if constexpr (is_same_v<T, double_t>)
          stream << value;
        else if constexpr (is_same_v<T, string>)
          stream << '"' << value << '"';
        else if constexpr (is_same_v<T, array_t>) {
          stream << "[\n";
          auto [indent, remaining] = make_tuple(prefix + SPACING, value.size());
          // for every json value, indent and print to stream
          for (auto& json : value)
            json.save(stream << indent, indent)
              // if jsons remaining (not last), append comma
              << (--remaining ? ",\n" : "\n");
          stream << prefix << "]";
        }
        else if constexpr (is_same_v<T, object_t>) {
          stream << "{\n";
          auto [indent, remaining] = make_tuple(prefix + SPACING, value.size());
          // for every json value, indent with key and print to stream
          for (auto& [key, json] : value)
            json.save(stream << indent << '"' << key << "\" : ", indent)
              // if jsons remaining (not last), append comma
              << (--remaining ? ",\n" : "\n");
          stream << prefix << "}";
        }
        }, *static_cast<aggregate_t*>(this));
      return stream;
    }

    auto load(std::istream& stream) -> std::istream& {
      using namespace std;

      switch ((stream >> ws).peek()) {
      case '"': {
        // get word surrounded by "
        stringbuf buffer;
        stream.ignore(1)
          .get(buffer, '"')
          .ignore(1);
        *this = buffer.str();
      } break;
      case '[': {
        array_t array;
        for (stream.ignore(1); (stream >> ws).peek() != ']';)
          // load child json and consume comma if available
          if ((array.emplace_back().load(stream) >> ws).peek() == ',')
            stream.ignore(1);
        stream.ignore(1);
        *this = move(array);
      } break;
      case '{': {
        object_t object;
        for (stream.ignore(1); (stream >> ws).peek() != '}';) {
          // get word surrounded by "
          stringbuf buffer;
          stream.ignore(numeric_limits<streamsize>::max(), '"')
            .get(buffer, '"')
            .ignore(numeric_limits<streamsize>::max(), ':');
          // load child json and consume comma if available
          if ((object[buffer.str()].load(stream) >> ws).peek() == ',')
            stream.ignore(1);
        }
        stream.ignore(1);
        *this = move(object);
      } break;
      default: {
        if (isdigit(stream.peek()) || stream.peek() == '.') {
          double_t number;
          stream >> number;
          *this = number;
        }
        else if (isalpha(stream.peek())) {
          // get alphabetic word
          string word;
          for (; isalpha(stream.peek()); stream.ignore())
            word.push_back(stream.peek());
          // set value to look-up table's value
          static auto keyword_lut = map<string_view, Value>{
            {"true", true}, {"false", false}, {"null", nullptr}};
          *this = keyword_lut[word];
        }
        else
          *this = nullptr;
      } break;
      }

      return stream;
    }

    auto save_to_path(std::string_view file_path) -> void {
      auto file = std::ofstream(std::string(file_path));
      save(file);
    }

    auto load_from_path(std::string_view file_path) -> void {
      auto file = std::ifstream(std::string(file_path));
      load(file);
    }

    static void test() {
      std::stringstream ss;
      {
        json::Value value;

        auto& employee = value["employee"];
        employee["name"] = "bob";
        employee["age"] = 21;
        employee["friends"][0] = "alice";
        employee["friends"][1] = "billy";
        employee["weight"] = 140.0;

        value.save(ss);
      }

      std::cout << ss.str() << "\n\n";

      {
        auto example = std::stringstream(R"(
    {
      "firstName": "John",
      "lastName": "Smith",
      "isAlive": true,
      "age": 27,
      "address": {
        "streetAddress": "21 2nd Street",
        "city": "New York",
        "state": "NY",
        "postalCode": "10021-3100"
      },
      "phoneNumbers": [
        {
          "type": "home",
          "number": "212 555-1234"
        },
        {
          "type": "office",
          "number": "646 555-4567"
        },
        {
          "type": "mobile",
          "number": "123 456-7890"
        }
      ],
      "children": [],
      "spouse": null
    })");
        json::Value value;
        value.load(example);

        ss.clear();
        value.save(ss);
      }

      std::cout << ss.str() << "\n\n";
    }
  };
}

int main() {
  json::Value::test();
  return getchar();
}