GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — perf: Replace MessageToDict with optimized custom dict builder by abhijeet-dhumal · Pull Request #6015 · feast-dev/feast

perf: Replace MessageToDict with optimized custom dict builder #6015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Sign up for GitHub

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
franciscojavierarceo merged 12 commits into feast-dev:master from abhijeet-dhumal:fix/custom-json-serialization-6013
May 28, 2026

Uh oh!

There was an error while loading. Please reload this page.

Merged

perf: Replace MessageToDict with optimized custom dict builder #6015

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
12 commits
Select commit Hold shift + click to select a range
b28ddf9
perf: Replace MessageToDict with optimized custom dict builder
abhijeet-dhumal Feb 25, 2026
40846ad
Fix test: Use GetOnlineFeaturesResponseMetadata instead of non-existe…
abhijeet-dhumal Mar 2, 2026
ad4882b
fix: Rename to convert_response_to_dict, fix timestamp float-rounding…
abhijeet-dhumal Mar 10, 2026
698d0a1
fix: Return raw bytes to match proto_json.patch() behavior
abhijeet-dhumal Mar 10, 2026
0c0637b
fix: serialize top-level status field in convert_response_to_dict
abhijeet-dhumal May 27, 2026
3ac1755
fix: replace deprecated ORJSONResponse with JSONResponse
abhijeet-dhumal May 27, 2026
5fff274
fix: handle set, list, and map Value types in _value_to_native
abhijeet-dhumal May 27, 2026
b2f2909
fix: warn on scalar_map_val; fix test_unix_timestamp_list_val; docume…
abhijeet-dhumal May 27, 2026
c559ac5
docs: document float_precision=18 trade-off; add double_val round-tri…
abhijeet-dhumal May 27, 2026
388f89b
fix: remove leftover orjson dead code causing mypy no-redef error
abhijeet-dhumal May 27, 2026
ecce3cf
fix: base64-encode bytes_val and bytes_list_val for JSON serialization
abhijeet-dhumal May 27, 2026
25e99b1
style: remove extra blank line in feature_server_utils.py
abhijeet-dhumal May 27, 2026
File filter
Filter by extension

Clear filters
Conversations
Failed to load comments.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Jump to
Jump to file
Failed to load files.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Diff view
Diff view

There are no files selected for viewing

16 changes: 5 additions & 11 deletions 16 sdk/python/feast/feature_server.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
from fastapi.logger import logger
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from google.protobuf.json_format import MessageToDict
from pydantic import BaseModel, field_validator

import feast
Expand All @@ -52,6 +51,7 @@
FeastError,
)
from feast.feast_object import FeastObject
from feast.feature_server_utils import convert_response_to_dict
from feast.feature_view_utils import get_feature_view_from_feature_store
from feast.permissions.action import WRITE, AuthzedAction
from feast.permissions.security_manager import assert_permissions
Expand Down Expand Up @@ -392,12 +392,9 @@ async def get_online_features(request: GetOnlineFeaturesRequest) -> Any:
)

response_dict = await run_in_threadpool(
MessageToDict,
response.proto,
preserving_proto_field_name=True,
float_precision=18,
convert_response_to_dict, response.proto
)
return response_dict
return JSONResponse(content=response_dict)

@app.post(
"/retrieve-online-documents",
Expand Down Expand Up @@ -434,12 +431,9 @@ async def retrieve_online_documents(
)

response_dict = await run_in_threadpool(
MessageToDict,
response.proto,
preserving_proto_field_name=True,
float_precision=18,
convert_response_to_dict, response.proto
)
return response_dict
return JSONResponse(content=response_dict)

@app.post("/push", dependencies=[Depends(inject_user_details)])
async def push(request: PushFeaturesRequest) -> Response:
Expand Down
141 changes: 141 additions & 0 deletions 141 sdk/python/feast/feature_server_utils.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""Fast serialization utilities for Feature Server responses.

Matches the output format of MessageToDict with proto_json.patch() applied.
Values are serialized as native Python types (not wrapped dicts).
"""

import base64
import logging
from datetime import datetime, timezone
from typing import Any, Dict, Optional

from feast.protos.feast.serving.ServingService_pb2 import GetOnlineFeaturesResponse
from feast.protos.feast.types.Value_pb2 import Value

logger = logging.getLogger(__name__)

# FieldStatus enum mapping (protos/feast/serving/ServingService.proto)
_STATUS_NAMES: Dict[int, str] = {
0: "INVALID",
1: "PRESENT",
2: "NULL_VALUE",
3: "NOT_FOUND",
4: "OUTSIDE_MAX_AGE",
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved

Uh oh!

There was an error while loading. Please reload this page.



def convert_response_to_dict(response: GetOnlineFeaturesResponse) -> Dict[str, Any]:
"""Convert GetOnlineFeaturesResponse to a JSON-serializable dict.

Matches the structure produced by MessageToDict(proto, preserving_proto_field_name=True)
with proto_json.patch() applied, with one intentional difference:

- double_val fields are returned as Python float objects (json.dumps uses Python 3.1+
shortest round-trip form, ~15-17 sig digits) rather than 18 fixed significant digits
(float_precision=18). Values are numerically identical; only the JSON string length
may differ. This is safe for all ML feature types and avoids unnecessary precision
overhead.
"""
result: Dict[str, Any] = {
"results": [
{
"values": [_value_to_native(v) for v in feature_vector.values],
"statuses": [
_STATUS_NAMES.get(s, "INVALID") for s in feature_vector.statuses
],
**(
{
"event_timestamps": [
_timestamp_to_str(ts)
for ts in feature_vector.event_timestamps
]
}
if feature_vector.event_timestamps
else {}
),
}
for feature_vector in response.results
]
}

if response.HasField("metadata"):
result["metadata"] = _metadata_to_dict(response.metadata)

if response.status:
result["status"] = response.status

return result
Comment thread
abhijeet-dhumal marked this conversation as resolved.
Resolved

Uh oh!

There was an error while loading. Please reload this page.



def _value_to_native(v: Value) -> Optional[Any]:
"""Convert a Value proto to a JSON-serializable Python type.

bytes_val and bytes_list_val are base64-encoded (RFC 4648) so that
JSONResponse can serialize them without TypeError. This matches standard
protobuf JSON encoding for bytes fields and is safe for all HTTP clients.
"""
which = v.WhichOneof("val")
if which is None or which == "null_val":
return None
# bytes must be base64-encoded for JSON serialization
elif which == "bytes_val":
return base64.b64encode(v.bytes_val).decode("ascii")
# RepeatedValue — nested Values that must be recursively converted
elif which in ("list_val", "set_val"):
return [_value_to_native(nested) for nested in getattr(v, which).val]
# Map<string, Value> — recursively convert nested Values
elif which in ("map_val", "struct_val"):
return {k: _value_to_native(vv) for k, vv in getattr(v, which).val.items()}
# MapList — list of Map<string, Value>
elif which in ("map_list_val", "struct_list_val"):
return [
{k: _value_to_native(vv) for k, vv in m.val.items()}
for m in getattr(v, which).val
]
# scalar_map_val has non-string keys; full conversion requires extra work and
# this type is not returned by standard get_online_features paths today.
elif which == "scalar_map_val":
logger.warning(
"scalar_map_val is not yet supported by convert_response_to_dict; value will be None"
)
return None
# bytes_list_val / bytes_set_val — base64-encode each element
elif which in ("bytes_list_val", "bytes_set_val"):
return [base64.b64encode(b).decode("ascii") for b in getattr(v, which).val]
# All other list/set types have scalar .val fields
elif "_list_" in which or "_set_" in which:
return list(getattr(v, which).val)
else:
return getattr(v, which)
Comment thread
Comment on lines +77 to +109

@devin-ai-integration devin-ai-integration Bot May 27, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

🔴 bytes_val values cause ORJSONResponse to raise TypeError

_value_to_native at feature_server_utils.py:68 returns raw bytes for bytes_val fields. Even if orjson were installed, orjson.dumps() cannot serialize Python bytes objects and raises TypeError: Type is not JSON serializable: bytes. The old code path returned the dict to FastAPI which processed it through jsonable_encoder (capable of handling bytes). The switch to ORJSONResponse bypasses that encoder, causing a regression for any response containing bytes-typed feature values.

Prompt for agents
In _value_to_native (feature_server_utils.py), when the value type is bytes_val, the raw bytes object is returned. orjson cannot serialize bytes. You need to either: (1) base64-encode bytes values before returning them (e.g. using base64.b64encode(val).decode('ascii')), or (2) not use ORJSONResponse and instead use a serializer that handles bytes. Note that bytes_list_val (handled by the _list_ branch) would also produce a list of bytes objects with the same issue.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@ntkathole ntkathole May 27, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

@abhijeet-dhumal bytes_val is still problem since you are using JSONResponse

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@abhijeet-dhumal abhijeet-dhumal May 27, 2026

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Thanks for catching this, Nikhil! Now bytes_val and bytes_list_val now base64-encode the output before returning, so JSONResponse / json.dumps can serialize them without TypeError.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@abhijeet-dhumal abhijeet-dhumal May 27, 2026

Copy link
Copy Markdown
Contributor Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Please review!

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions


def _timestamp_to_str(ts) -> str:
"""Convert protobuf Timestamp to RFC 3339 format with Z suffix.

Uses adaptive precision to match MessageToDict output:
- No fractional seconds when nanos == 0
- 3 digits (milliseconds) when nanos % 1_000_000 == 0
- 6 digits (microseconds) when nanos % 1_000 == 0
- 9 digits (nanoseconds) otherwise
"""
if ts.seconds == 0 and ts.nanos == 0:
return "1970-01-01T00:00:00Z"
dt = datetime.fromtimestamp(ts.seconds, tz=timezone.utc)
base = dt.strftime("%Y-%m-%dT%H:%M:%S")
nanos = ts.nanos
if nanos == 0:
return base + "Z"
elif nanos % 1_000_000 == 0:
return base + ".%03dZ" % (nanos // 1_000_000)
elif nanos % 1_000 == 0:
return base + ".%06dZ" % (nanos // 1_000)
else:
return base + ".%09dZ" % nanos


def _metadata_to_dict(metadata) -> Dict[str, Any]:
"""Convert FeatureResponseMeta to dict (matches proto_json.patch() format)."""
result: Dict[str, Any] = {}
if metadata.HasField("feature_names"):
result["feature_names"] = list(metadata.feature_names.val)
return result
Loading
Oops, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Loading
Oops, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.