Skip to content

Commit 330e10d

Browse files
Add config option to print 64-bit integers in JSON as unquoted ints if they can be losslessly converted into a 64-bit float.
PiperOrigin-RevId: 516625978
1 parent 626c7e7 commit 330e10d

File tree

5 files changed

+84
-27
lines changed

5 files changed

+84
-27
lines changed

src/google/protobuf/json/internal/unparser.cc

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@
3131
#include "google/protobuf/json/internal/unparser.h"
3232

3333
#include <cfloat>
34+
#include <cmath>
3435
#include <complex>
3536
#include <cstdint>
3637
#include <cstring>
3738
#include <limits>
3839
#include <memory>
3940
#include <sstream>
4041
#include <string>
42+
#include <type_traits>
4143
#include <utility>
4244

4345
#include "google/protobuf/descriptor.h"
@@ -104,6 +106,33 @@ void WriteEnum(JsonWriter& writer, Field<Traits> field, int32_t value,
104106
}
105107
}
106108

109+
// Returns true if x round-trips through being cast to a double, i.e., if
110+
// x is represenable exactly as a double. This is a slightly weaker condition
111+
// than x < 2^52.
112+
template <typename Int>
113+
bool RoundTripsThroughDouble(Int x) {
114+
auto d = static_cast<double>(x);
115+
// d has guaranteed to be finite with no fractional part, because it came from
116+
// an integer, so we only need to check that it is not outside of the
117+
// representable range of `int`. The way to do this is somewhat not obvious:
118+
// UINT64_MAX isn't representable, and what it gets rounded to when we go
119+
// int->double is unspecified!
120+
//
121+
// Thus, we have to go through ldexp.
122+
double min = 0;
123+
double max_plus_one = std::ldexp(1.0, sizeof(Int) * 8);
124+
if (std::is_signed<Int>::value) {
125+
max_plus_one /= 2;
126+
min = -max_plus_one;
127+
}
128+
129+
if (d < min || d >= max_plus_one) {
130+
return false;
131+
}
132+
133+
return static_cast<Int>(d) == x;
134+
}
135+
107136
// Mutually recursive with functions that follow.
108137
template <typename Traits>
109138
absl::Status WriteMessage(JsonWriter& writer, const Msg<Traits>& msg,
@@ -143,14 +172,24 @@ absl::Status WriteSingular(JsonWriter& writer, Field<Traits> field,
143172
case FieldDescriptor::TYPE_INT64: {
144173
auto x = Traits::GetInt64(field, std::forward<Args>(args)...);
145174
RETURN_IF_ERROR(x.status());
146-
writer.Write(MakeQuoted(*x));
175+
if (writer.options().unquote_int64_if_possible &&
176+
RoundTripsThroughDouble(*x)) {
177+
writer.Write(*x);
178+
} else {
179+
writer.Write(MakeQuoted(*x));
180+
}
147181
break;
148182
}
149183
case FieldDescriptor::TYPE_FIXED64:
150184
case FieldDescriptor::TYPE_UINT64: {
151185
auto x = Traits::GetUInt64(field, std::forward<Args>(args)...);
152186
RETURN_IF_ERROR(x.status());
153-
writer.Write(MakeQuoted(*x));
187+
if (writer.options().unquote_int64_if_possible &&
188+
RoundTripsThroughDouble(*x)) {
189+
writer.Write(*x);
190+
} else {
191+
writer.Write(MakeQuoted(*x));
192+
}
154193
break;
155194
}
156195
case FieldDescriptor::TYPE_SFIXED32:

src/google/protobuf/json/internal/writer.h

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ struct WriterOptions {
6969
bool always_print_enums_as_ints = false;
7070
// Whether to preserve proto field names
7171
bool preserve_proto_field_names = false;
72+
// If set, int64 values that can be represented exactly as a double are
73+
// printed without quotes.
74+
bool unquote_int64_if_possible = false;
7275
// The original parser used by json_util2 accepted a number of non-standard
7376
// options. Setting this flag enables them.
7477
//
@@ -153,8 +156,19 @@ class JsonWriter {
153156
Write(view);
154157
}
155158

156-
void Write(int64_t) = delete;
157-
void Write(uint64_t) = delete;
159+
void Write(int64_t val) {
160+
char buf[22];
161+
int len = absl::SNPrintF(buf, sizeof(buf), "%d", val);
162+
absl::string_view view(buf, static_cast<size_t>(len));
163+
Write(view);
164+
}
165+
166+
void Write(uint64_t val) {
167+
char buf[22];
168+
int len = absl::SNPrintF(buf, sizeof(buf), "%d", val);
169+
absl::string_view view(buf, static_cast<size_t>(len));
170+
Write(view);
171+
}
158172

159173
template <typename... Ts>
160174
void Write(Quoted<Ts...> val) {
@@ -206,20 +220,6 @@ class JsonWriter {
206220

207221
void WriteQuoted(absl::string_view val) { WriteEscapedUtf8(val); }
208222

209-
void WriteQuoted(int64_t val) {
210-
char buf[22];
211-
int len = absl::SNPrintF(buf, sizeof(buf), "%d", val);
212-
absl::string_view view(buf, static_cast<size_t>(len));
213-
Write(view);
214-
}
215-
216-
void WriteQuoted(uint64_t val) {
217-
char buf[22];
218-
int len = absl::SNPrintF(buf, sizeof(buf), "%d", val);
219-
absl::string_view view(buf, static_cast<size_t>(len));
220-
Write(view);
221-
}
222-
223223
// Tries to write a non-finite double if necessary; returns false if
224224
// nothing was written.
225225
bool MaybeWriteSpecialFp(double val);

src/google/protobuf/json/json.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ absl::Status BinaryToJsonStream(google::protobuf::util::TypeResolver* resolver,
5757
opts.preserve_proto_field_names = options.preserve_proto_field_names;
5858
opts.always_print_enums_as_ints = options.always_print_enums_as_ints;
5959
opts.always_print_primitive_fields = options.always_print_primitive_fields;
60+
opts.unquote_int64_if_possible = options.unquote_int64_if_possible;
6061

6162
// TODO(b/234868512): Drop this setting.
6263
opts.allow_legacy_syntax = true;
@@ -110,6 +111,7 @@ absl::Status MessageToJsonString(const Message& message, std::string* output,
110111
opts.preserve_proto_field_names = options.preserve_proto_field_names;
111112
opts.always_print_enums_as_ints = options.always_print_enums_as_ints;
112113
opts.always_print_primitive_fields = options.always_print_primitive_fields;
114+
opts.unquote_int64_if_possible = options.unquote_int64_if_possible;
113115

114116
// TODO(b/234868512): Drop this setting.
115117
opts.allow_legacy_syntax = true;

src/google/protobuf/json/json.h

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,6 @@ struct ParseOptions {
5656
// this option. If your enum needs to support different casing, consider using
5757
// allow_alias instead.
5858
bool case_insensitive_enum_parsing = false;
59-
60-
ParseOptions()
61-
: ignore_unknown_fields(false), case_insensitive_enum_parsing(false) {}
6259
};
6360

6461
struct PrintOptions {
@@ -75,12 +72,9 @@ struct PrintOptions {
7572
bool always_print_enums_as_ints = false;
7673
// Whether to preserve proto field names
7774
bool preserve_proto_field_names = false;
78-
79-
PrintOptions()
80-
: add_whitespace(false),
81-
always_print_primitive_fields(false),
82-
always_print_enums_as_ints(false),
83-
preserve_proto_field_names(false) {}
75+
// If set, int64 values that can be represented exactly as a double are
76+
// printed without quotes.
77+
bool unquote_int64_if_possible = false;
8478
};
8579

8680
// Converts from protobuf message to JSON and appends it to |output|. This is a

src/google/protobuf/json/json_test.cc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,28 @@ TEST_P(JsonTest, EvilString) {
315315
EXPECT_EQ(m->string_value(), "\n\r\b\f\1\2\3");
316316
}
317317

318+
TEST_P(JsonTest, Unquoted64) {
319+
TestMessage m;
320+
m.add_repeated_int64_value(0);
321+
m.add_repeated_int64_value(42);
322+
m.add_repeated_int64_value(-((int64_t{1} << 60) + 1));
323+
m.add_repeated_int64_value(INT64_MAX);
324+
// This is a power of two and is therefore representable.
325+
m.add_repeated_int64_value(INT64_MIN);
326+
m.add_repeated_uint64_value(0);
327+
m.add_repeated_uint64_value(42);
328+
m.add_repeated_uint64_value((uint64_t{1} << 60) + 1);
329+
// This will be UB without the min/max check in RoundTripsThroughDouble().
330+
m.add_repeated_uint64_value(UINT64_MAX);
331+
332+
PrintOptions opts;
333+
opts.unquote_int64_if_possible = true;
334+
EXPECT_THAT(
335+
ToJson(m, opts),
336+
R"({"repeatedInt64Value":[0,42,"-1152921504606846977","9223372036854775807",-9223372036854775808],)"
337+
R"("repeatedUint64Value":[0,42,"1152921504606846977","18446744073709551615"]})");
338+
}
339+
318340
TEST_P(JsonTest, TestAlwaysPrintEnumsAsInts) {
319341
TestMessage orig;
320342
orig.set_enum_value(proto3::BAR);

0 commit comments

Comments
 (0)