Skip to content
This repository was archived by the owner on Feb 23, 2026. It is now read-only.

Commit 0a582e6

Browse files
committed
fix: setting 64bit fields from strings supported
Due to limitations in certain browser/javascript combinations, fields that are 64 bit integer types are converted to strings when encoding a message to JSON or a dict. Decoding from JSON handles this explicitly, but it makes converting back from a dict an error. This fix adds support for setting these 64 bit fields explicitly from strings. E.g. class Squid(proto.Message): mass_kg = proto.Field(proto.INT64, number=1) s = Squid(mass_kg=10) s_dict = Squid.to_dict(s) assert s == Squid(**s_dict) # Supported assert s == Squid(s_dict) # NOT supported for performance reasons s.mass_kg = "20" # Supported
1 parent debd16f commit 0a582e6

4 files changed

Lines changed: 120 additions & 0 deletions

File tree

docs/marshal.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,28 @@ Protocol buffer type Python type Nullable
7272
7373
assert msg == msg_pb == msg_two
7474
75+
.. warning::
7576

77+
Due to certain browser/javascript limitations, 64 bit sized fields, e.g. INT64, UINT64,
78+
are converted to strings when marshalling messages to dictionaries or JSON.
79+
Decoding JSON handles this correctly, but dicts must be unpacked when reconstructing messages. This is necessary to trigger a special case workaround.
80+
81+
.. code-block:: python
82+
83+
import proto
84+
85+
class MyMessage(proto.Message):
86+
serial_id = proto.Field(proto.INT64, number=1)
87+
88+
msg = MyMessage(serial_id=12345)
89+
msg_dict = MyMessage.to_dict(msg)
90+
91+
msg_2 = MyMessage(msg_dict) # Raises an exception
92+
93+
msg_3 = MyMessage(**msg_dict) # Works without exception
94+
assert msg == msg_3
95+
96+
7697
Wrapper types
7798
-------------
7899

proto/marshal/marshal.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from proto.marshal.collections import Repeated
2727
from proto.marshal.collections import RepeatedComposite
2828
from proto.marshal.rules import bytes as pb_bytes
29+
from proto.marshal.rules import stringy_numbers
2930
from proto.marshal.rules import dates
3031
from proto.marshal.rules import struct
3132
from proto.marshal.rules import wrappers
@@ -147,6 +148,11 @@ def reset(self):
147148
# Special case for bytes to allow base64 encode/decode
148149
self.register(ProtoType.BYTES, pb_bytes.BytesRule())
149150

151+
# Special case for int64 from strings because of dict round trip.
152+
# See https://github.com/protocolbuffers/protobuf/issues/2679
153+
for rule_class in stringy_numbers.STRINGY_NUMBER_RULES:
154+
self.register(rule_class._proto_type, rule_class())
155+
150156
def to_python(self, proto_type, value, *, absent: bool = None):
151157
# Internal protobuf has its own special type for lists of values.
152158
# Return a view around it that implements MutableSequence.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Copyright (C) 2021 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from proto.primitives import ProtoType
16+
17+
18+
class StringyNumberRule:
19+
"""A marshal between certain numeric types and strings
20+
21+
This is a necessary hack to allow round trip conversion
22+
from messages to dicts back to messages.
23+
24+
See https://github.com/protocolbuffers/protobuf/issues/2679
25+
and
26+
https://developers.google.com/protocol-buffers/docs/proto3#json
27+
for more details.
28+
"""
29+
30+
def to_python(self, value, *, absent: bool = None):
31+
return value
32+
33+
def to_proto(self, value):
34+
return self._python_type(value)
35+
36+
37+
class Int64Rule(StringyNumberRule):
38+
_python_type = int
39+
_proto_type = ProtoType.INT64
40+
41+
42+
class UInt64Rule(StringyNumberRule):
43+
_python_type = int
44+
_proto_type = ProtoType.UINT64
45+
46+
47+
class SInt64Rule(StringyNumberRule):
48+
_python_type = int
49+
_proto_type = ProtoType.SINT64
50+
51+
52+
class Fixed64Rule(StringyNumberRule):
53+
_python_type = int
54+
_proto_type = ProtoType.FIXED64
55+
56+
57+
class SFixed64Rule(StringyNumberRule):
58+
_python_type = int
59+
_proto_type = ProtoType.SFIXED64
60+
61+
62+
STRINGY_NUMBER_RULES = [
63+
Int64Rule,
64+
UInt64Rule,
65+
SInt64Rule,
66+
Fixed64Rule,
67+
SFixed64Rule,
68+
]

tests/test_fields_int.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,28 @@ class Foo(proto.Message):
9393

9494
bar_field = Foo.meta.fields["bar"]
9595
assert bar_field.descriptor is bar_field.descriptor
96+
97+
98+
def test_int64_dict_round_trip():
99+
# When converting a message to other types, protobuf turns int64 fields
100+
# into decimal coded strings.
101+
# This is not a problem for round trip JSON, but it is a problem
102+
# when doing a round trip conversion from a message to a dict to a message.
103+
# See https://github.com/protocolbuffers/protobuf/issues/2679
104+
# and
105+
# https://developers.google.com/protocol-buffers/docs/proto3#json
106+
# for more details.
107+
class Squid(proto.Message):
108+
mass_kg = proto.Field(proto.INT64, number=1)
109+
length_cm = proto.Field(proto.UINT64, number=2)
110+
age_s = proto.Field(proto.FIXED64, number=3)
111+
depth_m = proto.Field(proto.SFIXED64, number=4)
112+
serial_num = proto.Field(proto.SINT64, number=5)
113+
114+
s = Squid(mass_kg=10, length_cm=20, age_s=30, depth_m=40, serial_num=50)
115+
116+
s_dict = Squid.to_dict(s)
117+
118+
s2 = Squid(**s_dict)
119+
120+
assert s == s2

0 commit comments

Comments
 (0)