Skip to content

feat(firestore): add PyMongo duck-typing serialization support - #18406

Open
ohmayr wants to merge 1 commit into
bson-pr3-orderingfrom
bson-pr4-duck-typing
Open

ohmayr wants to merge 1 commit into
bson-pr3-orderingfrom
bson-pr4-duck-typing

Conversation

@ohmayr

@ohmayr ohmayr commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Adds duck-typing detection in encode_value() for native PyMongo / third-party BSON objects (bson.ObjectId, bson.Decimal128, bson.Regex, bson.Timestamp, bson.MinKey, bson.MaxKey).

Allows applications migrating from MongoDB that use native PyMongo BSON objects to write directly to Firestore without triggering TypeError serialization failures or requiring manual type conversions.

Note that this is optional work and can be skipped. We can discuss this offline.

Fixes b/562164315 🦕

@ohmayr
ohmayr added this pull request to stack #18386 September 16, 2026 22:57

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds duck-typing support for native PyMongo and third-party BSON objects (such as ObjectId, Decimal128, Regex, Timestamp, MinKey, and MaxKey) within the encode_value helper function, along with corresponding unit tests. The review feedback suggests optimizing this performance-critical code path by using an O(1) set lookup for class names to avoid unnecessary attribute checks and recommends running benchmarks to verify that these changes do not introduce performance regressions.

Comment on lines +218 to +241
if hasattr(value, "__class__"):
cls_name = value.__class__.__name__
if cls_name == "ObjectId" and hasattr(value, "binary"):
return encode_value({"__oid__": str(value).lower()})
if cls_name == "Decimal128" and hasattr(value, "to_decimal"):
return encode_value({"__decimal128__": str(value)})
if cls_name == "Regex" and hasattr(value, "pattern"):
opts = getattr(value, "flags", "") or getattr(value, "options", "")
return encode_value(
{"__regex__": {"pattern": value.pattern, "options": str(opts)}}
)
if cls_name == "Timestamp" and hasattr(value, "time") and hasattr(value, "inc"):
return encode_value(
{
"__request_timestamp__": {
"seconds": value.time,
"increment": value.inc,
}
}
)
if cls_name == "MinKey":
return encode_value({"__min__": None})
if cls_name == "MaxKey":
return encode_value({"__max__": None})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The encode_value function is a critical hot path called recursively for every field of every document during serialization. While the proposed change aims to optimize standard types using an O(1) set lookup, any changes to this performance-critical code path must be validated and benchmarked to ensure they do not degrade performance or eliminate fast-path optimizations (such as the overhead of double getattr calls). Please run benchmarks to verify the performance impact of this change.

    cls_name = getattr(getattr(value, "__class__", None), "__name__", None)
    if cls_name in {"ObjectId", "Decimal128", "Regex", "Timestamp", "MinKey", "MaxKey"}:
        if cls_name == "ObjectId" and hasattr(value, "binary"):
            return encode_value({"__oid__": str(value).lower()})
        elif cls_name == "Decimal128" and hasattr(value, "to_decimal"):
            return encode_value({"__decimal128__": str(value)})
        elif cls_name == "Regex" and hasattr(value, "pattern"):
            opts = getattr(value, "flags", "") or getattr(value, "options", "")
            return encode_value(
                {"__regex__": {"pattern": value.pattern, "options": str(opts)}}
            )
        elif cls_name == "Timestamp" and hasattr(value, "time") and hasattr(value, "inc"):
            return encode_value(
                {
                    "__request_timestamp__": {
                        "seconds": value.time,
                        "increment": value.inc,
                    }
                }
            )
        elif cls_name == "MinKey":
            return encode_value({"__min__": None})
        elif cls_name == "MaxKey":
            return encode_value({"__max__": None})
References
  1. For performance-critical code paths executed on every request, validate and benchmark any proposed readability simplifications to ensure they do not degrade performance or eliminate fast-path optimizations.

@ohmayr
ohmayr force-pushed the bson-pr4-duck-typing branch from e40a2d2 to 06445ee Compare September 16, 2026 23:04
@ohmayr
ohmayr marked this pull request as ready for review September 16, 2026 23:11
@ohmayr
ohmayr requested a review from a team as a code owner September 16, 2026 23:11
@ohmayr
ohmayr force-pushed the bson-pr4-duck-typing branch from 06445ee to 80c0c3f Compare September 16, 2026 23:19
@ohmayr
ohmayr force-pushed the bson-pr4-duck-typing branch from 80c0c3f to 740326c Compare September 18, 2026 20:06
Args:
value (Union[NoneType, bool, int, float, datetime.datetime, \
str, bytes, dict, ~google.cloud.Firestore.GeoPoint, \
~google.cloud.firestore_v1.vector.Vector]): A native

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like we should add to this type here

if cls_name == "MinKey":
return encode_value({"__min__": None})
if cls_name == "MaxKey":
return encode_value({"__max__": None})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of encoding all of these special strings here, we should be able to rely on the BSONType class, which has this knowledge built in already. We should have a simple way to find the matching BSONType for this PyMongo class, and a simple way to convert any BSONType to a Value. Then we can just string it together as something like BSONType._from_cls(value)._encode_value()

@ohmayr
ohmayr force-pushed the bson-pr4-duck-typing branch from 740326c to 7038386 Compare September 18, 2026 22:11
@ohmayr
ohmayr force-pushed the bson-pr4-duck-typing branch from 7038386 to 337ec30 Compare September 18, 2026 22:52
@ohmayr
ohmayr force-pushed the bson-pr4-duck-typing branch from 337ec30 to bb5e132 Compare September 19, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants