Skip to content

Commit

Permalink
Support writing longs as strings (opt-in)
Browse files Browse the repository at this point in the history
`jackson-datatype-protobuf` is a missing link in the Jackson ecosystem,
but it doesn't currently provide a canonical representation of long
integers, which poses a problem for strict parsers given the limitation
of the maximum value to
[2^53 - 1](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)
in JavaScript, whereas this value is for instance
[2^63 - 1](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html)
in Java.

The change proposed here is to provide users who need it with an option
to activate this canonical representation, without altering the existing
default behavior.
  • Loading branch information
rdesgroppes committed Jun 9, 2023
1 parent ea45ad7 commit b797d1f
Show file tree
Hide file tree
Showing 13 changed files with 131 additions and 41 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ after which functionality is available for all normal Jackson operations.
### Interop with Protobuf 3 Canonical JSON Representation

Protobuf 3 specifies a canonical JSON representation (available [here](https://developers.google.com/protocol-buffers/docs/proto3#json)). This library conforms to that representation with a few exceptions:
- int64, fixed64, uint64 are written as JSON numbers instead of strings
- int64, fixed64, uint64 are written as JSON numbers instead of strings. However, you may opt for a conformal representation by means of:
```java
ProtobufJacksonConfig config = ProtobufJacksonConfig.builder().writeLongsAsStrings(true).build();
new ObjectMapper().registerModules(new ProtobufModule(config));
```
- `Any` objects don't have any special handling, so the value will be a base64 string, and the type URL field name is `typeUrl` instead of `@type`

### Protobuf 2 Support
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.hubspot.jackson.datatype.protobuf;

import java.io.IOException;

import com.fasterxml.jackson.core.JsonGenerator;
import com.google.protobuf.ExtensionRegistry;

public class ProtobufJacksonConfig {
private final ExtensionRegistryWrapper extensionRegistry;
private final boolean acceptLiteralFieldnames;
private final LongWriter longWriter;

private ProtobufJacksonConfig(ExtensionRegistryWrapper extensionRegistry, boolean acceptLiteralFieldnames) {
private ProtobufJacksonConfig(ExtensionRegistryWrapper extensionRegistry, boolean acceptLiteralFieldnames, LongWriter longWriter) {
this.extensionRegistry = extensionRegistry;
this.acceptLiteralFieldnames = acceptLiteralFieldnames;
this.longWriter = longWriter;
}

public static Builder builder() {
Expand All @@ -23,9 +28,14 @@ public boolean acceptLiteralFieldnames() {
return acceptLiteralFieldnames;
}

public LongWriter longWriter() {
return longWriter;
}

public static class Builder {
private ExtensionRegistryWrapper extensionRegistry = ExtensionRegistryWrapper.empty();
private boolean acceptLiteralFieldnames = false;
private boolean writeLongsAsStrings = false;

private Builder() {}

Expand All @@ -43,8 +53,19 @@ public Builder acceptLiteralFieldnames(boolean acceptLiteralFieldnames) {
return this;
}

public Builder writeLongsAsStrings(boolean writeLongsAsStrings) {
this.writeLongsAsStrings = writeLongsAsStrings;
return this;
}

public ProtobufJacksonConfig build() {
return new ProtobufJacksonConfig(extensionRegistry, acceptLiteralFieldnames);
return new ProtobufJacksonConfig(extensionRegistry, acceptLiteralFieldnames, writeLongsAsStrings
? (generator, value) -> generator.writeString(String.valueOf(value)) : JsonGenerator::writeNumber);
}
}

@FunctionalInterface
public interface LongWriter {
void write(JsonGenerator generator, long value) throws IOException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,22 @@ public Version version() {
public void setupModule(SetupContext context) {
SimpleSerializers serializers = new SimpleSerializers();
serializers.addSerializer(new MessageSerializer(config));
serializers.addSerializer(new DurationSerializer());
serializers.addSerializer(new FieldMaskSerializer());
serializers.addSerializer(new ListValueSerializer());
serializers.addSerializer(new DurationSerializer(config));
serializers.addSerializer(new FieldMaskSerializer(config));
serializers.addSerializer(new ListValueSerializer(config));
serializers.addSerializer(new NullValueSerializer());
serializers.addSerializer(new StructSerializer());
serializers.addSerializer(new TimestampSerializer());
serializers.addSerializer(new ValueSerializer());
serializers.addSerializer(new WrappedPrimitiveSerializer<>(DoubleValue.class));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(FloatValue.class));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(Int64Value.class));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(UInt64Value.class));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(Int32Value.class));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(UInt32Value.class));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(BoolValue.class));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(StringValue.class));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(BytesValue.class));
serializers.addSerializer(new StructSerializer(config));
serializers.addSerializer(new TimestampSerializer(config));
serializers.addSerializer(new ValueSerializer(config));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(DoubleValue.class, config));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(FloatValue.class, config));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(Int64Value.class, config));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(UInt64Value.class, config));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(Int32Value.class, config));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(UInt32Value.class, config));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(BoolValue.class, config));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(StringValue.class, config));
serializers.addSerializer(new WrappedPrimitiveSerializer<>(BytesValue.class, config));

context.addSerializers(serializers);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@
import com.google.protobuf.Message;
import com.google.protobuf.MessageOrBuilder;
import com.google.protobuf.NullValue;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

public abstract class ProtobufSerializer<T extends MessageOrBuilder> extends StdSerializer<T> {
private static final String NULL_VALUE_FULL_NAME = NullValue.getDescriptor().getFullName();

@SuppressFBWarnings(value="SE_BAD_FIELD")
protected final ProtobufJacksonConfig config;
private final Map<Class<?>, JsonSerializer<Object>> serializerCache;

public ProtobufSerializer(Class<T> protobufType) {
public ProtobufSerializer(Class<T> protobufType, ProtobufJacksonConfig config) {
super(protobufType);

this.config = config;
this.serializerCache = new ConcurrentHashMap<>();
}

Expand Down Expand Up @@ -64,7 +67,7 @@ protected void writeValue(
generator.writeNumber((Integer) value);
break;
case LONG:
generator.writeNumber((Long) value);
config.longWriter().write(generator, (Long) value);
break;
case FLOAT:
generator.writeNumber((Float) value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import com.fasterxml.jackson.databind.SerializerProvider;
import com.google.protobuf.Duration;
import com.google.protobuf.util.Durations;
import com.hubspot.jackson.datatype.protobuf.ProtobufJacksonConfig;
import com.hubspot.jackson.datatype.protobuf.ProtobufSerializer;

public class DurationSerializer extends ProtobufSerializer<Duration> {

public DurationSerializer() {
super(Duration.class);
public DurationSerializer(ProtobufJacksonConfig config) {
super(Duration.class, config);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import com.fasterxml.jackson.databind.SerializerProvider;
import com.google.protobuf.FieldMask;
import com.google.protobuf.util.FieldMaskUtil;
import com.hubspot.jackson.datatype.protobuf.ProtobufJacksonConfig;
import com.hubspot.jackson.datatype.protobuf.ProtobufSerializer;

public class FieldMaskSerializer extends ProtobufSerializer<FieldMask> {

public FieldMaskSerializer() {
super(FieldMask.class);
public FieldMaskSerializer(ProtobufJacksonConfig config) {
super(FieldMask.class, config);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.ListValue;
import com.google.protobuf.Value;
import com.hubspot.jackson.datatype.protobuf.ProtobufJacksonConfig;
import com.hubspot.jackson.datatype.protobuf.ProtobufSerializer;

public class ListValueSerializer extends ProtobufSerializer<ListValue> {
private static final FieldDescriptor VALUES_FIELD = ListValue.getDescriptor().findFieldByName("values");

public ListValueSerializer() {
super(ListValue.class);
public ListValueSerializer(ProtobufJacksonConfig config) {
super(ListValue.class, config);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,7 @@
import com.hubspot.jackson.datatype.protobuf.ProtobufSerializer;
import com.hubspot.jackson.datatype.protobuf.internal.PropertyNamingCache;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

public class MessageSerializer extends ProtobufSerializer<MessageOrBuilder> {
@SuppressFBWarnings(value="SE_BAD_FIELD")
private final ProtobufJacksonConfig config;
private final boolean unwrappingSerializer;
private final Map<Descriptor, PropertyNamingCache> propertyNamingCache;

Expand All @@ -45,8 +41,7 @@ public MessageSerializer(ProtobufJacksonConfig config) {
}

private MessageSerializer(ProtobufJacksonConfig config, boolean unwrappingSerializer) {
super(MessageOrBuilder.class);
this.config = config;
super(MessageOrBuilder.class, config);
this.unwrappingSerializer = unwrappingSerializer;
this.propertyNamingCache = new ConcurrentHashMap<>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import com.fasterxml.jackson.databind.SerializerProvider;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Struct;
import com.hubspot.jackson.datatype.protobuf.ProtobufJacksonConfig;
import com.hubspot.jackson.datatype.protobuf.ProtobufSerializer;

public class StructSerializer extends ProtobufSerializer<Struct> {
private static final FieldDescriptor FIELDS_FIELD = Struct.getDescriptor().findFieldByName("fields");

public StructSerializer() {
super(Struct.class);
public StructSerializer(ProtobufJacksonConfig config) {
super(Struct.class, config);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import com.fasterxml.jackson.databind.SerializerProvider;
import com.google.protobuf.Timestamp;
import com.google.protobuf.util.Timestamps;
import com.hubspot.jackson.datatype.protobuf.ProtobufJacksonConfig;
import com.hubspot.jackson.datatype.protobuf.ProtobufSerializer;

public class TimestampSerializer extends ProtobufSerializer<Timestamp> {

public TimestampSerializer() {
super(Timestamp.class);
public TimestampSerializer(ProtobufJacksonConfig config) {
super(Timestamp.class, config);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
import com.fasterxml.jackson.databind.SerializerProvider;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Value;
import com.hubspot.jackson.datatype.protobuf.ProtobufJacksonConfig;
import com.hubspot.jackson.datatype.protobuf.ProtobufSerializer;

public class ValueSerializer extends ProtobufSerializer<Value> {

public ValueSerializer() {
super(Value.class);
public ValueSerializer(ProtobufJacksonConfig config) {
super(Value.class, config);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import com.fasterxml.jackson.databind.SerializerProvider;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.MessageOrBuilder;
import com.hubspot.jackson.datatype.protobuf.ProtobufJacksonConfig;
import com.hubspot.jackson.datatype.protobuf.ProtobufSerializer;

public class WrappedPrimitiveSerializer<T extends MessageOrBuilder> extends ProtobufSerializer<T> {

public WrappedPrimitiveSerializer(Class<T> wrapperType) {
super(wrapperType);
public WrappedPrimitiveSerializer(Class<T> wrapperType, ProtobufJacksonConfig config) {
super(wrapperType, config);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.hubspot.jackson.datatype.protobuf;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;

import org.junit.Test;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hubspot.jackson.datatype.protobuf.util.TestProtobuf.AllFields;

public class WriteLongsAsStringsTest {

@Test
public void itDoesntWriteLongsAsStringsByDefault() throws IOException {
ProtobufJacksonConfig config = ProtobufJacksonConfig.builder().build();

assertThat(writeSomeInt64s(config)).isEqualTo("{\"int64\":-42,\"uint64\":42}");
}

@Test
public void itDoesntWriteLongsAsStringsWhenDisabled() throws IOException {
ProtobufJacksonConfig config = ProtobufJacksonConfig.builder().writeLongsAsStrings(false).build();

assertThat(writeSomeInt64s(config)).isEqualTo("{\"int64\":-42,\"uint64\":42}");
}

@Test
public void itDoesntWriteLongsAsStringsWhenEnabledThenDisabled() throws IOException {
ProtobufJacksonConfig config = ProtobufJacksonConfig.builder()
.writeLongsAsStrings(true)
.writeLongsAsStrings(false)
.build();

assertThat(writeSomeInt64s(config)).isEqualTo("{\"int64\":-42,\"uint64\":42}");
}

@Test
public void itWritesLongsAsStringsWhenEnabled() throws IOException {
ProtobufJacksonConfig config = ProtobufJacksonConfig.builder().writeLongsAsStrings(true).build();

assertThat(writeSomeInt64s(config)).isEqualTo("{\"int64\":\"-42\",\"uint64\":\"42\"}");
}

@Test
public void itWritesLongsAsStringsWhenDisabledThenEnabled() throws IOException {
ProtobufJacksonConfig config = ProtobufJacksonConfig.builder()
.writeLongsAsStrings(false)
.writeLongsAsStrings(true)
.build();

assertThat(writeSomeInt64s(config)).isEqualTo("{\"int64\":\"-42\",\"uint64\":\"42\"}");
}

private static String writeSomeInt64s(final ProtobufJacksonConfig config) throws IOException {
ObjectMapper mapper = new ObjectMapper().registerModules(new ProtobufModule(config));
AllFields someInt64s = AllFields.newBuilder().setInt64(-42).setUint64(42).build();
return mapper.writeValueAsString(someInt64s);
}
}

0 comments on commit b797d1f

Please sign in to comment.