-
Notifications
You must be signed in to change notification settings - Fork 48
Add a generic enum-as-number serdes #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: jackson29-proto3
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package com.hubspot.jackson.datatype.protobuf; | ||
|
||
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; | ||
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.JsonMappingException; | ||
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; | ||
|
||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | ||
|
||
public final class ProtobufEnumAsNumber { | ||
|
||
private ProtobufEnumAsNumber() {} | ||
|
||
public static class Serializer extends StdSerializer<ProtocolMessageEnum> { | ||
|
||
protected Serializer() { | ||
super(ProtocolMessageEnum.class); | ||
} | ||
|
||
@Override | ||
public void serialize(ProtocolMessageEnum protocolMessageEnum, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { | ||
jsonGenerator.writeNumber(protocolMessageEnum.getNumber()); | ||
} | ||
} | ||
|
||
@SuppressFBWarnings({"SE_TRANSIENT_FIELD_NOT_RESTORED", "SE_NO_SERIALVERSIONID"}) | ||
public static class Deserializer extends StdDeserializer<ProtocolMessageEnum> implements ContextualDeserializer { | ||
|
||
private final transient Class<?> enumClass; | ||
private final transient Method forNumberMethod; | ||
|
||
protected Deserializer() { | ||
this(null, null); | ||
} | ||
|
||
protected Deserializer(Class<?> enumClass, Method forNumberMethod) { | ||
super(ProtocolMessageEnum.class); | ||
this.enumClass = enumClass; | ||
this.forNumberMethod = forNumberMethod; | ||
} | ||
|
||
@Override | ||
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(); | ||
|
||
if (javaType == null) { | ||
javaType = beanProperty.getMember().getType(); | ||
} | ||
|
||
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(); | ||
try { | ||
return (ProtocolMessageEnum) Preconditions.checkNotNull(forNumberMethod, "forNumberMethod").invoke(null, intValue); | ||
} catch (IllegalAccessException | InvocationTargetException e) { | ||
context.reportWrongTokenException(ProtocolMessageEnum.class, JsonToken.VALUE_NUMBER_INT, wrongTokenMessage(context)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I borrowed this pattern from the other deserializer implementations, but I'm not sure if this is really a wrong-token exception. |
||
// 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"; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
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 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; | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Happy to add more test cases if you think there are major blind spots. |
||
|
||
@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); | ||
} | ||
} | ||
|
||
@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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy to reorganize this into two separate classes if you prefer that style.