diff --git a/README.md b/README.md index ac8c1dd..c973f44 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufJacksonConfig.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufJacksonConfig.java index 676db2a..99cc93c 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufJacksonConfig.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufJacksonConfig.java @@ -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() { @@ -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() {} @@ -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; + } } diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufModule.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufModule.java index d453f39..050365c 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufModule.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufModule.java @@ -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); diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufSerializer.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufSerializer.java index a0df340..285f1b6 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufSerializer.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufSerializer.java @@ -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 extends StdSerializer { private static final String NULL_VALUE_FULL_NAME = NullValue.getDescriptor().getFullName(); + @SuppressFBWarnings(value="SE_BAD_FIELD") + protected final ProtobufJacksonConfig config; private final Map, JsonSerializer> serializerCache; - public ProtobufSerializer(Class protobufType) { + public ProtobufSerializer(Class protobufType, ProtobufJacksonConfig config) { super(protobufType); - + this.config = config; this.serializerCache = new ConcurrentHashMap<>(); } @@ -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); diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/DurationSerializer.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/DurationSerializer.java index 030f865..547ff32 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/DurationSerializer.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/DurationSerializer.java @@ -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 { - public DurationSerializer() { - super(Duration.class); + public DurationSerializer(ProtobufJacksonConfig config) { + super(Duration.class, config); } @Override diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/FieldMaskSerializer.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/FieldMaskSerializer.java index 08e74ec..f4499a1 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/FieldMaskSerializer.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/FieldMaskSerializer.java @@ -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 { - public FieldMaskSerializer() { - super(FieldMask.class); + public FieldMaskSerializer(ProtobufJacksonConfig config) { + super(FieldMask.class, config); } @Override diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/ListValueSerializer.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/ListValueSerializer.java index cbb18ba..02ab083 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/ListValueSerializer.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/ListValueSerializer.java @@ -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 { private static final FieldDescriptor VALUES_FIELD = ListValue.getDescriptor().findFieldByName("values"); - public ListValueSerializer() { - super(ListValue.class); + public ListValueSerializer(ProtobufJacksonConfig config) { + super(ListValue.class, config); } @Override diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/MessageSerializer.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/MessageSerializer.java index be95650..4fde64b 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/MessageSerializer.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/MessageSerializer.java @@ -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 { - @SuppressFBWarnings(value="SE_BAD_FIELD") - private final ProtobufJacksonConfig config; private final boolean unwrappingSerializer; private final Map propertyNamingCache; @@ -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<>(); } diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/StructSerializer.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/StructSerializer.java index d61817c..b4913c9 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/StructSerializer.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/StructSerializer.java @@ -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 { private static final FieldDescriptor FIELDS_FIELD = Struct.getDescriptor().findFieldByName("fields"); - public StructSerializer() { - super(Struct.class); + public StructSerializer(ProtobufJacksonConfig config) { + super(Struct.class, config); } @Override diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/TimestampSerializer.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/TimestampSerializer.java index 4acd550..92b4efb 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/TimestampSerializer.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/TimestampSerializer.java @@ -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 { - public TimestampSerializer() { - super(Timestamp.class); + public TimestampSerializer(ProtobufJacksonConfig config) { + super(Timestamp.class, config); } @Override diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/ValueSerializer.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/ValueSerializer.java index d7c6b8e..0ca8514 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/ValueSerializer.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/ValueSerializer.java @@ -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 { - public ValueSerializer() { - super(Value.class); + public ValueSerializer(ProtobufJacksonConfig config) { + super(Value.class, config); } @Override diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/WrappedPrimitiveSerializer.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/WrappedPrimitiveSerializer.java index f683e6c..d57625c 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/WrappedPrimitiveSerializer.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/builtin/serializers/WrappedPrimitiveSerializer.java @@ -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 extends ProtobufSerializer { - public WrappedPrimitiveSerializer(Class wrapperType) { - super(wrapperType); + public WrappedPrimitiveSerializer(Class wrapperType, ProtobufJacksonConfig config) { + super(wrapperType, config); } @Override diff --git a/src/test/java/com/hubspot/jackson/datatype/protobuf/WriteLongsAsStringsTest.java b/src/test/java/com/hubspot/jackson/datatype/protobuf/WriteLongsAsStringsTest.java new file mode 100644 index 0000000..ce64f34 --- /dev/null +++ b/src/test/java/com/hubspot/jackson/datatype/protobuf/WriteLongsAsStringsTest.java @@ -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); + } +}