diff --git a/src/metabase/driver/mongo/query_processor.clj b/src/metabase/driver/mongo/query_processor.clj
index 4e27d66d57140600f9380fc92e11de81b4643a67..4dac3f89814c8677bfd63ebbcaad925229b68545 100644
--- a/src/metabase/driver/mongo/query_processor.clj
+++ b/src/metabase/driver/mongo/query_processor.clj
@@ -379,31 +379,63 @@
                     v)}))))
 
 
-;;; ------------------------------------------------------------ Handling ISODate(...) forms ------------------------------------------------------------
-;; In Mongo it's fairly common use ISODate(...) forms in queries, which unfortunately are not valid JSON,
+;;; ------------------------------------------------------------ Handling ISODate(...) and ObjectId(...) forms ------------------------------------------------------------
+;; In Mongo it's fairly common use ISODate(...) or ObjectId(...) forms in queries, which unfortunately are not valid JSON,
 ;; and thus cannot be parsed by Cheshire. But we are clever so we will:
 ;;
 ;; 1) Convert forms like ISODate(...) to valid JSON forms like ["___ISODate", ...]
 ;; 2) Parse Normally
-;; 3) Walk the parsed JSON and convert forms like [:___ISODate ...] to JodaTime dates
+;; 3) Walk the parsed JSON and convert forms like [:___ISODate ...] to JodaTime dates, and [:___ObjectId ...] to BSON IDs
+
+;; add more fn handlers here as needed
+(def ^:private fn-name->decoder
+  {:ISODate (fn [arg]
+              (DateTime. arg))
+   :ObjectId (fn [^String arg]
+               (ObjectId. arg))})
+
+(defn- form->encoded-fn-name
+  "If FORM is an encoded fn call form return the key representing the fn call that was encoded.
+   If it doesn't represent an encoded fn, return `nil`.
+
+     (form->encoded-fn-name [:___ObjectId \"583327789137b2700a1621fb\"]) -> :ObjectId"
+  [form]
+  (when (vector? form)
+    (when (u/string-or-keyword? (first form))
+      (when-let [[_ k] (re-matches #"^___(\w+$)" (name (first form)))]
+        (let [k (keyword k)]
+          (when (contains? fn-name->decoder k)
+            k))))))
+
+(defn- maybe-decode-fncall [form]
+  (if-let [fn-name (form->encoded-fn-name form)]
+    ((fn-name->decoder fn-name) (second form))
+    form))
 
-(defn- encoded-iso-date? [form]
-  (and (vector? form)
-       (= (first form) "___ISODate")))
+(defn- decode-fncalls [query]
+  (walk/postwalk maybe-decode-fncall query))
 
-(defn- maybe-decode-iso-date-fncall [form]
-  (if (encoded-iso-date? form)
-    (DateTime. (second form))
-    form))
+(defn- encode-fncalls-for-fn
+  "Walk QUERY-STRING and replace fncalls to fn with FN-NAME with encoded forms that can be parsed as valid JSON.
+
+     (encode-fncalls-for-fn \"ObjectId\" \"{\\\"$match\\\":ObjectId(\\\"583327789137b2700a1621fb\\\")}\")
+     ;; -> \"{\\\"$match\\\":[\\\"___ObjectId\\\", \\\"583327789137b2700a1621fb\\\"]}\""
+  [fn-name query-string]
+  (s/replace query-string
+             (re-pattern (format "%s\\(([^)]+)\\)" (name fn-name)))
+             (format "[\"___%s\", $1]" (name fn-name))))
 
-(defn- decode-iso-date-fncalls [query]
-  (walk/postwalk maybe-decode-iso-date-fncall query))
+(defn- encode-fncalls
+  "Replace occurances of `ISODate(...)` and similary function calls (invalid JSON, but legal in Mongo)
+   with legal JSON forms like `[:___ISODate ...]` that we can decode later.
 
-(defn- encode-iso-date-fncalls
-  "Replace occurances of `ISODate(...)` function calls (invalid JSON, but legal in Mongo)
-   with legal JSON forms like `[:___ISODate ...]` that we can decode later."
+   Walks QUERY-STRING and encodes all the various fncalls we support."
   [query-string]
-  (s/replace query-string #"ISODate\(([^)]+)\)" "[\"___ISODate\", $1]"))
+  (loop [query-string query-string, [fn-name & more] (keys fn-name->decoder)]
+    (if-not fn-name
+      query-string
+      (recur (encode-fncalls-for-fn fn-name query-string)
+             more))))
 
 
 ;;; ------------------------------------------------------------ Query Execution ------------------------------------------------------------
@@ -427,7 +459,7 @@
          (string? collection)
          (map? database)]}
   (let [query   (if (string? query)
-                  (decode-iso-date-fncalls (json/parse-string (encode-iso-date-fncalls query) keyword))
+                  (decode-fncalls (json/parse-string (encode-fncalls query) keyword))
                   query)
         results (mc/aggregate *mongo-connection* collection query
                               :allow-disk-use true)
diff --git a/test/metabase/driver/mongo_test.clj b/test/metabase/driver/mongo_test.clj
index 3332a298277eff2370b30b79f5f3f47781186cdc..1387e674bc6939b26d293749344eb763b1c821f4 100644
--- a/test/metabase/driver/mongo_test.clj
+++ b/test/metabase/driver/mongo_test.clj
@@ -175,21 +175,33 @@
             (ql/filter (ql/= $bird_id "abcdefabcdefabcdefabcdef"))))))
 
 
-;;; ------------------------------------------------------------ Test that we can handle native queries with "ISODate(...)" forms (#3741) ------------------------------------------------------------
+;;; ------------------------------------------------------------ Test that we can handle native queries with "ISODate(...)" and "ObjectId(...) forms (#3741, #4448) ------------------------------------------------------------
 (tu/resolve-private-vars metabase.driver.mongo.query-processor
-  maybe-decode-iso-date-fncall decode-iso-date-fncalls encode-iso-date-fncalls)
+  maybe-decode-fncall decode-fncalls encode-fncalls)
 
 (expect
   "[{\"$match\":{\"date\":{\"$gte\":[\"___ISODate\", \"2012-01-01\"]}}}]"
-  (encode-iso-date-fncalls "[{\"$match\":{\"date\":{\"$gte\":ISODate(\"2012-01-01\")}}}]"))
+  (encode-fncalls "[{\"$match\":{\"date\":{\"$gte\":ISODate(\"2012-01-01\")}}}]"))
+
+(expect
+  "[{\"$match\":{\"entityId\":{\"$eq\":[\"___ObjectId\", \"583327789137b2700a1621fb\"]}}}]"
+  (encode-fncalls "[{\"$match\":{\"entityId\":{\"$eq\":ObjectId(\"583327789137b2700a1621fb\")}}}]"))
 
 (expect
   (DateTime. "2012-01-01")
-  (maybe-decode-iso-date-fncall ["___ISODate" "2012-01-01"]))
+  (maybe-decode-fncall ["___ISODate" "2012-01-01"]))
+
+(expect
+  (ObjectId. "583327789137b2700a1621fb")
+  (maybe-decode-fncall ["___ObjectId" "583327789137b2700a1621fb"]))
 
 (expect
   [{:$match {:date {:$gte (DateTime. "2012-01-01")}}}]
-  (decode-iso-date-fncalls [{:$match {:date {:$gte ["___ISODate" "2012-01-01"]}}}]))
+  (decode-fncalls [{:$match {:date {:$gte ["___ISODate" "2012-01-01"]}}}]))
+
+(expect
+  [{:$match {:entityId {:$eq (ObjectId. "583327789137b2700a1621fb")}}}]
+  (decode-fncalls [{:$match {:entityId {:$eq ["___ObjectId" "583327789137b2700a1621fb"]}}}]))
 
 (datasets/expect-with-engine :mongo
   5
@@ -197,3 +209,11 @@
                                              :collection "checkins"}
                                   :type     :native
                                   :database (data/id)}))))
+
+(datasets/expect-with-engine :mongo
+  0
+  ;; this query shouldn't match anything, so we're just checking that it completes successfully
+  (count (rows (qp/process-query {:native   {:query      "[{\"$match\": {\"_id\": {\"$eq\": ObjectId(\"583327789137b2700a1621fb\")}}}]"
+                                             :collection "venues"}
+                                  :type     :native
+                                  :database (data/id)}))))