From f584f6400cbc1bf28607a8767ee5c7382a126076 Mon Sep 17 00:00:00 2001 From: Matt Ball Date: Wed, 6 Mar 2019 16:02:05 -0500 Subject: [PATCH 1/5] Add a generic enum-as-number serdes This is particularly useful when you want to serialize a specific field on a specific type in this manner, without modifying the root object mapper config. --- .../protobuf/ProtobufEnumAsNumber.java | 79 +++++++++++++++++++ .../protobuf/ProtobufEnumAsNumberTest.java | 56 +++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java create mode 100644 src/test/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumberTest.java diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java new file mode 100644 index 0000000..370920c --- /dev/null +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java @@ -0,0 +1,79 @@ +package com.hubspot.jackson.datatype.protobuf; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.google.common.base.Preconditions; +import com.google.protobuf.ProtocolMessageEnum; + +public final class ProtobufEnumAsNumber { + + private ProtobufEnumAsNumber() {} + + public static class Serializer extends StdSerializer { + + protected Serializer() { + super(ProtocolMessageEnum.class); + } + + @Override + public void serialize(ProtocolMessageEnum protocolMessageEnum, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { + jsonGenerator.writeNumber(protocolMessageEnum.getNumber()); + } + } + + public static class Deserializer extends StdDeserializer implements ContextualDeserializer { + + private final JavaType javaType; + + protected Deserializer() { + this(null); + } + + protected Deserializer(JavaType javaType) { + super(ProtocolMessageEnum.class); + this.javaType = javaType; + } + + @Override + public JsonDeserializer createContextual(DeserializationContext context, BeanProperty beanProperty) { + // beanProperty is null when the type to deserialize is the top-level type or a generic type, not a type of a bean property + JavaType javaType = context.getContextualType(); + + if (javaType == null) { + javaType = beanProperty.getMember().getType(); + } + + return new Deserializer(javaType); + } + + @Override + public ProtocolMessageEnum deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { + int intValue = jsonParser.getIntValue(); + Class enumClass = Preconditions.checkNotNull(javaType, "javaType").getRawClass(); + try { + return (ProtocolMessageEnum) enumClass.getMethod("forNumber", int.class).invoke(null, intValue); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + context.reportWrongTokenException(ProtocolMessageEnum.class, JsonToken.VALUE_NUMBER_INT, wrongTokenMessage(context)); + // the previous method should have thrown + throw new AssertionError(); + } + } + } + + // TODO share this? + private static String wrongTokenMessage(DeserializationContext context) { + return "Can not deserialize instance of com.google.protobuf.ProtocolMessageEnum out of " + context.getParser().currentToken() + " token"; + } +} diff --git a/src/test/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumberTest.java b/src/test/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumberTest.java new file mode 100644 index 0000000..8700b47 --- /dev/null +++ b/src/test/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumberTest.java @@ -0,0 +1,56 @@ +package com.hubspot.jackson.datatype.protobuf; + +import static com.hubspot.jackson.datatype.protobuf.util.ObjectMapperHelper.camelCase; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.hubspot.jackson.datatype.protobuf.util.TestProtobuf; +import com.hubspot.jackson.datatype.protobuf.util.TestProtobuf.Enum; + +public class ProtobufEnumAsNumberTest { + + @Test + public void itSerializesToEnumNumber() { + ObjectMapper mapper = camelCase(); + + for (Enum anEnum : Enum.values()) { + ObjectWithProtobufEnumField input = new ObjectWithProtobufEnumField(); + input.anEnum = anEnum; + + JsonNode node = mapper.valueToTree(input); + + assertThat(node.has("anEnum")).isTrue(); + assertThat(node.get("anEnum").isInt()).isTrue(); + assertThat(node.get("anEnum").intValue()).isEqualTo(anEnum.getNumber()); + } + } + + @Test + public void itDeserializesFromEnumNumber() throws JsonProcessingException { + ObjectMapper mapper = camelCase(); + + for (Enum anEnum : Enum.values()) { + ObjectWithProtobufEnumField input = new ObjectWithProtobufEnumField(); + input.anEnum = anEnum; + + JsonNode node = mapper.valueToTree(input); + + ObjectWithProtobufEnumField output = mapper.treeToValue(node, ObjectWithProtobufEnumField.class); + assertThat(output).isNotSameAs(input); + assertThat(output.anEnum).isEqualTo(input.anEnum); + } + } + + private static class ObjectWithProtobufEnumField { + + @JsonSerialize(using = ProtobufEnumAsNumber.Serializer.class) + @JsonDeserialize(using = ProtobufEnumAsNumber.Deserializer.class) + private TestProtobuf.Enum anEnum; + } +} From 9d513eca6fda91ac2ee562bca82d2f57e356d9a2 Mon Sep 17 00:00:00 2001 From: Matt Ball Date: Mon, 11 Mar 2019 18:00:52 -0400 Subject: [PATCH 2/5] Move reflection out of `deserialize()` https://github.com/HubSpot/jackson-datatype-protobuf/pull/59#discussion_r263135496 --- .../protobuf/ProtobufEnumAsNumber.java | 40 +++++++++++---- .../protobuf/ProtobufEnumAsNumberTest.java | 49 +++++++++++++++++++ 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java index 370920c..0a9901f 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -10,6 +11,7 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; @@ -35,19 +37,21 @@ public void serialize(ProtocolMessageEnum protocolMessageEnum, JsonGenerator jso public static class Deserializer extends StdDeserializer implements ContextualDeserializer { - private final JavaType javaType; + private final Class enumClass; + private final Method forNumberMethod; protected Deserializer() { - this(null); + this(null, null); } - protected Deserializer(JavaType javaType) { + protected Deserializer(Class enumClass, Method forNumberMethod) { super(ProtocolMessageEnum.class); - this.javaType = javaType; + this.enumClass = enumClass; + this.forNumberMethod = forNumberMethod; } @Override - public JsonDeserializer createContextual(DeserializationContext context, BeanProperty beanProperty) { + public JsonDeserializer createContextual(DeserializationContext context, BeanProperty beanProperty) throws JsonMappingException { // beanProperty is null when the type to deserialize is the top-level type or a generic type, not a type of a bean property JavaType javaType = context.getContextualType(); @@ -55,16 +59,34 @@ public JsonDeserializer createContextual(DeserializationContext context, Bean javaType = beanProperty.getMember().getType(); } - return new Deserializer(javaType); + Class enumClass = javaType.getRawClass(); + + if (enumClass == this.enumClass + && this.forNumberMethod != null) { + // short circuit if this instance is already correctly configured + return this; + } + + Method forNumberMethod; + + try { + forNumberMethod = enumClass.getMethod("forNumber", int.class); + } catch (NoSuchMethodException e) { + context.reportBadDefinition(javaType, "Could not find a static forNumber(int) method on this type"); + // the previous method should have thrown + throw new AssertionError(); + } + + return new Deserializer(enumClass, forNumberMethod); } @Override public ProtocolMessageEnum deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { int intValue = jsonParser.getIntValue(); - Class enumClass = Preconditions.checkNotNull(javaType, "javaType").getRawClass(); + // Class enumClass = Preconditions.checkNotNull(javaType, "javaType").getRawClass(); try { - return (ProtocolMessageEnum) enumClass.getMethod("forNumber", int.class).invoke(null, intValue); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + return (ProtocolMessageEnum) Preconditions.checkNotNull(forNumberMethod, "forNumberMethod").invoke(null, intValue); + } catch (IllegalAccessException | InvocationTargetException e) { context.reportWrongTokenException(ProtocolMessageEnum.class, JsonToken.VALUE_NUMBER_INT, wrongTokenMessage(context)); // the previous method should have thrown throw new AssertionError(); diff --git a/src/test/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumberTest.java b/src/test/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumberTest.java index 8700b47..374a71b 100644 --- a/src/test/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumberTest.java +++ b/src/test/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumberTest.java @@ -2,10 +2,12 @@ import static com.hubspot.jackson.datatype.protobuf.util.ObjectMapperHelper.camelCase; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; import org.junit.Test; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -47,10 +49,57 @@ public void itDeserializesFromEnumNumber() throws JsonProcessingException { } } + @Test + public void itFailsWhenSerializingNonProtobufEnumFields() { + ObjectMapper mapper = camelCase(); + + for (NonProtobufEnum anEnum : NonProtobufEnum.values()) { + ObjectWithVanillaEnumField input = new ObjectWithVanillaEnumField(); + input.anEnum = anEnum; + + try { + mapper.valueToTree(input); + fail("expected an exception to be thrown"); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class); + } + } + } + + @Test + public void itFailsWhenDeserializingNonProtobufEnumFields() { + ObjectMapper mapper = camelCase(); + + for (NonProtobufEnum anEnum : NonProtobufEnum.values()) { + ObjectWithProtobufEnumField input = new ObjectWithProtobufEnumField(); + input.anEnum = Enum.valueOf(anEnum.name()); + + JsonNode node = mapper.valueToTree(input); + + try { + mapper.treeToValue(node, ObjectWithVanillaEnumField.class); + fail("expected an exception to be thrown"); + } catch (Exception e) { + assertThat(e).isInstanceOf(JsonMappingException.class); + } + } + } + private static class ObjectWithProtobufEnumField { @JsonSerialize(using = ProtobufEnumAsNumber.Serializer.class) @JsonDeserialize(using = ProtobufEnumAsNumber.Deserializer.class) private TestProtobuf.Enum anEnum; } + + private static class ObjectWithVanillaEnumField { + + @JsonSerialize(using = ProtobufEnumAsNumber.Serializer.class) + @JsonDeserialize(using = ProtobufEnumAsNumber.Deserializer.class) + private NonProtobufEnum anEnum; + } + + private enum NonProtobufEnum { + ONE, TWO + } } From f7322f17c0818ffdf94abb10ce8dc6bd5f0bd1f9 Mon Sep 17 00:00:00 2001 From: Matt Ball Date: Mon, 11 Mar 2019 18:13:07 -0400 Subject: [PATCH 3/5] Fix Findbugs SE_BAD_FIELD build failure --- .../jackson/datatype/protobuf/ProtobufEnumAsNumber.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java index 0a9901f..25a38ab 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.google.common.base.Preconditions; import com.google.protobuf.ProtocolMessageEnum; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; public final class ProtobufEnumAsNumber { @@ -35,10 +36,11 @@ public void serialize(ProtocolMessageEnum protocolMessageEnum, JsonGenerator jso } } + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") public static class Deserializer extends StdDeserializer implements ContextualDeserializer { - private final Class enumClass; - private final Method forNumberMethod; + private final transient Class enumClass; + private final transient Method forNumberMethod; protected Deserializer() { this(null, null); From 16bdee1ea1bf6472c3d223bac1b892dad7599e24 Mon Sep 17 00:00:00 2001 From: Matt Ball Date: Mon, 11 Mar 2019 18:35:07 -0400 Subject: [PATCH 4/5] Suppress more findbugs serialization nonsense --- .../hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java index 25a38ab..f93c61e 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java @@ -36,7 +36,7 @@ public void serialize(ProtocolMessageEnum protocolMessageEnum, JsonGenerator jso } } - @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + @SuppressFBWarnings({"SE_TRANSIENT_FIELD_NOT_RESTORED", "SE_NO_SERIALVERSIONID"}) public static class Deserializer extends StdDeserializer implements ContextualDeserializer { private final transient Class enumClass; From d744502c86037d039de4ef019376f7b6143b9fee Mon Sep 17 00:00:00 2001 From: Matt Ball Date: Mon, 11 Mar 2019 18:42:09 -0400 Subject: [PATCH 5/5] Organize imports --- .../hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java index f93c61e..9d7ad97 100644 --- a/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java +++ b/src/main/java/com/hubspot/jackson/datatype/protobuf/ProtobufEnumAsNumber.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.google.common.base.Preconditions; import com.google.protobuf.ProtocolMessageEnum; + import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; public final class ProtobufEnumAsNumber {