diff --git a/include/skyr/v2/core/parse_path.hpp b/include/skyr/v2/core/parse_path.hpp
new file mode 100644
index 00000000..4278a783
--- /dev/null
+++ b/include/skyr/v2/core/parse_path.hpp
@@ -0,0 +1,37 @@
+// Copyright 2020 Glyn Matthews.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// http://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef SKYR_V2_CORE_PARSE_QUERY_HPP
+#define SKYR_V2_CORE_PARSE_QUERY_HPP
+
+#include <string>
+#include <vector>
+#include <skyr/v2/core/parse.hpp>
+
+namespace skyr::inline v2 {
+///
+/// \param path
+/// \param validation_error
+/// \return
+inline auto parse_path(
+    std::string_view path, bool *validation_error) -> tl::expected<std::vector<std::string>, url_parse_errc> {
+  auto url = details::basic_parse(path, validation_error, nullptr, nullptr, url_parse_state::path_start);
+  if (url) {
+    return url.value().path;
+  }
+  return tl::make_unexpected(url.error());
+}
+
+///
+/// \param path
+/// \return
+inline auto parse_path(
+    std::string_view path) -> tl::expected<std::vector<std::string>, url_parse_errc> {
+  [[maybe_unused]] bool validation_error = false;
+  return parse_path(path, &validation_error);
+}
+}  // namespace skyr::inline v2
+
+#endif  // SKYR_V2_CORE_PARSE_QUERY_HPP
diff --git a/include/skyr/v2/core/parse_query.hpp b/include/skyr/v2/core/parse_query.hpp
new file mode 100644
index 00000000..18f7f019
--- /dev/null
+++ b/include/skyr/v2/core/parse_query.hpp
@@ -0,0 +1,85 @@
+// Copyright 2020 Glyn Matthews.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt or copy at
+// http://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef SKYR_V2_CORE_PARSE_QUERY_HPP
+#define SKYR_V2_CORE_PARSE_QUERY_HPP
+
+#include <string>
+#include <vector>
+#include <range/v3/view/split_when.hpp>
+#include <range/v3/view/transform.hpp>
+#include <skyr/v2/core/parse.hpp>
+#include <skyr/v2/percent_encoding/percent_decode.hpp>
+
+namespace skyr::inline v2 {
+///
+struct query_parameter {
+  std::string name;
+  std::optional<std::string> value;
+
+  /// Constructor
+  query_parameter() = default;
+
+  /// Constructor
+  /// \param name The parameter name
+  query_parameter(std::string name) : name(std::move(name)) {}
+
+  /// Constructor
+  /// \param name The parameter name
+  /// \param value The parameter value
+  query_parameter(std::string name, std::string value) : name(std::move(name)), value(std::move(value)) {}
+};
+
+///
+/// \param query
+/// \param validation_error
+/// \return
+inline auto parse_query(
+    std::string_view query, bool *validation_error) -> tl::expected<std::vector<query_parameter>, url_parse_errc> {
+  if (!query.empty() && (query.front() == '?')) {
+    query.remove_prefix(1);
+  }
+
+  auto url = details::basic_parse(query, validation_error, nullptr, nullptr, url_parse_state::query);
+  if (url) {
+    static constexpr auto is_separator = [](auto c) { return c == '&' || c == ';'; };
+
+    static constexpr auto to_nvp = [](auto &&param) -> query_parameter {
+      if (ranges::empty(param)) {
+        return {};
+      }
+
+      auto element = std::string_view(std::addressof(*std::begin(param)), ranges::distance(param));
+      auto delim = element.find_first_of('=');
+      if (delim != std::string_view::npos) {
+        return {std::string(element.substr(0, delim)), std::string(element.substr(delim + 1))};
+      } else {
+        return {std::string(element)};
+      }
+    };
+
+    std::vector<query_parameter> parameters{};
+    if (url.value().query) {
+      for (auto &&parameter :
+           url.value().query.value() | ranges::views::split_when(is_separator) | ranges::views::transform(to_nvp)) {
+        parameters.emplace_back(parameter);
+      }
+    }
+    return parameters;
+  }
+  return tl::make_unexpected(url.error());
+}
+
+///
+/// \param query
+/// \return
+inline auto parse_query(
+    std::string_view query) -> tl::expected<std::vector<query_parameter>, url_parse_errc> {
+  [[maybe_unused]] bool validation_error = false;
+  return parse_query(query, &validation_error);
+}
+}  // namespace skyr::inline v2
+
+#endif  // SKYR_V2_CORE_PARSE_QUERY_HPP
diff --git a/include/skyr/v2/core/serialize.hpp b/include/skyr/v2/core/serialize.hpp
index f9f5d432..9a686890 100644
--- a/include/skyr/v2/core/serialize.hpp
+++ b/include/skyr/v2/core/serialize.hpp
@@ -28,15 +28,18 @@ inline auto serialize_file_scheme(const url_record &url) -> std::string {
   return (!url.host && (url.scheme == "file")) ? "//" : "";
 }
 
-inline auto serialize_host(const url_record &url) -> std::string {
+inline auto serialize_authority(const url_record &url) -> std::string {
   return url.host
              ? fmt::format("//{}{}{}", serialize_credentials(url), url.host.value().serialize(), serialize_port(url))
              : serialize_file_scheme(url);
 }
 
+inline auto serialize_path(const std::vector<std::string> &path) -> std::string {
+  return fmt::format("/{}", path | ranges::views::join('/') | ranges::to<std::string>());
+}
+
 inline auto serialize_path(const url_record &url) -> std::string {
-  return url.cannot_be_a_base_url ? url.path.front()
-                                  : fmt::format("/{}", url.path | ranges::views::join('/') | ranges::to<std::string>());
+  return url.cannot_be_a_base_url ? url.path.front() : serialize_path(url.path);
 }
 
 inline auto serialize_query(const url_record &url) -> std::string {
@@ -54,7 +57,7 @@ inline auto serialize_fragment(const url_record &url) -> std::string {
 /// \param url A URL record
 /// \returns A serialized URL string, excluding the fragment
 inline auto serialize_excluding_fragment(const url_record &url) -> url_record::string_type {
-  return fmt::format("{}:{}{}{}", url.scheme, details::serialize_host(url), details::serialize_path(url),
+  return fmt::format("{}:{}{}{}", url.scheme, details::serialize_authority(url), details::serialize_path(url),
                      details::serialize_query(url));
 }
 
diff --git a/include/skyr/v2/core/url_parser_context.hpp b/include/skyr/v2/core/url_parser_context.hpp
index 37f17296..e9746392 100644
--- a/include/skyr/v2/core/url_parser_context.hpp
+++ b/include/skyr/v2/core/url_parser_context.hpp
@@ -236,19 +236,10 @@ class url_parser_context {
       buffer.push_back(lower);
     } else if (byte == ':') {
       if (state_override) {
-        if (url.is_special() && !is_special(buffer)) {
-          return tl::make_unexpected(url_parse_errc::cannot_override_scheme);
-        }
-
-        if (!url.is_special() && is_special(buffer)) {
-          return tl::make_unexpected(url_parse_errc::cannot_override_scheme);
-        }
-
-        if ((url.includes_credentials() || url.port) && (buffer == "file")) {
-          return tl::make_unexpected(url_parse_errc::cannot_override_scheme);
-        }
-
-        if ((url.scheme == "file") && (!url.host || url.host.value().is_empty())) {
+        if ((url.is_special() && !is_special(buffer)) ||
+            (!url.is_special() && is_special(buffer)) ||
+            ((url.includes_credentials() || url.port) && (buffer == "file")) ||
+            ((url.scheme == "file") && (!url.host || url.host.value().is_empty()))) {
           return tl::make_unexpected(url_parse_errc::cannot_override_scheme);
         }
       }
@@ -895,11 +886,17 @@ class url_parser_context {
   }
 
   void pct_encode_and_append_to_query(char byte) {
+    if (!url.query) {
+      set_empty_query();
+    }
     auto pct_encoded = percent_encode_byte(std::byte(byte), percent_encoding::encode_set::none);
     url.query.value() += std::move(pct_encoded).to_string();
   }
 
   void append_to_query(char byte) {
+    if (!url.query) {
+      set_empty_query();
+    }
     url.query.value().push_back(byte);
   }
 
@@ -908,6 +905,9 @@ class url_parser_context {
   }
 
   void append_to_fragment(char byte) {
+    if (!url.fragment) {
+      set_empty_fragment();
+    }
     auto pct_encoded = percent_encode_byte(std::byte(byte), percent_encoding::encode_set::fragment);
     url.fragment.value() += pct_encoded.to_string();
   }
diff --git a/include/skyr/v2/json/json.hpp b/include/skyr/v2/json/json.hpp
index 25cb2c4b..ac001de9 100644
--- a/include/skyr/v2/json/json.hpp
+++ b/include/skyr/v2/json/json.hpp
@@ -10,9 +10,9 @@
 #include <optional>
 #include <vector>
 #include <tl/expected.hpp>
-#include <range/v3/view/split_when.hpp>
-#include <range/v3/view/transform.hpp>
 #include <nlohmann/json.hpp>
+#include <fmt/format.h>
+#include <skyr/v2/core/parse_query.hpp>
 #include <skyr/v2/percent_encoding/percent_encode.hpp>
 #include <skyr/v2/percent_encoding/percent_decode.hpp>
 
@@ -28,78 +28,52 @@ inline auto encode_query(const nlohmann::json &json, char separator='&', char eq
   -> tl::expected<std::string, json_errc> {
   using namespace std::string_literals;
 
-  auto result = ""s;
+  std::string result{};
 
   if (!json.is_object()) {
     return tl::make_unexpected(json_errc::invalid_query);
   }
 
   for (auto &[key, value] : json.items()) {
-
     if (value.is_string()) {
-      result += percent_encode(key);
-      result += equal;
-      result += percent_encode(value.get<std::string>());
-      result += separator;
-    }
-    else if (value.is_array()) {
+      result += fmt::format(
+          "{}{}{}{}", percent_encode(key), equal, percent_encode(value.get<std::string>()), separator);
+    } else if (value.is_array()) {
       for (auto &element : value.items()) {
-        result += percent_encode(key);
-        result += equal;
-        result += percent_encode(element.value().get<std::string>());
-        result += separator;
+        result += fmt::format(
+            "{}{}{}{}", percent_encode(key), equal, percent_encode(element.value().get<std::string>()), separator);
       }
-    }
-    else {
-      result += percent_encode(key);
-      result += equal;
-      result += separator;
+    } else {
+      result += fmt::format("{}{}{}", percent_encode(key), equal, separator);
     }
   }
 
   return result.substr(0, result.size() - 1);
 }
 
-inline auto decode_query(std::string_view query, char separator='&', char equal='=') -> nlohmann::json {
-  if (query[0] == '?') {
-    query.remove_prefix(1);
-  }
-
-  static constexpr auto is_separator = [] (auto &&c) {
-    return c == '&' || c == ';';
-  };
-
-  static constexpr auto to_nvp = [] (auto &&param) -> std::pair<std::string_view, std::optional<std::string_view>> {
-    auto element = std::string_view(std::addressof(*std::begin(param)), ranges::distance(param));
-    auto delim = element.find_first_of("=");
-    if (delim != std::string_view::npos) {
-      return { element.substr(0, delim), element.substr(delim + 1) };
-    }
-    else {
-      return { element, std::nullopt };
-    }
-  };
-
+inline auto decode_query(std::string_view query) -> nlohmann::json {
   nlohmann::json object;
-  for (auto [name, value] : query | ranges::views::split_when(is_separator) | ranges::views::transform(to_nvp)) {
-    const auto name_ = skyr::percent_decode(name).value();
-    const auto value_ = value? skyr::percent_decode(value.value()).value() : std::string();
 
-    if (object.contains(name_)) {
-      auto current_value = object[name_];
-      if (current_value.is_string()) {
-        auto prev_value = current_value.get<std::string>();
-        object[name_] = std::vector<std::string>{prev_value, value_};
-      }
-      else if (current_value.is_array()) {
-        auto values = current_value.get<std::vector<std::string>>();
-        values.emplace_back(value_);
-        object[name_] = values;
+  auto parameters = parse_query(query);
+  if (parameters) {
+    for (auto &&[name, value] : parameters.value()) {
+      const auto name_ = skyr::percent_decode(name).value();
+      const auto value_ = value ? skyr::percent_decode(value.value()).value() : std::string();
+
+      if (object.contains(name_)) {
+        auto current_value = object[name_];
+        if (current_value.is_string()) {
+          auto prev_value = current_value.get<std::string>();
+          object[name_] = std::vector<std::string>{prev_value, value_};
+        } else if (current_value.is_array()) {
+          auto values = current_value.get<std::vector<std::string>>();
+          values.emplace_back(value_);
+          object[name_] = values;
+        }
+      } else {
+        object[name_] = value_;
       }
     }
-    else {
-      object[name_] = value_;
-    }
   }
   return object;
 }
diff --git a/include/skyr/v2/platform/endianness.hpp b/include/skyr/v2/platform/endianness.hpp
index 67c816d5..2f72a550 100644
--- a/include/skyr/v2/platform/endianness.hpp
+++ b/include/skyr/v2/platform/endianness.hpp
@@ -25,12 +25,12 @@ requires std::is_integral_v<intT> constexpr inline auto swap_endianness(intT v)
 }  // namespace details
 
 template <class intT>
-requires std::is_integral_v<intT> constexpr inline auto to_network_byte_order(intT v) noexcept {
+requires std::is_integral_v<intT> constexpr inline auto to_network_byte_order(intT v) noexcept -> intT {
   return (std::endian::big == std::endian::native) ? v : details::swap_endianness(v);  // NOLINT
 }
 
 template <class intT>
-requires std::is_integral_v<intT> constexpr inline auto from_network_byte_order(intT v) noexcept {
+requires std::is_integral_v<intT> constexpr inline auto from_network_byte_order(intT v) noexcept -> intT {
   return (std::endian::big == std::endian::native) ? v : details::swap_endianness(v);  // NOLINT
 }
 }  // namespace skyr::inline v2
diff --git a/include/skyr/v2/url_search_parameters.hpp b/include/skyr/v2/url_search_parameters.hpp
index b9b10ed7..562851ed 100644
--- a/include/skyr/v2/url_search_parameters.hpp
+++ b/include/skyr/v2/url_search_parameters.hpp
@@ -13,12 +13,8 @@
 #include <optional>
 #include <algorithm>
 #include <cassert>
-#include <range/v3/algorithm/sort.hpp>
-#include <range/v3/view/split_when.hpp>
-#include <range/v3/view/transform.hpp>
-#include <range/v3/algorithm/find_if.hpp>
-#include <range/v3/algorithm/remove_if.hpp>
-#include <range/v3/action/erase.hpp>
+#include <fmt/format.h>
+#include <skyr/v2/core/parse_query.hpp>
 #include <skyr/v2/percent_encoding/percent_encode.hpp>
 #include <skyr/v2/percent_encoding/percent_decode.hpp>
 
@@ -30,8 +26,8 @@ struct is_name {
   explicit is_name(std::string_view name) : name_(name) {
   }
 
-  auto operator()(const std::pair<std::string, std::string> &parameter) noexcept {
-    return name_ == parameter.first;
+  auto operator()(const query_parameter &parameter) noexcept {
+    return name_ == parameter.name;
   }
 
   std::string_view name_;
@@ -52,7 +48,7 @@ class url_search_parameters {
   using string_type = std::string;
 
   /// A key-value pair
-  using value_type = std::pair<string_type, string_type>;
+  using value_type = query_parameter;
 
   /// An iterator through the search parameters
   using const_iterator = std::vector<value_type>::const_iterator;
@@ -72,7 +68,11 @@ class url_search_parameters {
     initialize(query);
   }
 
-  ///
+  /// Constructor
+  /// \param parameters
+  explicit url_search_parameters(std::vector<query_parameter> parameters) : parameters_(std::move(parameters)) {}
+
+  /// Constructor
   /// \param parameters
   url_search_parameters(std::initializer_list<value_type> parameters) : parameters_(parameters) {}
 
@@ -87,7 +87,7 @@ class url_search_parameters {
   /// \param name The parameter name
   /// \param value The parameter value
   void append(std::string_view name, std::string_view value) {
-    parameters_.emplace_back(name, value);
+    parameters_.emplace_back(std::string(name), std::string(value));
     update();
   }
 
@@ -95,16 +95,18 @@ class url_search_parameters {
   ///
   /// \param name The name of the parameter to remove
   void remove(std::string_view name) {
-    auto it = ranges::remove_if(parameters_, details::is_name(name));
-    ranges::erase(parameters_, it, ranges::end(parameters_));
+    auto first = std::begin(parameters_), last = std::end(parameters_);
+    auto it = std::remove_if(first, last, details::is_name(name));
+    parameters_.erase(it, last);
     update();
   }
 
   /// \param name The search parameter name
   /// \returns The first search parameter value with the given name
   [[nodiscard]] auto get(std::string_view name) const -> std::optional<string_type> {
-    auto it = ranges::find_if(parameters_, details::is_name(name));
-    return (it != ranges::cend(parameters_)) ? std::make_optional(it->second) : std::nullopt;
+    auto first = std::cbegin(parameters_), last = std::cend(parameters_);
+    auto it = std::find_if(first, last, details::is_name(name));
+    return (it != last) ? it->value : std::nullopt;
   }
 
   /// \param name The search parameter name
@@ -114,7 +116,7 @@ class url_search_parameters {
     result.reserve(parameters_.size());
     for (auto [parameter_name, value] : parameters_) {
       if (parameter_name == name) {
-        result.emplace_back(value);
+        result.emplace_back(value.value_or(""));
       }
     }
     return result;
@@ -126,7 +128,8 @@ class url_search_parameters {
   /// \returns `true` if the value is in the search parameters,
   /// `false` otherwise.
   [[nodiscard]] auto contains(std::string_view name) const noexcept -> bool {
-    return ranges::find_if(parameters_, details::is_name(name)) != ranges::cend(parameters_);
+    auto first = std::cbegin(parameters_), last = std::cend(parameters_);
+    return std::find_if(first, last, details::is_name(name)) != last;
   }
 
   /// Sets a URL search parameter
@@ -134,15 +137,16 @@ class url_search_parameters {
   /// \param name The search parameter name
   /// \param value The search parameter value
   void set(std::string_view name, std::string_view value) {
-    auto it = ranges::find_if(parameters_, details::is_name(name));
-    if (it != ranges::end(parameters_)) {
-      it->second = value;
+    auto first = std::begin(parameters_), last = std::end(parameters_);
+    auto it = std::find_if(first, last, details::is_name(name));
+    if (it != last) {
+      it->value = value;
 
       ++it;
-      it = std::remove_if(it, ranges::end(parameters_), details::is_name(name));
-      ranges::erase(parameters_, it, ranges::end(parameters_));
+      it = std::remove_if(it, last, details::is_name(name));
+      ranges::erase(parameters_, it, last);
     } else {
-      parameters_.emplace_back(name, value);
+      append(name, value);
     }
     update();
   }
@@ -166,9 +170,10 @@ class url_search_parameters {
   /// assert(url.search() == "?key=e1f7bc78&q=%F0%9F%8F%B3%EF%B8%8F%E2%80%8D%F0%9F%8C%88");
   /// ```
   void sort() {
-    static constexpr auto less_name = [](const auto &lhs, const auto &rhs) { return lhs.first < rhs.first; };
+    static constexpr auto less_name = [](const auto &lhs, const auto &rhs) { return lhs.name < rhs.name; };
 
-    ranges::sort(parameters_, less_name);
+    auto first = std::begin(parameters_), last = std::end(parameters_);
+    std::sort(first, last, less_name);
     update();
   }
 
@@ -211,13 +216,14 @@ class url_search_parameters {
     bool start = true;
     for (const auto &[name, value] : parameters_) {
       if (start) {
+        result.append(percent_encode(name));
         start = false;
       } else {
-        result.append("&");
+        result.append(fmt::format("&{}", percent_encode(name)));
+      }
+      if (value) {
+        result.append(fmt::format("={}", percent_encode(value.value())));
       }
-      result.append(percent_encode(name));
-      result.append("=");
-      result.append(percent_encode(value));
     }
 
     return result;
@@ -227,26 +233,12 @@ class url_search_parameters {
   explicit url_search_parameters(url *url);
 
   void initialize(std::string_view query) {
-    if (!query.empty() && (query.front() == '?')) {
-      query.remove_prefix(1);
-    }
-
-    static constexpr auto is_separator = [](auto &&c) { return c == '&' || c == ';'; };
-
-    static constexpr auto to_nvp = [](auto &&param) -> std::pair<std::string_view, std::optional<std::string_view>> {
-      auto element = std::string_view(std::addressof(*ranges::begin(param)), ranges::distance(param));
-      auto delim = element.find_first_of("=");
-      if (delim != std::string_view::npos) {
-        return {element.substr(0, delim), element.substr(delim + 1)};
-      } else {
-        return {element, std::nullopt};
+    if (auto parameters = parse_query(query); parameters) {
+      for (auto [name, value] : parameters.value()) {
+        auto name_ = percent_decode(name).value_or(std::string(name));
+        auto value_ = value ? percent_decode(value.value()).value_or(std::string(value.value())) : std::string();
+        parameters_.emplace_back(name_, value_);
       }
-    };
-
-    for (auto [name, value] : query | ranges::views::split_when(is_separator) | ranges::views::transform(to_nvp)) {
-      auto name_ = percent_decode(name).value_or(std::string(name));
-      auto value_ = value ? percent_decode(value.value()).value_or(std::string(value.value())) : std::string();
-      parameters_.emplace_back(name_, value_);
     }
   }
 
diff --git a/tests/v2/core/CMakeLists.txt b/tests/v2/core/CMakeLists.txt
index aca3b468..0cf0106e 100644
--- a/tests/v2/core/CMakeLists.txt
+++ b/tests/v2/core/CMakeLists.txt
@@ -6,6 +6,8 @@
 foreach (file_name
         parse_host_tests.cpp
         url_parse_tests.cpp
+        parse_path_tests.cpp
+        parse_query_tests.cpp
         url_serialize_tests.cpp
         )
     skyr_create_test(${file_name} ${PROJECT_BINARY_DIR}/tests/core test_name v2)
diff --git a/tests/v2/core/parse_path_tests.cpp b/tests/v2/core/parse_path_tests.cpp
new file mode 100644
index 00000000..d06df103
--- /dev/null
+++ b/tests/v2/core/parse_path_tests.cpp
@@ -0,0 +1,33 @@
+// Copyright 2020 Glyn Matthews.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt of copy at
+// http://www.boost.org/LICENSE_1_0.txt)
+
+#define CATCH_CONFIG_MAIN
+#include <catch2/catch.hpp>
+#include <skyr/v2/core/parse_path.hpp>
+
+TEST_CASE("path_parsing_example_tests", "[parse]") {
+  SECTION("url_path_1") {
+    auto path = skyr::parse_path("///");
+    REQUIRE(path);
+    CHECK(3 == path.value().size());
+  }
+
+  SECTION("url_path_2") {
+    auto path = skyr::parse_path("/a/b/./c");
+    REQUIRE(path);
+    CHECK(3 == path.value().size());
+    CHECK("a" == path.value()[0]);
+    CHECK("b" == path.value()[1]);
+    CHECK("c" == path.value()[2]);
+  }
+
+  SECTION("url_path_3") {
+    auto path = skyr::parse_path("/a/b/../c");
+    REQUIRE(path);
+    CHECK(2 == path.value().size());
+    CHECK("a" == path.value()[0]);
+    CHECK("c" == path.value()[1]);
+  }
+}
diff --git a/tests/v2/core/parse_query_tests.cpp b/tests/v2/core/parse_query_tests.cpp
new file mode 100644
index 00000000..754340fb
--- /dev/null
+++ b/tests/v2/core/parse_query_tests.cpp
@@ -0,0 +1,40 @@
+// Copyright 2020 Glyn Matthews.
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE_1_0.txt of copy at
+// http://www.boost.org/LICENSE_1_0.txt)
+
+#define CATCH_CONFIG_MAIN
+#include <catch2/catch.hpp>
+#include <skyr/v2/core/parse_query.hpp>
+
+TEST_CASE("query_parsing_example_tests", "[parse]") {
+  SECTION("url_query_1") {
+    auto query = skyr::parse_query("?a=b");
+    REQUIRE(query);
+    CHECK(1 == query.value().size());
+    CHECK(query.value().front().name == "a");
+    CHECK(query.value().front().value);
+    CHECK(query.value().front().value.value() == "b");
+  }
+
+  SECTION("url_query_2") {
+    auto query = skyr::parse_query("c=d");
+    REQUIRE(query);
+    CHECK(1 == query.value().size());
+    CHECK(query.value().front().name == "c");
+    CHECK(query.value().front().value);
+    CHECK(query.value().front().value.value() == "d");
+  }
+
+  SECTION("url_query_3") {
+    auto query = skyr::parse_query("?q=\xf0\x9f\x8f\xb3\xef\xb8\x8f\xe2\x80\x8d\xf0\x9f\x8c\x88&key=e1f7bc78");
+    REQUIRE(query);
+    CHECK(2 == query.value().size());
+    CHECK(query.value()[0].name == "q");
+    CHECK(query.value()[0].value);
+    CHECK(query.value()[0].value.value() == "%F0%9F%8F%B3%EF%B8%8F%E2%80%8D%F0%9F%8C%88");
+    CHECK(query.value()[1].name == "key");
+    CHECK(query.value()[1].value);
+    CHECK(query.value()[1].value.value() == "e1f7bc78");
+  }
+}
diff --git a/tests/v2/json/json_query_tests.cpp b/tests/v2/json/json_query_tests.cpp
index 277fcfcd..61cf6402 100644
--- a/tests/v2/json/json_query_tests.cpp
+++ b/tests/v2/json/json_query_tests.cpp
@@ -9,7 +9,7 @@
 #include <vector>
 #include <skyr/v2/json/json.hpp>
 
-TEST_CASE("ipv6_address_tests", "[json.query]") {
+TEST_CASE("json_tests", "[json.query]") {
   using namespace std::string_literals;
 
   SECTION("decode_simple_query") {
@@ -32,11 +32,11 @@ TEST_CASE("ipv6_address_tests", "[json.query]") {
     CHECK(json["a"] == "\xcf\x80");
   }
 
-//  SECTION("decode_empty_query") {
-//    auto query = ""s;
-//    auto json = skyr::json::decode_query(query);
-//    CHECK(json.empty());
-//  }
+  SECTION("decode_empty_query") {
+    auto query = ""s;
+    auto json = skyr::json::decode_query(query);
+    CHECK(json.empty());
+  }
 
   SECTION("encode_simple_query") {
     auto json = nlohmann::json{ { "a", "b" }, { "c", "d" } };
