diff --git a/src/json.hpp b/src/json.hpp
index 8b281d93..8dc9b383 100644
--- a/src/json.hpp
+++ b/src/json.hpp
@@ -106,17 +106,6 @@ SOFTWARE.
 */
 namespace nlohmann
 {
-// TODO add real documentation before PR
-
-// Traits structure declaration, users can specialize it for their own types
-// 
-// constructing a json object from a user-defined type will call the
-// 'json to_json(T)' function
-//
-// whereas calling json::get<T> will call 'T from_json(json const&)'
-template <typename T, typename = void>
-struct json_traits;
-
 // alias templates to reduce boilerplate
 template <bool B, typename T = void>
 using enable_if_t = typename std::enable_if<B, T>::type;
@@ -166,64 +155,24 @@ void from_json();
 
 struct to_json_fn
 {
-  private:
-    // fallback overload
     template <typename T>
-    static constexpr auto
-    impl(T &&val, long) noexcept(noexcept(to_json(std::forward<T>(val))))
+    constexpr auto
+    operator()(T &&val) const noexcept(noexcept(to_json(std::forward<T>(val))))
         -> decltype(to_json(std::forward<T>(val)))
     {
       return to_json(std::forward<T>(val));
     }
 
-    // preferred overload
-    template <typename T>
-    static constexpr auto impl(T &&val, int) noexcept(
-        noexcept(json_traits<uncvref_t<T>>::to_json(std::forward<T>(val))))
-        -> decltype(json_traits<uncvref_t<T>>::to_json(std::forward<T>(val)))
-    {
-      return json_traits<uncvref_t<T>>::to_json(std::forward<T>(val));
-    }
-
-  public:
-    template <typename T>
-    constexpr auto operator()(T &&val) const
-        noexcept(noexcept(to_json_fn::impl(std::forward<T>(val), 0)))
-            -> decltype(to_json_fn::impl(std::forward<T>(val), 0))
-    {
-      // decltype(0) -> int, so the compiler will try to take the 'preferred overload'
-      // if there is no specialization, the 'fallback overload' will be taken by converting 0 to long
-      return to_json_fn::impl(std::forward<T>(val), 0);
-    }
 };
 
 struct from_json_fn
 {
-  private:
     template <typename T, typename Json>
-    static constexpr auto impl(Json const &j, T &val,
-                               long) noexcept(noexcept(from_json(j, val)))
-        -> decltype(from_json(j, val))
+    constexpr auto operator()(Json &&j, T &val) const
+                               noexcept(noexcept(from_json(std::forward<Json>(j), val)))
+        -> decltype(from_json(std::forward<Json>(j), val))
     {
-      return from_json(j, val);
-    }
-
-    template <typename T, typename Json>
-    static constexpr auto
-    impl(Json const &j, T &val,
-         int) noexcept(noexcept(json_traits<T>::from_json(j, val)))
-        -> decltype(json_traits<T>::from_json(j, val))
-    {
-      return json_traits<T>::from_json(j, val);
-    }
-
-  public:
-    template <typename T, typename Json>
-    constexpr auto operator()(Json const &j, T &val) const
-        noexcept(noexcept(from_json_fn::impl(j, val, 0)))
-            -> decltype(from_json_fn::impl(j, val, 0))
-    {
-      return from_json_fn::impl(j, val, 0);
+      return from_json(std::forward<Json>(j), val);
     }
 };
 
@@ -265,6 +214,32 @@ inline namespace
   constexpr auto const& from_json = _static_const<detail::from_json_fn>::value;
 }
 
+// default JSONSerializer template argument
+// will use ADL for serialization
+struct adl_serializer
+{
+  template <typename T, typename Json, typename = enable_if_t<std::is_default_constructible<uncvref_t<T>>::value>>
+  static auto from_json(Json&& j) -> uncvref_t<decltype(::nlohmann::from_json(std::forward<Json>(j), std::declval<T&>()), std::declval<T>())>
+  {
+    uncvref_t<T> ret;
+    ::nlohmann::from_json(std::forward<Json>(j), ret);
+    return ret;
+  }
+
+  template <typename T, typename Json>
+  static auto from_json(Json&& j, T& val) -> decltype(::nlohmann::from_json(std::forward<Json>(j), val))
+  {
+    ::nlohmann::from_json(std::forward<Json>(j), val);
+  }
+
+  template <typename T>
+  static auto to_json(T&& val) -> decltype(::nlohmann::to_json(std::forward<T>(val)))
+  {
+    return ::nlohmann::to_json(std::forward<T>(val));
+  }
+};
+
+
 /*!
 @brief a class to store JSON values
 
@@ -352,7 +327,8 @@ template <
     class NumberIntegerType = std::int64_t,
     class NumberUnsignedType = std::uint64_t,
     class NumberFloatType = double,
-    template<typename U> class AllocatorType = std::allocator
+    template<typename U> class AllocatorType = std::allocator,
+    class JSONSerializer = adl_serializer
     >
 class basic_json
 {
@@ -1421,30 +1397,10 @@ class basic_json
         assert_invariant();
     }
 
-    // constructor chosen for user-defined types that either have:
-    // - a to_json free function in their type's namespace
-    // - a json_traits specialization for their type
-    //
-    // If there is both a free function and a specialization, the latter will be chosen,
-    // since it is a more advanced use
-    //
-    // note: constructor is marked explicit to avoid the following issue:
-    //
-    // struct not_equality_comparable{};
-    // 
-    // not_equality_comparable{} == not_equality_comparable{};
-    // 
-    // this will construct implicitely 2 json objects and call operator== on them
-    // which can cause nasty bugs on the user's in json-unrelated code
-    // 
-    // the trade-off is expressiveness in initializer-lists
-    // auto j = json{{"a", json(not_equality_comparable{})}};
-    // 
-    // we can remove this constraint though, since lots of ctor are not explicit already
-    template <typename T, typename = decltype(::nlohmann::to_json(
-                              std::declval<uncvref_t<T>>()))>
+    // constructor chosen when JSONSerializer::to_json exists for type T
+    template <typename T, typename = decltype(JSONSerializer::to_json(std::declval<uncvref_t<T>>()))>
     explicit basic_json(T &&val)
-        : basic_json(::nlohmann::to_json(std::forward<T>(val))) {}
+        : basic_json(JSONSerializer::to_json(std::forward<T>(val))) {}
 
     /*!
     @brief create a string (explicit)
@@ -3115,16 +3071,11 @@ class basic_json
         return get_impl(static_cast<ValueType*>(nullptr));
     }
 
-    template <typename ValueType>
-    auto get() const -> remove_reference_t<
-        decltype(::nlohmann::from_json(*this, std::declval<ValueType &>()),
-                 std::declval<ValueType>())>
+    template <typename ValueType, typename = enable_if_t<std::is_default_constructible<uncvref_t<ValueType>>::value, float>>
+    auto get() const -> remove_reference_t<decltype(JSONSerializer::from_json(*this, std::declval<ValueType&>()), std::declval<ValueType>())>
     {
-      static_assert(std::is_default_constructible<ValueType>::value,
-                    "ValueType must be default-constructible when user-defined "
-                    "from_json method is used");
-      ValueType ret;
-      ::nlohmann::from_json(*this, ret);
+      uncvref_t<ValueType> ret;
+      JSONSerializer::from_json(*this, ret);
       return ret;
     }
 
diff --git a/test/src/unit-constructor3.cpp b/test/src/unit-constructor3.cpp
index de52762f..77b31c6a 100644
--- a/test/src/unit-constructor3.cpp
+++ b/test/src/unit-constructor3.cpp
@@ -35,9 +35,6 @@ using nlohmann::json;
 
 namespace udt
 {
-// only used by counter_type
-auto nb_free_function_calls = 0;
-
 struct empty_type {};
 struct pod_type {
   int a;
@@ -51,10 +48,6 @@ struct bit_more_complex_type {
   std::string c;
 };
 
-struct counter_type
-{
-};
-
 // best optional implementation ever
 template <typename T>
 class optional_type
@@ -75,11 +68,6 @@ private:
   std::shared_ptr<T> _val;
 };
 
-struct no_json_traits_type
-{
-  int a;
-};
-
 // free to/from_json functions
 
 json to_json(empty_type)
@@ -107,17 +95,6 @@ json to_json(optional_type<T> const& opt)
   return json(*opt);
 }
 
-json to_json(no_json_traits_type const& p)
-{
-  return {{"a", p.a}};
-}
-
-json to_json(counter_type)
-{
-  ++nb_free_function_calls;
-  return json::object();
-}
-
 void from_json(json const&j, empty_type& t)
 {
   assert(j.empty());
@@ -136,11 +113,6 @@ void from_json(json const&j, bit_more_complex_type& t)
         j["c"].get<std::string>()};
 }
 
-void from_json(json const& j, no_json_traits_type& t)
-{
-  t.a = j["a"].get<int>();
-}
-
 template <typename T>
 void from_json(json const& j, optional_type<T>& t)
 {
@@ -150,11 +122,6 @@ void from_json(json const& j, optional_type<T>& t)
     t = j.get<T>();
 }
 
-void from_json(json const&, counter_type&)
-{
-  ++nb_free_function_calls;
-}
-
 inline bool operator==(pod_type const& lhs, pod_type const& rhs) noexcept
 {
   return std::tie(lhs.a, lhs.b, lhs.c) == std::tie(rhs.a, rhs.b, rhs.c);
@@ -174,106 +141,7 @@ inline bool operator==(optional_type<T> const& lhs, optional_type<T> const& rhs)
     return false;
   return *lhs == *rhs;
 }
-
-inline bool operator==(no_json_traits_type const& lhs, no_json_traits_type const& rhs)
-{
-  return lhs.a == rhs.a;
 }
-}
-
-namespace nlohmann
-{
-template <>
-struct json_traits<udt::empty_type>
-{
-  using type = udt::empty_type;
-
-  static json to_json(type)
-  {
-    return json::object();
-  }
-
-  static type from_json(json const& j)
-  {
-    assert(j.is_object() and j.empty());
-    return {};
-  }
-};
-
-template <>
-struct json_traits<udt::pod_type>
-{  
-  using type = udt::pod_type;
-
-  static json to_json(type const& t)
-  {
-    return {{"a", t.a}, {"b", t.b}, {"c", t.c}};
-  }
-
-  static type from_json(json const& j)
-  {
-    assert(j.is_object());
-    return {j["a"].get<int>(), j["b"].get<char>(), j["c"].get<short>()};
-  }
-};
-
-template <>
-struct json_traits<udt::bit_more_complex_type>
-{
-  using type = udt::bit_more_complex_type;
-
-  static json to_json(type const& t)
-  {
-    return json{{"a", json{t.a}}, {"b", json{t.b}}, {"c", t.c}};
-  }
-
-  static type from_json(json const& j)
-  {
-    return {j["a"].get<udt::pod_type>(), j["b"].get<udt::pod_type>(),
-            j["c"].get<std::string>()};
-  }
-};
-
-template <typename T>
-struct json_traits<udt::optional_type<T>>
-{
-  using type = udt::optional_type<T>;
-
-  static json to_json(type const& t)
-  {
-    if (t)
-      return json(*t);
-    return {};
-  }
-
-  static type from_json(json const& j)
-  {
-    if (j.is_null())
-      return {};
-    return type{j.get<T>()};
-  }
-};
-
-template <>
-struct json_traits<udt::counter_type>
-{
-  using type = udt::counter_type;
-  static int nb_calls;
-
-  static json to_json(type)
-  {
-    ++nb_calls;
-    return json::object();
-  }
-
-  static void from_json(json const&, type&)
-  {
-    ++nb_calls;
-  }
-};
-int json_traits<udt::counter_type>::nb_calls{0};
-}
-
 
 TEST_CASE("constructors for user-defined types", "[udt]")
 {
@@ -415,24 +283,6 @@ TEST_CASE("get<> for user-defined types", "[udt]")
       CHECK(*v == expected);
     }
   }
-
-  SECTION("no json_traits specialization, use of ADL")
-  {
-    udt::no_json_traits_type val{42};
-    auto const expected = json{{"a", 42}};
-    auto const j = json(val);
-    CHECK(j == expected);
-  }
-
-  SECTION("counter_type")
-  {
-    // check that the traits specialization is chosen
-    auto const j = json{udt::counter_type{}};
-    CHECK(nlohmann::json_traits<udt::counter_type>::nb_calls == 1);
-    auto const elem = j.get<udt::counter_type>();
-    CHECK(nlohmann::json_traits<udt::counter_type>::nb_calls == 2);
-    CHECK(udt::nb_free_function_calls == 0);
-  }
 }
 
 TEST_CASE("to_json free function", "[udt]")
@@ -477,16 +327,6 @@ TEST_CASE("to_json free function", "[udt]")
       CHECK(expected == j);
     }
   }
-
-  SECTION("no json_traits specialization")
-  {
-    udt::no_json_traits_type t{42};
-
-    json expected;
-    expected["a"] = 42;
-    auto const j = nlohmann::to_json(t);
-    CHECK(j == expected);
-  }
 }
 
 TEST_CASE("from_json free function", "[udt]")
@@ -535,16 +375,4 @@ TEST_CASE("from_json free function", "[udt]")
       CHECK(expected == o);
     }
   }
-
-  SECTION("no json_traits specialization")
-  {
-    udt::no_json_traits_type expected{42};
-    udt::no_json_traits_type res;
-    auto const j = json{{"a", 42}};
-    nlohmann::from_json(j, res);
-    CHECK(res == expected);
-
-    res = j.get<udt::no_json_traits_type>();
-    CHECK(res == expected);
-  }
 }