Writing a simple JSON library from scratch: a tour through modern C++
That  struct JSONValue {
    std::optional string;
    std::optional number;
    std::optional boolean;
    std::optional> array;
    std::optional> object;
    JSONValueType type;
  };

really ought to be a variant, that would simplify things a lot. It'd just be  struct JSONValue;
  using variant_type = 
    std::variant<
     std::string
   , double
   , bool
   , std::vector
   , std::map
  >;
  struct JSONValue : variant_type { 
    using variant::variant;
  };

then instead of switches you'd do:  std::string_view print_type(const variant_type& jtt) {
    using namespace std::literals;
    struct {
      auto operator()(const std::string&) const noexcept { return "String"sv; }
      auto operator()(double) const noexcept { return "Number"sv; }
      auto operator()(bool) const noexcept { return "Bool"sv; }
      auto operator()(const std::vector&) const noexcept { return "Array"sv; }
      auto operator()(const std::map&) const noexcept { return "Dict"sv; }
      auto operator()(std::monostate) const noexcept { return "Null"sv; }
    } vis;
    return std::visit(vis, jtt);
  }

which is much safer than switch/cases