@@ -70,6 +70,32 @@ class RemoteOnlineStore(OnlineStore): |
70 | 70 | remote online store implementation wrapper to communicate with feast online server. |
71 | 71 | """ |
72 | 72 |
|
| 73 | + @staticmethod |
| 74 | + def _proto_value_to_transport_value(proto_value: ValueProto) -> Any: |
| 75 | + """ |
| 76 | + Convert a proto Value to a JSON-serializable Python value suitable for |
| 77 | + HTTP transport. Unlike ``feast_value_type_to_python_type``, this keeps |
| 78 | + ``json_val`` as a raw string so the receiving server can reconstruct a |
| 79 | + DataFrame whose column types match the original (string for JSON, dict |
| 80 | + for Map/Struct). Parsing JSON strings into dicts would cause PyArrow to |
| 81 | + infer a struct column on the server, which can crash with complex nested |
| 82 | + types (lists inside dicts). |
| 83 | + """ |
| 84 | + val_attr = proto_value.WhichOneof("val") |
| 85 | + if val_attr is None: |
| 86 | + return None |
| 87 | + |
| 88 | + # Keep JSON values as raw strings for correct DataFrame reconstruction. |
| 89 | + # Parsing them into dicts causes PyArrow to infer struct columns on the |
| 90 | + # server whose nested lists round-trip as numpy arrays, breaking |
| 91 | + # json.dumps during proto conversion. |
| 92 | + if val_attr == "json_val": |
| 93 | + return getattr(proto_value, val_attr) |
| 94 | + if val_attr == "json_list_val": |
| 95 | + return list(getattr(proto_value, val_attr).val) |
| 96 | + |
| 97 | + return feast_value_type_to_python_type(proto_value) |
| 98 | + |
73 | 99 | def online_write_batch( |
74 | 100 | self, |
75 | 101 | config: RepoConfig, |
@@ -97,10 +123,11 @@ def online_write_batch( |
97 | 123 | feast_value_type_to_python_type(entity_value_proto) |
98 | 124 | ) |
99 | 125 |
|
100 | | - # Populate feature values |
| 126 | + # Populate feature values – use transport-safe conversion that |
| 127 | + # preserves JSON strings instead of parsing them into dicts. |
101 | 128 | for feature_name, feature_value_proto in feature_values_proto.items(): |
102 | 129 | columnar_data[feature_name].append( |
103 | | - feast_value_type_to_python_type(feature_value_proto) |
| 130 | + self._proto_value_to_transport_value(feature_value_proto) |
104 | 131 | ) |
105 | 132 |
|
106 | 133 | # Populate timestamps |
|